]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch '0.9.x' into testing
authorBrion Vibber <brion@pobox.com>
Tue, 25 Jan 2011 20:57:49 +0000 (12:57 -0800)
committerBrion Vibber <brion@pobox.com>
Tue, 25 Jan 2011 20:57:49 +0000 (12:57 -0800)
67 files changed:
actions/apiaccountupdatedeliverydevice.php
actions/apiaccountupdateprofile.php
actions/apiaccountupdateprofilebackgroundimage.php
actions/apiaccountupdateprofilecolors.php
actions/apiaccountupdateprofileimage.php
actions/apigroupmembership.php
actions/apistatusesupdate.php
actions/apitimelineuser.php
actions/atompubsubscriptionfeed.php
actions/avatarsettings.php
actions/deleteaccount.php
actions/grouplogo.php
actions/newgroup.php
actions/profilesettings.php
actions/subscribers.php
actions/subscriptions.php
classes/Notice.php
classes/User_group.php
db/notice_source.sql
lib/accountmover.php [new file with mode: 0644]
lib/accountsettingsaction.php
lib/activity.php
lib/activitymover.php [new file with mode: 0644]
lib/activitysink.php [new file with mode: 0644]
lib/apiaction.php
lib/arraywrapper.php
lib/common.php
lib/connectsettingsaction.php
lib/discovery.php [new file with mode: 0644]
lib/imagefile.php
lib/linkheader.php [new file with mode: 0644]
lib/logingroupnav.php
lib/mail.php
lib/profileaction.php
lib/queuemanager.php
lib/useractivitystream.php
lib/userprofile.php
lib/xrd.php
lib/xrdaction.php
plugins/Bookmark/Bookmark.php
plugins/CasAuthentication/CasAuthenticationPlugin.php
plugins/Facebook/FacebookPlugin.php
plugins/FacebookBridge/FacebookBridgePlugin.php
plugins/FacebookBridge/actions/facebookfinishlogin.php
plugins/InfiniteScroll/infinitescroll.js
plugins/InfiniteScroll/jquery.infinitescroll.js
plugins/NewMenu/NewMenuPlugin.php
plugins/OStatus/OStatusPlugin.php
plugins/OStatus/classes/Magicsig.php
plugins/OStatus/classes/Ostatus_profile.php
plugins/OStatus/lib/discovery.php [deleted file]
plugins/OStatus/lib/linkheader.php [deleted file]
plugins/OStatus/lib/magicenvelope.php
plugins/OStatus/lib/salmon.php
plugins/OStatus/tests/MagicEnvelopeTest.php [new file with mode: 0644]
plugins/OStatus/tests/remote-tests.php
plugins/OStatus/tests/slap.php [new file with mode: 0644]
plugins/OpenID/OpenIDPlugin.php
plugins/OpenID/finishopenidlogin.php
plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php
plugins/SubMirror/SubMirrorPlugin.php
plugins/SubMirror/actions/mirrorsettings.php
plugins/TwitterBridge/TwitterBridgePlugin.php
plugins/TwitterBridge/twitterauthorization.php
plugins/UserFlag/UserFlagPlugin.php
scripts/moveuser.php [new file with mode: 0644]
tests/ActivityParseTests.php

index ec87c4c953b74e5fedcdcb44e0a09d1b58622b3d..a9ccb4fca8062aef3b8c848599d839a10753eb7d 100644 (file)
@@ -143,7 +143,7 @@ class ApiAccountUpdateDeliveryDeviceAction extends ApiAuthAction
 
         if ($this->format == 'xml') {
             $this->initDocument('xml');
-            $this->showTwitterXmlUser($twitter_user);
+            $this->showTwitterXmlUser($twitter_user, 'user', true);
             $this->endDocument('xml');
         } elseif ($this->format == 'json') {
             $this->initDocument('json');
index 163dac42d874907aaed4aedd4ec5c29873123835..d0b9abe9b79fffcf3b8d5dbb6e56574e094d4d69 100644 (file)
@@ -154,7 +154,7 @@ class ApiAccountUpdateProfileAction extends ApiAuthAction
 
         if ($this->format == 'xml') {
             $this->initDocument('xml');
-            $this->showTwitterXmlUser($twitter_user);
+            $this->showTwitterXmlUser($twitter_user, 'user', true);
             $this->endDocument('xml');
         } elseif ($this->format == 'json') {
             $this->initDocument('json');
index badd8db002a71d43d295a0d5f2c8bd798cc98eab..f26c30198d40335dec3139550ee616c3ac898d5f 100644 (file)
@@ -204,7 +204,7 @@ class ApiAccountUpdateProfileBackgroundImageAction extends ApiAuthAction
 
         if ($this->format == 'xml') {
             $this->initDocument('xml');
-            $this->showTwitterXmlUser($twitter_user);
+            $this->showTwitterXmlUser($twitter_user, 'user', true);
             $this->endDocument('xml');
         } elseif ($this->format == 'json') {
             $this->initDocument('json');
index 109fbf959e2f545ddf861caeceb1ed2fdb7f6646..4c102c4090f6f9fa1e5a9953811519e0a914f2a3 100644 (file)
@@ -188,7 +188,7 @@ class ApiAccountUpdateProfileColorsAction extends ApiAuthAction
 
         if ($this->format == 'xml') {
             $this->initDocument('xml');
-            $this->showTwitterXmlUser($twitter_user);
+            $this->showTwitterXmlUser($twitter_user, 'user', true);
             $this->endDocument('xml');
         } elseif ($this->format == 'json') {
             $this->initDocument('json');
index f2886509d7cf4338ba994ab22e84c7ad90add934..986a8f3f1eed69c5c11d46dec84112b1e29458cb 100644 (file)
@@ -112,16 +112,17 @@ class ApiAccountUpdateProfileImageAction extends ApiAuthAction
             return;
         }
 
+        $type = $imagefile->preferredType();
         $filename = Avatar::filename(
             $user->id,
-            image_type_to_extension($imagefile->type),
+            image_type_to_extension($type),
             null,
             'tmp'.common_timestamp()
         );
 
         $filepath = Avatar::path($filename);
 
-        move_uploaded_file($imagefile->filepath, $filepath);
+        $imagefile->copyTo($filepath);
 
         $profile = $this->user->getProfile();
 
@@ -139,7 +140,7 @@ class ApiAccountUpdateProfileImageAction extends ApiAuthAction
 
         if ($this->format == 'xml') {
             $this->initDocument('xml');
-            $this->showTwitterXmlUser($twitter_user);
+            $this->showTwitterXmlUser($twitter_user, 'user', true);
             $this->endDocument('xml');
         } elseif ($this->format == 'json') {
             $this->initDocument('json');
index 99ac965fa18d2c5aa9de82cf91ef317a3be8e025..939d22d757fb5a2f2c19939bc1dd903352f30144 100644 (file)
@@ -66,6 +66,12 @@ class ApiGroupMembershipAction extends ApiPrivateAuthAction
         parent::prepare($args);
 
         $this->group    = $this->getTargetGroup($this->arg('id'));
+        if (empty($this->group)) {
+            // TRANS: Client error displayed trying to show group membership on a non-existing group.
+            $this->clientError(_('Group not found.'), 404, $this->format);
+            return false;
+        }
+
         $this->profiles = $this->getProfiles();
 
         return true;
@@ -84,12 +90,6 @@ class ApiGroupMembershipAction extends ApiPrivateAuthAction
     {
         parent::handle($args);
 
-        if (empty($this->group)) {
-            // TRANS: Client error displayed trying to show group membership on a non-existing group.
-            $this->clientError(_('Group not found.'), 404, $this->format);
-            return false;
-        }
-
         // XXX: RSS and Atom
 
         switch($this->format) {
index a8ec7f8bb9d647cec9ad4170c366a46d276c537a..5773bdc2e8106ffc4fd8e7787bede0a4616b2dbc 100644 (file)
@@ -377,7 +377,7 @@ class ApiStatusesUpdateAction extends ApiAuthAction
     function supported($cmd)
     {
         static $cmdlist = array('MessageCommand', 'SubCommand', 'UnsubCommand',
-            'FavCommand', 'OnCommand', 'OffCommand');
+            'FavCommand', 'OnCommand', 'OffCommand', 'JoinCommand', 'LeaveCommand');
 
         if (in_array(get_class($cmd), $cmdlist)) {
             return true;
index 85e8f97b2acfabdbd727f99ebd183bf9b6b4c5c6..c1e66021b6a4046c069bb455dade60ad58ebcd86 100644 (file)
@@ -430,11 +430,11 @@ class ApiTimelineUserAction extends ApiBareAuthAction
                 $profile = Profile::fromURI($uri);
 
                 if (!empty($profile)) {
-                    $options['replies'] = $uri;
+                    $options['replies'][] = $uri;
                 } else {
                     $group = User_group::staticGet('uri', $uri);
                     if (!empty($group)) {
-                        $options['groups'] = $uri;
+                        $options['groups'][] = $uri;
                     } else {
                         // @fixme: hook for discovery here
                         common_log(LOG_WARNING, sprintf('AtomPub post with unknown attention URI %s', $uri));
index 0893c4b045405515077ff7b49c667f9a461275d7..43e7933272c77de7d95792b11dba1775944a565e 100644 (file)
@@ -268,6 +268,14 @@ class AtompubsubscriptionfeedAction extends ApiAuthAction
                 return;
             }
 
+            if (Subscription::exists($this->_profile, $profile)) {
+                // 409 Conflict
+                $this->clientError(sprintf(_('Already subscribed to %s'),
+                                           $person->id), 
+                                   409);
+                return;
+            }
+
             if (Subscription::start($this->_profile, $profile)) {
                 $sub = Subscription::pkeyGet(array('subscriber' => $this->_profile->id,
                                                    'subscribed' => $profile->id));
index 375420c5c9d5fc656f20b80054333438e28e1bb1..10c82ebfd72d722d644e5860428d33854a59b35f 100644 (file)
@@ -320,21 +320,20 @@ class AvatarsettingsAction extends AccountSettingsAction
         }
 
         $cur = common_current_user();
-
+        $type = $imagefile->preferredType();
         $filename = Avatar::filename($cur->id,
-                                     image_type_to_extension($imagefile->type),
+                                     image_type_to_extension($type),
                                      null,
                                      'tmp'.common_timestamp());
 
         $filepath = Avatar::path($filename);
-
-        move_uploaded_file($imagefile->filepath, $filepath);
+        $imagefile->copyTo($filepath);
 
         $filedata = array('filename' => $filename,
                           'filepath' => $filepath,
                           'width' => $imagefile->width,
                           'height' => $imagefile->height,
-                          'type' => $imagefile->type);
+                          'type' => $type);
 
         $_SESSION['FILEDATA'] = $filedata;
 
index dd587d04453b9c2370ea6db5d7f26baca6c7770a..614519d47430590f0b5b499de6155ef842edce3d 100644 (file)
@@ -161,7 +161,7 @@ class DeleteaccountAction extends Action
         if ($this->trimmed('iamsure') != $iamsure ) {
             // TRANS: Notification for user about the text that must be input to be able to delete a user account.
             // TRANS: %s is the text that needs to be input.
-            $this->_error = sprintf(_('You must write  "%s" exactly in the box.', $iamsure));
+            $this->_error = sprintf(_('You must write "%s" exactly in the box.'), $iamsure);
             $this->showPage();
             return;
         }
index 7b7c01b23b9fc7425d63585971669e713e86207d..d2e8fd0e91bf290dda890e154ee238b1add0d759 100644 (file)
@@ -353,20 +353,21 @@ class GrouplogoAction extends GroupDesignAction
             return;
         }
 
+        $type = $imagefile->preferredType();
         $filename = Avatar::filename($this->group->id,
-                                     image_type_to_extension($imagefile->type),
+                                     image_type_to_extension($type),
                                      null,
                                      'group-temp-'.common_timestamp());
 
         $filepath = Avatar::path($filename);
 
-        move_uploaded_file($imagefile->filepath, $filepath);
+        $imagefile->copyTo($filepath);
 
         $filedata = array('filename' => $filename,
                           'filepath' => $filepath,
                           'width' => $imagefile->width,
                           'height' => $imagefile->height,
-                          'type' => $imagefile->type);
+                          'type' => $type);
 
         $_SESSION['FILEDATA'] = $filedata;
 
index 42d488e54ebfef3a09bfd7b3ce15a96a01cf2807..fb7f6347d4c35504a004d1004fa8d87e42c8f77f 100644 (file)
@@ -200,8 +200,6 @@ class NewgroupAction extends Action
             }
         }
 
-        $mainpage = common_local_url('showgroup', array('nickname' => $nickname));
-
         $cur = common_current_user();
 
         // Checked in prepare() above
@@ -215,7 +213,6 @@ class NewgroupAction extends Action
                                             'location' => $location,
                                             'aliases'  => $aliases,
                                             'userid'   => $cur->id,
-                                            'mainpage' => $mainpage,
                                             'local'    => true));
 
         common_redirect($group->homeUrl(), 303);
index 7227c5c7e96974ee4bbcc709bbe0273a7d4434b4..ee50e104d8d2162e0248d5983b355bd08ec89654 100644 (file)
@@ -454,6 +454,9 @@ class ProfilesettingsAction extends AccountSettingsAction
 
         $this->elementStart('div', array('id' => 'aside_primary',
                                          'class' => 'aside'));
+
+        $this->elementStart('div', array('id' => 'account_actions',
+                                         'class' => 'section'));
         $this->elementStart('ul');
         if (Event::handle('StartProfileSettingsActions', array($this))) {
             if ($user->hasRight(Right::BACKUPACCOUNT)) {
@@ -484,5 +487,6 @@ class ProfilesettingsAction extends AccountSettingsAction
         }
         $this->elementEnd('ul');
         $this->elementEnd('div');
+        $this->elementEnd('div');
     }
 }
index 2862f35c6d275eac8596c8b686ba3434dcb5f1c3..ad522a4bae63372b0632f302ec794a329ba88735 100644 (file)
@@ -100,8 +100,6 @@ class SubscribersAction extends GalleryAction
             }
         }
 
-        $subscribers->free();
-
         $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE,
                           $this->page, 'subscribers',
                           array('nickname' => $this->user->nickname));
index a814a4f354172dd4d24ec2c3156590f576ecdc01..ddcf237e6271647552f19c7253fa9535b24b5082 100644 (file)
@@ -106,8 +106,6 @@ class SubscriptionsAction extends GalleryAction
                 }
             }
 
-            $subscriptions->free();
-
             $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE,
                               $this->page, 'subscriptions',
                               array('nickname' => $this->user->nickname));
index 561999966c5842bb3ad2df84beae942d2e8d1416..0910ba39b7c650b501402569c65b11368970df8e 100644 (file)
@@ -819,9 +819,18 @@ class Notice extends Memcached_DataObject
 
         // Exclude any deleted, non-local, or blocking recipients.
         $profile = $this->getProfile();
+        $originalProfile = null;
+        if ($this->repeat_of) {
+            // Check blocks against the original notice's poster as well.
+            $original = Notice::staticGet('id', $this->repeat_of);
+            if ($original) {
+                $originalProfile = $original->getProfile();
+            }
+        }
         foreach ($ni as $id => $source) {
             $user = User::staticGet('id', $id);
-            if (empty($user) || $user->hasBlocked($profile)) {
+            if (empty($user) || $user->hasBlocked($profile) ||
+                ($originalProfile && $user->hasBlocked($originalProfile))) {
                 unset($ni[$id]);
             }
         }
index 68f61cb7f48a104b43aae7b84fcf5ba4503d7ca3..d402ed47733e3b631bca94c153bac8a28ed1dbbf 100644 (file)
@@ -487,6 +487,7 @@ class User_group extends Memcached_DataObject
         }
 
         // MAGICALLY put fields into current scope
+        // @fixme kill extract(); it makes debugging absurdly hard
 
         extract($fields);
 
@@ -498,6 +499,9 @@ class User_group extends Memcached_DataObject
             // fill in later...
             $uri = null;
         }
+        if (empty($mainpage)) {
+            $mainpage = common_local_url('showgroup', array('nickname' => $nickname));
+        }
 
         $group->nickname    = $nickname;
         $group->fullname    = $fullname;
index 82074077b4b93a41160dcb6d2bc44d48b12a2c8e..b13cfb3e3b8cf835bb15fa467aed672bf101e2f4 100644 (file)
@@ -77,4 +77,5 @@ VALUES
     ('twitvim','TwitVim','http://vim.sourceforge.net/scripts/script.php?script_id=2204', now()),
     ('Updating.Me','Updating.Me','http://updating.me/', now()),
     ('urfastr','urfastr','http://urfastr.net/', now()),
-    ('yatca','Yatca','http://www.yatca.com/', now());
+    ('yatca','Yatca','http://www.yatca.com/', now()),
+    ('rss.me', 'rss.me', 'http://rss.me/', now());
diff --git a/lib/accountmover.php b/lib/accountmover.php
new file mode 100644 (file)
index 0000000..85c95c1
--- /dev/null
@@ -0,0 +1,147 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A class for moving an account to a new server
+ * 
+ * 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);
+}
+
+/**
+ * Moves an account from this server to another
+ *
+ * @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 AccountMover extends QueueHandler
+{
+    function transport()
+    {
+        return 'acctmove';
+    }
+
+    function handle($object)
+    {
+        list($user, $remote, $password) = $object;
+
+        $remote = Discovery::normalize($remote);
+
+        $oprofile = Ostatus_profile::ensureProfileURI($remote);
+
+        if (empty($oprofile)) {
+            throw new Exception("Can't locate account {$remote}");
+        }
+
+        list($svcDocUrl, $username) = self::getServiceDocument($remote);
+
+        $sink = new ActivitySink($svcDocUrl, $username, $password);
+
+        $this->log(LOG_INFO, 
+                   "Moving user {$user->nickname} ".
+                   "to {$remote}.");
+
+        $stream = new UserActivityStream($user);
+
+        // Reverse activities to run in correct chron order
+
+        $acts = array_reverse($stream->activities);
+
+        $this->log(LOG_INFO,
+                   "Got ".count($acts)." activities ".
+                   "for {$user->nickname}.");
+
+        $qm = QueueManager::get();
+
+        foreach ($acts as $act) {
+            $qm->enqueue(array($act, $sink, $user->uri, $remote), 'actmove');
+        }
+
+        $this->log(LOG_INFO,
+                   "Finished moving user {$user->nickname} ".
+                   "to {$remote}.");
+    }
+
+    static function getServiceDocument($remote)
+    {
+        $discovery = new Discovery();
+
+        $xrd = $discovery->lookup($remote);
+
+        if (empty($xrd)) {
+            throw new Exception("Can't find XRD for $remote");
+        } 
+
+        $svcDocUrl = null;
+        $username  = null;
+
+        foreach ($xrd->links as $link) {
+            if ($link['rel'] == 'http://apinamespace.org/atom' &&
+                $link['type'] == 'application/atomsvc+xml') {
+                $svcDocUrl = $link['href'];
+                if (!empty($link['property'])) {
+                    foreach ($link['property'] as $property) {
+                        if ($property['type'] == 'http://apinamespace.org/atom/username') {
+                            $username = $property['value'];
+                            break;
+                        }
+                    }
+                }
+                break;
+            }
+        }
+
+        if (empty($svcDocUrl)) {
+            throw new Exception("No AtomPub API service for $remote.");
+        }
+
+        return array($svcDocUrl, $username);
+    }
+
+    /**
+     * Log some data
+     * 
+     * Add a header for our class so we know who did it.
+     *
+     * @param int    $level   Log level, like LOG_ERR or LOG_INFO
+     * @param string $message Message to log
+     *
+     * @return void
+     */
+
+    protected function log($level, $message)
+    {
+        common_log($level, "AccountMover: " . $message);
+    }
+}
index 7991c9002c0fa287f8d2a0d25b34f69c9dd008f0..b1ea998bfab2b21d233d6250391794627a02e675 100644 (file)
@@ -96,7 +96,7 @@ class AccountSettingsNav extends Widget
         $action_name = $this->action->trimmed('action');
         $this->action->elementStart('ul', array('class' => 'nav'));
 
-        if (Event::handle('StartAccountSettingsNav', array(&$this->action))) {
+        if (Event::handle('StartAccountSettingsNav', array($this->action))) {
             $user = common_current_user();
 
             if(Event::handle('StartAccountSettingsProfileMenuItem', array($this, &$menu))){
@@ -142,7 +142,7 @@ class AccountSettingsNav extends Widget
                 Event::handle('EndAccountSettingsOtherMenuItem', array($this, &$menu));
             }
 
-            Event::handle('EndAccountSettingsNav', array(&$this->action));
+            Event::handle('EndAccountSettingsNav', array($this->action));
         }
 
         $this->action->elementEnd('ul');
index 8d7ae1540b7b23b06daba33d30ff580a13227235..802d09304a7c3750e8296d3cad062373fae08b24 100644 (file)
@@ -182,6 +182,9 @@ class Activity
         $actorEl = $this->_child($entry, self::ACTOR);
 
         if (!empty($actorEl)) {
+            // Standalone <activity:actor> elements are a holdover from older
+            // versions of ActivityStreams. Newer feeds should have this data
+            // integrated straight into <atom:author>.
 
             $this->actor = new ActivityObject($actorEl);
 
@@ -196,19 +199,24 @@ class Activity
                     $this->actor->id = $authorObj->id;
                 }
             }
-        } else if (!empty($feed) &&
-                   $subjectEl = $this->_child($feed, self::SUBJECT)) {
-
-            $this->actor = new ActivityObject($subjectEl);
-
         } else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) {
 
+            // An <atom:author> in the entry overrides any author info on
+            // the surrounding feed.
             $this->actor = new ActivityObject($authorEl);
 
         } else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR,
                                                               self::ATOM)) {
 
+            // If there's no <atom:author> on the entry, it's safe to assume
+            // the containing feed's authorship info applies.
             $this->actor = new ActivityObject($authorEl);
+        } else if (!empty($feed) &&
+                   $subjectEl = $this->_child($feed, self::SUBJECT)) {
+
+            // Feed subject is used for things like groups.
+            // Should actually possibly not be interpreted as an actor...?
+            $this->actor = new ActivityObject($subjectEl);
         }
 
         $contextEl = $this->_child($entry, self::CONTEXT);
@@ -322,6 +330,7 @@ class Activity
      *
      * @return DOMElement Atom entry
      */
+
     function toAtomEntry()
     {
         return null;
@@ -330,7 +339,12 @@ class Activity
     function asString($namespace=false, $author=true, $source=false)
     {
         $xs = new XMLStringer(true);
+        $this->outputTo($xs, $namespace, $author, $source);
+        return $xs->getString();
+    }
 
+    function outputTo($xs, $namespace=false, $author=true, $source=false)
+    {
         if ($namespace) {
             $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
                            'xmlns:thr' => 'http://purl.org/syndication/thread/1.0',
@@ -518,9 +532,7 @@ class Activity
 
         $xs->elementEnd('entry');
 
-        $str = $xs->getString();
-       
-        return $str;
+        return;
     }
 
     private function _child($element, $tag, $namespace=self::SPEC)
diff --git a/lib/activitymover.php b/lib/activitymover.php
new file mode 100644 (file)
index 0000000..7032331
--- /dev/null
@@ -0,0 +1,168 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Title of module
+ * 
+ * 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 ActivityMover extends QueueHandler
+{
+    function transport()
+    {
+        return 'actmove';
+    }
+    
+    function handle($data)
+    {
+        list ($act, $sink, $userURI, $remoteURI) = $data;
+
+        $user   = User::staticGet('uri', $userURI);
+        $remote = Profile::fromURI($remoteURI);
+
+        try {
+            $this->moveActivity($act, $sink, $user, $remote);
+        } catch (ClientException $cex) {
+            $this->log(LOG_WARNING,
+                       $cex->getMessage());
+            // "don't retry me"
+            return true;
+        } catch (ServerException $sex) {
+            $this->log(LOG_WARNING,
+                       $sex->getMessage());
+            // "retry me" (because we think the server might handle it next time)
+            return false;
+        } catch (Exception $ex) {
+            $this->log(LOG_WARNING,
+                       $ex->getMessage());
+            // "don't retry me"
+            return true;
+        }
+    }
+
+    function moveActivity($act, $sink, $user, $remote)
+    {
+        if (empty($user)) {
+            throw new Exception("No such user {$act->actor->id}");
+        }
+
+        switch ($act->verb) {
+        case ActivityVerb::FAVORITE:
+            $this->log(LOG_INFO,
+                       "Moving favorite of {$act->objects[0]->id} by ".
+                       "{$act->actor->id} to {$remote->nickname}.");
+            // push it, then delete local
+            $sink->postActivity($act);
+            $notice = Notice::staticGet('uri', $act->objects[0]->id);
+            if (!empty($notice)) {
+                $fave = Fave::pkeyGet(array('user_id' => $user->id,
+                                            'notice_id' => $notice->id));
+                $fave->delete();
+            }
+            break;
+        case ActivityVerb::POST:
+            $this->log(LOG_INFO,
+                       "Moving notice {$act->objects[0]->id} by ".
+                       "{$act->actor->id} to {$remote->nickname}.");
+            // XXX: send a reshare, not a post
+            $sink->postActivity($act);
+            $notice = Notice::staticGet('uri', $act->objects[0]->id);
+            if (!empty($notice)) {
+                $notice->delete();
+            }
+            break;
+        case ActivityVerb::JOIN:
+            $this->log(LOG_INFO,
+                       "Moving group join of {$act->objects[0]->id} by ".
+                       "{$act->actor->id} to {$remote->nickname}.");
+            $sink->postActivity($act);
+            $group = User_group::staticGet('uri', $act->objects[0]->id);
+            if (!empty($group)) {
+                Group_member::leave($group->id, $user->id);
+            }
+            break;
+        case ActivityVerb::FOLLOW:
+            if ($act->actor->id == $user->uri) {
+                $this->log(LOG_INFO,
+                           "Moving subscription to {$act->objects[0]->id} by ".
+                           "{$act->actor->id} to {$remote->nickname}.");
+                $sink->postActivity($act);
+                $other = Profile::fromURI($act->objects[0]->id);
+                if (!empty($other)) {
+                    Subscription::cancel($user->getProfile(), $other);
+                }
+            } else {
+                $otherUser = User::staticGet('uri', $act->actor->id);
+                if (!empty($otherUser)) {
+                    $this->log(LOG_INFO,
+                               "Changing sub to {$act->objects[0]->id}".
+                               "by {$act->actor->id} to {$remote->nickname}.");
+                    $otherProfile = $otherUser->getProfile();
+                    Subscription::start($otherProfile, $remote);
+                    Subscription::cancel($otherProfile, $user->getProfile());
+                } else {
+                    $this->log(LOG_NOTICE,
+                               "Not changing sub to {$act->objects[0]->id}".
+                               "by remote {$act->actor->id} ".
+                               "to {$remote->nickname}.");
+                }
+            }
+            break;
+        }
+    }
+
+    /**
+     * Log some data
+     * 
+     * Add a header for our class so we know who did it.
+     *
+     * @param int    $level   Log level, like LOG_ERR or LOG_INFO
+     * @param string $message Message to log
+     *
+     * @return void
+     */
+
+    protected function log($level, $message)
+    {
+        common_log($level, "ActivityMover: " . $message);
+    }
+}
diff --git a/lib/activitysink.php b/lib/activitysink.php
new file mode 100644 (file)
index 0000000..fbe1f1e
--- /dev/null
@@ -0,0 +1,169 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A remote, atompub-receiving service
+ * 
+ * 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  AtomPub
+ * @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);
+}
+
+/**
+ * A remote service that supports AtomPub
+ *
+ * @category  AtomPub
+ * @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 ActivitySink
+{
+    protected $svcDocUrl   = null;
+    protected $username    = null;
+    protected $password    = null;
+    protected $collections = array();
+
+    function __construct($svcDocUrl, $username, $password)
+    {
+        $this->svcDocUrl = $svcDocUrl;
+        $this->username  = $username;
+        $this->password  = $password;
+
+        $this->_parseSvcDoc();
+    }
+
+    private function _parseSvcDoc()
+    {
+        $client   = new HTTPClient();
+        $response = $client->get($this->svcDocUrl);
+
+        if ($response->getStatus() != 200) {
+            throw new Exception("Can't get {$this->svcDocUrl}; response status " . $response->getStatus());
+        }
+
+        $xml = $response->getBody();
+
+        $dom = new DOMDocument();
+
+        // We don't want to bother with white spaces
+        $dom->preserveWhiteSpace = false;
+
+        // Don't spew XML warnings to output
+        $old = error_reporting();
+        error_reporting($old & ~E_WARNING);
+        $ok = $dom->loadXML($xml);
+        error_reporting($old);
+
+        $path = new DOMXPath($dom);
+
+        $path->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
+        $path->registerNamespace('app', 'http://www.w3.org/2007/app');
+        $path->registerNamespace('activity', 'http://activitystrea.ms/spec/1.0/');
+
+        $collections = $path->query('//app:collection');
+
+        for ($i = 0; $i < $collections->length; $i++) {
+            $collection = $collections->item($i);
+            $url = $collection->getAttribute('href');
+            $takesEntries = false;
+            $accepts = $path->query('app:accept', $collection);
+            for ($j = 0; $j < $accepts->length; $j++) {
+                $accept = $accepts->item($j);
+                $acceptValue = $accept->nodeValue;
+                if (preg_match('#application/atom\+xml(;\s*type=entry)?#', $acceptValue)) {
+                    $takesEntries = true;
+                    break;
+                }
+            }
+            if (!$takesEntries) {
+                continue;
+            }
+            $verbs = $path->query('activity:verb', $collection);
+            if ($verbs->length == 0) {
+                $this->_addCollection(ActivityVerb::POST, $url);
+            } else {
+                for ($k = 0; $k < $verbs->length; $k++) {
+                    $verb = $verbs->item($k);
+                    $this->_addCollection($verb->nodeValue, $url);
+                }
+            }
+        }
+    }
+
+    private function _addCollection($verb, $url)
+    {
+        if (array_key_exists($verb, $this->collections)) {
+            $this->collections[$verb][] = $url;
+        } else {
+            $this->collections[$verb] = array($url);
+        }
+        return;
+    }
+
+    function postActivity($activity)
+    {
+        if (!array_key_exists($activity->verb, $this->collections)) {
+            throw new Exception("No collection for verb {$activity->verb}");
+        } else {
+            if (count($this->collections[$activity->verb]) > 1) {
+                common_log(LOG_NOTICE, "More than one collection for verb {$activity->verb}");
+            }
+            $this->postToCollection($this->collections[$activity->verb][0], $activity);
+        }
+    }
+
+    function postToCollection($url, $activity)
+    {
+        $client = new HTTPClient($url);
+
+        $client->setMethod('POST');
+        $client->setAuth($this->username, $this->password);
+        $client->setHeader('Content-Type', 'application/atom+xml;type=entry');
+        $client->setBody($activity->asString(true, true, true));
+
+        $response = $client->send();
+
+        $status = $response->getStatus();
+        $reason = $response->getReasonPhrase();
+
+        if ($status >= 200 && $status < 300) {
+            return true;
+        } else if ($status >= 400 && $status < 500) {
+            throw new ClientException("{$url} {$status} {$reason}");
+        } else if ($status >= 500 && $status < 600) {
+            throw new ServerException("{$url} {$status} {$reason}");
+        } else {
+            // That's unexpected.
+            throw new Exception("{$url} {$status} {$reason}");
+        }
+    }
+}
index 2de513cbb1ab71431ce0d2918e3d054352144625..dcce18ef2783c4d6869d0537a9758f1501c9eaf8 100644 (file)
@@ -1437,41 +1437,23 @@ class ApiAction extends Action
     {
         if (empty($id)) {
             if (self::is_decimal($this->arg('id'))) {
-                return User_group::staticGet($this->arg('id'));
+                return User_group::staticGet('id', $this->arg('id'));
             } else if ($this->arg('id')) {
-                $nickname = common_canonical_nickname($this->arg('id'));
-                $local = Local_group::staticGet('nickname', $nickname);
-                if (empty($local)) {
-                    return null;
-                } else {
-                    return User_group::staticGet('id', $local->id);
-                }
+                return User_group::getForNickname($this->arg('id'));
             } else if ($this->arg('group_id')) {
-                // This is to ensure that a non-numeric user_id still
-                // overrides screen_name even if it doesn't get used
+                // This is to ensure that a non-numeric group_id still
+                // overrides group_name even if it doesn't get used
                 if (self::is_decimal($this->arg('group_id'))) {
                     return User_group::staticGet('id', $this->arg('group_id'));
                 }
             } else if ($this->arg('group_name')) {
-                $nickname = common_canonical_nickname($this->arg('group_name'));
-                $local = Local_group::staticGet('nickname', $nickname);
-                if (empty($local)) {
-                    return null;
-                } else {
-                    return User_group::staticGet('id', $local->group_id);
-                }
+                return User_group::getForNickname($this->arg('group_name'));
             }
 
         } else if (self::is_decimal($id)) {
-            return User_group::staticGet($id);
+            return User_group::staticGet('id', $id);
         } else {
-            $nickname = common_canonical_nickname($id);
-            $local = Local_group::staticGet('nickname', $nickname);
-            if (empty($local)) {
-                return null;
-            } else {
-                return User_group::staticGet('id', $local->group_id);
-            }
+            return User_group::getForNickname($id);
         }
     }
 
index 8a1cdd96e1f03363a766361b0582f3bfc7acf6ce..f9d3c3cf946313d14b35b2ee4cfca2fd1e59a706 100644 (file)
@@ -76,6 +76,10 @@ class ArrayWrapper
     function __call($name, $args)
     {
         $item =& $this->_items[$this->_i];
+        if (!is_object($item)) {
+            common_log(LOG_ERR, "Invalid entry " . var_export($item, true) . " at index $this->_i of $this->N; calling $name()");
+            throw new ServerException("Internal error: bad entry in array wrapper list.");
+        }
         return call_user_func_array(array($item, $name), $args);
     }
 }
index 94540e3a8520d8c8c7eb6db42928aaa0ba3eefa3..08779fb6fd82cd0141f8c5f10d93fe14078ef42a 100644 (file)
@@ -22,13 +22,13 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
 //exit with 200 response, if this is checking fancy from the installer
 if (isset($_REQUEST['p']) && $_REQUEST['p'] == 'check-fancy') {  exit; }
 
-define('STATUSNET_BASE_VERSION', '0.9.8');
-define('STATUSNET_LIFECYCLE', 'dev'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
+define('STATUSNET_BASE_VERSION', '0.9.7');
+define('STATUSNET_LIFECYCLE', 'alpha1'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
 define('STATUSNET_VERSION', STATUSNET_BASE_VERSION . STATUSNET_LIFECYCLE);
 
 define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility
 
-define('STATUSNET_CODENAME', 'Letter Never Sent');
+define('STATUSNET_CODENAME', 'World Leader Pretend');
 
 define('AVATAR_PROFILE_SIZE', 96);
 define('AVATAR_STREAM_SIZE', 48);
@@ -100,7 +100,11 @@ function _have_config()
     return StatusNet::haveConfig();
 }
 
-function __autoload($cls)
+/**
+ * Wrapper for class autoloaders.
+ * This used to be the special function name __autoload(), but that causes bugs with PHPUnit 3.5+
+ */
+function autoload_sn($cls)
 {
     if (file_exists(INSTALLDIR.'/classes/' . $cls . '.php')) {
         require_once(INSTALLDIR.'/classes/' . $cls . '.php');
@@ -116,6 +120,8 @@ function __autoload($cls)
     }
 }
 
+spl_autoload_register('autoload_sn');
+
 // XXX: how many of these could be auto-loaded on use?
 // XXX: note that these files should not use config options
 // at compile time since DB config options are not yet loaded.
index 325276c5fcd2a1c9643bd9c0df95821f4aa32289..20f18ef0d97485ddf91dd9611d00aa5d55267c4a 100644 (file)
@@ -96,7 +96,7 @@ class ConnectSettingsNav extends Widget
         $action_name = $this->action->trimmed('action');
         $this->action->elementStart('ul', array('class' => 'nav'));
 
-        if (Event::handle('StartConnectSettingsNav', array(&$this->action))) {
+        if (Event::handle('StartConnectSettingsNav', array($this->action))) {
 
             # action => array('prompt', 'title')
             $menu = array();
@@ -129,7 +129,7 @@ class ConnectSettingsNav extends Widget
                         $action_name === $menuaction);
             }
 
-            Event::handle('EndConnectSettingsNav', array(&$this->action));
+            Event::handle('EndConnectSettingsNav', array($this->action));
         }
 
         $this->action->elementEnd('ul');
diff --git a/lib/discovery.php b/lib/discovery.php
new file mode 100644 (file)
index 0000000..d67ec94
--- /dev/null
@@ -0,0 +1,469 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Use Hammer discovery stack to find out interesting things about an URI
+ *
+ * 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  Discovery
+ * @package   StatusNet
+ * @author    James Walker <james@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')) {
+    exit(1);
+}
+
+/**
+ * This class implements LRDD-based service discovery based on the "Hammer Draft"
+ * (including webfinger)
+ *
+ * @category  Discovery
+ * @package   StatusNet
+ * @author    James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ *
+ * @see       http://groups.google.com/group/webfinger/browse_thread/thread/9f3d93a479e91bbf
+ */
+
+class Discovery
+{
+    const LRDD_REL    = 'lrdd';
+    const PROFILEPAGE = 'http://webfinger.net/rel/profile-page';
+    const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from';
+    const HCARD       = 'http://microformats.org/profile/hcard';
+
+    public $methods = array();
+
+    /**
+     * Constructor for a discovery object
+     *
+     * Registers different discovery methods.
+     *
+     * @return Discovery this
+     */
+
+    public function __construct()
+    {
+        $this->registerMethod('Discovery_LRDD_Host_Meta');
+        $this->registerMethod('Discovery_LRDD_Link_Header');
+        $this->registerMethod('Discovery_LRDD_Link_HTML');
+    }
+
+    /**
+     * Register a discovery class
+     * 
+     * @param string $class Class name
+     *
+     * @return void
+     */
+
+    public function registerMethod($class)
+    {
+        $this->methods[] = $class;
+    }
+
+    /**
+     * Given a "user id" make sure it's normalized to either a webfinger
+     * acct: uri or a profile HTTP URL.
+     *
+     * @param string $user_id User ID to normalize
+     *
+     * @return string normalized acct: or http(s)?: URI
+     */
+
+    public static function normalize($user_id)
+    {
+        if (substr($user_id, 0, 5) == 'http:' ||
+            substr($user_id, 0, 6) == 'https:' ||
+            substr($user_id, 0, 5) == 'acct:') {
+            return $user_id;
+        }
+
+        if (strpos($user_id, '@') !== false) {
+            return 'acct:' . $user_id;
+        }
+
+        return 'http://' . $user_id;
+    }
+
+    /**
+     * Determine if a string is a Webfinger ID
+     *
+     * Webfinger IDs look like foo@example.com or acct:foo@example.com
+     *
+     * @param string $user_id ID to check
+     *
+     * @return boolean true if $user_id is a Webfinger, else false
+     */
+     
+    public static function isWebfinger($user_id)
+    {
+        $uri = Discovery::normalize($user_id);
+
+        return (substr($uri, 0, 5) == 'acct:');
+    }
+
+    /**
+     * Given a user ID, return the first available XRD
+     *
+     * @param string $id User ID URI
+     *
+     * @return XRD XRD object for the user
+     */
+
+    public function lookup($id)
+    {
+        // Normalize the incoming $id to make sure we have a uri
+        $uri = $this->normalize($id);
+
+        foreach ($this->methods as $class) {
+            $links = call_user_func(array($class, 'discover'), $uri);
+            if ($link = Discovery::getService($links, Discovery::LRDD_REL)) {
+                // Load the LRDD XRD
+                if (!empty($link['template'])) {
+                    $xrd_uri = Discovery::applyTemplate($link['template'], $uri);
+                } else {
+                    $xrd_uri = $link['href'];
+                }
+
+                $xrd = $this->fetchXrd($xrd_uri);
+                if ($xrd) {
+                    return $xrd;
+                }
+            }
+        }
+
+        // TRANS: Exception.
+        throw new Exception(sprintf(_('Unable to find services for %s.'), $id));
+    }
+
+    /**
+     * Given an array of links, returns the matching service
+     *
+     * @param array  $links   Links to check
+     * @param string $service Service to find
+     *
+     * @return array $link assoc array representing the link
+     */
+
+    public static function getService($links, $service)
+    {
+        if (!is_array($links)) {
+            return false;
+        }
+
+        foreach ($links as $link) {
+            if ($link['rel'] == $service) {
+                return $link;
+            }
+        }
+    }
+
+    /**
+     * Apply a template using an ID
+     *
+     * Replaces {uri} in template string with the ID given.
+     *
+     * @param string $template Template to match
+     * @param string $id       User ID to replace with
+     *
+     * @return string replaced values
+     */
+
+    public static function applyTemplate($template, $id)
+    {
+        $template = str_replace('{uri}', urlencode($id), $template);
+
+        return $template;
+    }
+
+    /**
+     * Fetch an XRD file and parse
+     *
+     * @param string $url URL of the XRD
+     * 
+     * @return XRD object representing the XRD file
+     */
+
+    public static function fetchXrd($url)
+    {
+        try {
+            $client   = new HTTPClient();
+            $response = $client->get($url);
+        } catch (HTTP_Request2_Exception $e) {
+            return false;
+        }
+
+        if ($response->getStatus() != 200) {
+            return false;
+        }
+
+        return XRD::parse($response->getBody());
+    }
+}
+
+/**
+ * Abstract interface for discovery
+ *
+ * Objects that implement this interface can retrieve an array of
+ * XRD links for the URI.
+ *
+ * @category  Discovery
+ * @package   StatusNet
+ * @author    James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+interface Discovery_LRDD
+{
+    /**
+     * Discover interesting info about the URI
+     *
+     * @param string $uri URI to inquire about
+     *
+     * @return array Links in the XRD file
+     */
+
+    public function discover($uri);
+}
+
+/**
+ * Implementation of discovery using host-meta file
+ *
+ * Discovers XRD file for a user by going to the organization's
+ * host-meta file and trying to find a template for LRDD.
+ *
+ * @category  Discovery
+ * @package   StatusNet
+ * @author    James Walker <james@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 Discovery_LRDD_Host_Meta implements Discovery_LRDD
+{
+    /**
+     * Discovery core method
+     *
+     * For Webfinger and HTTP URIs, fetch the host-meta file
+     * and look for LRDD templates
+     *
+     * @param string $uri URI to inquire about
+     *
+     * @return array Links in the XRD file
+     */
+
+    public function discover($uri)
+    {
+        if (Discovery::isWebfinger($uri)) {
+            // We have a webfinger acct: - start with host-meta
+            list($name, $domain) = explode('@', $uri);
+        } else {
+            $domain = parse_url($uri, PHP_URL_HOST);
+        }
+
+        $url = 'http://'. $domain .'/.well-known/host-meta';
+
+        $xrd = Discovery::fetchXrd($url);
+
+        if ($xrd) {
+            if ($xrd->host != $domain) {
+                return false;
+            }
+
+            return $xrd->links;
+        }
+    }
+}
+
+/**
+ * Implementation of discovery using HTTP Link header
+ *
+ * Discovers XRD file for a user by fetching the URL and reading any
+ * Link: headers in the HTTP response.
+ *
+ * @category  Discovery
+ * @package   StatusNet
+ * @author    James Walker <james@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 Discovery_LRDD_Link_Header implements Discovery_LRDD
+{
+    /**
+     * Discovery core method
+     *
+     * For HTTP IDs fetch the URL and look for Link headers.
+     *
+     * @param string $uri URI to inquire about
+     *
+     * @return array Links in the XRD file
+     *
+     * @todo fail out of Webfinger URIs faster 
+     */
+
+    public function discover($uri)
+    {
+        try {
+            $client   = new HTTPClient();
+            $response = $client->get($uri);
+        } catch (HTTP_Request2_Exception $e) {
+            return false;
+        }
+
+        if ($response->getStatus() != 200) {
+            return false;
+        }
+
+        $link_header = $response->getHeader('Link');
+        if (!$link_header) {
+            //            return false;
+        }
+
+        return array(Discovery_LRDD_Link_Header::parseHeader($link_header));
+    }
+
+    /**
+     * Given a string or array of headers, returns XRD-like assoc array
+     *
+     * @param string|array $header string or array of strings for headers
+     * 
+     * @return array Link header in XRD-like format
+     */
+
+    protected static function parseHeader($header)
+    {
+        $lh = new LinkHeader($header);
+
+        return array('href' => $lh->href,
+                     'rel'  => $lh->rel,
+                     'type' => $lh->type);
+    }
+}
+
+/**
+ * Implementation of discovery using HTML <link> element
+ *
+ * Discovers XRD file for a user by fetching the URL and reading any
+ * <link> elements in the HTML response.
+ *
+ * @category  Discovery
+ * @package   StatusNet
+ * @author    James Walker <james@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 Discovery_LRDD_Link_HTML implements Discovery_LRDD
+{
+    /**
+     * Discovery core method
+     *
+     * For HTTP IDs, fetch the URL and look for <link> elements
+     * in the HTML response.
+     *
+     * @param string $uri URI to inquire about
+     *
+     * @return array Links in XRD-ish assoc array
+     *
+     * @todo fail out of Webfinger URIs faster 
+     */
+
+    public function discover($uri)
+    {
+        try {
+            $client   = new HTTPClient();
+            $response = $client->get($uri);
+        } catch (HTTP_Request2_Exception $e) {
+            return false;
+        }
+
+        if ($response->getStatus() != 200) {
+            return false;
+        }
+
+        return Discovery_LRDD_Link_HTML::parse($response->getBody());
+    }
+
+    /**
+     * Parse HTML and return <link> elements
+     *
+     * Given an HTML string, scans the string for <link> elements
+     *
+     * @param string $html HTML to scan
+     *
+     * @return array array of associative arrays in XRD-ish format
+     */
+
+    public function parse($html)
+    {
+        $links = array();
+
+        preg_match('/<head(\s[^>]*)?>(.*?)<\/head>/is', $html, $head_matches);
+        $head_html = $head_matches[2];
+
+        preg_match_all('/<link\s[^>]*>/i', $head_html, $link_matches);
+
+        foreach ($link_matches[0] as $link_html) {
+            $link_url  = null;
+            $link_rel  = null;
+            $link_type = null;
+
+            preg_match('/\srel=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $rel_matches);
+            if ( isset($rel_matches[3]) ) {
+                $link_rel = $rel_matches[3];
+            } else if ( isset($rel_matches[1]) ) {
+                $link_rel = $rel_matches[1];
+            }
+
+            preg_match('/\shref=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $href_matches);
+            if ( isset($href_matches[3]) ) {
+                $link_uri = $href_matches[3];
+            } else if ( isset($href_matches[1]) ) {
+                $link_uri = $href_matches[1];
+            }
+
+            preg_match('/\stype=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $type_matches);
+            if ( isset($type_matches[3]) ) {
+                $link_type = $type_matches[3];
+            } else if ( isset($type_matches[1]) ) {
+                $link_type = $type_matches[1];
+            }
+
+            $links[] = array(
+                'href' => $link_url,
+                'rel' => $link_rel,
+                'type' => $link_type,
+            );
+        }
+
+        return $links;
+    }
+}
index 159deead61130a283e306c72c72a0c0469a38aba..56a7b7d4a20ad21f19dfdaf1edcbedab3c70891d 100644 (file)
@@ -128,7 +128,7 @@ class ImageFile
      */
     function resize($size, $x = 0, $y = 0, $w = null, $h = null)
     {
-        $targetType = $this->preferredType($this->type);
+        $targetType = $this->preferredType();
         $outname = Avatar::filename($this->id,
                                     image_type_to_extension($targetType),
                                     $size,
@@ -138,6 +138,19 @@ class ImageFile
         return $outname;
     }
 
+    /**
+     * Copy the image file to the given destination.
+     * For obscure formats, this will automatically convert to PNG;
+     * otherwise the original file will be copied as-is.
+     *
+     * @param string $outpath
+     * @return string filename
+     */
+    function copyTo($outpath)
+    {
+        return $this->resizeTo($outpath, $this->width, $this->height);
+    }
+
     /**
      * Create and save a thumbnail image.
      *
@@ -154,7 +167,7 @@ class ImageFile
     {
         $w = ($w === null) ? $this->width:$w;
         $h = ($h === null) ? $this->height:$h;
-        $targetType = $this->preferredType($this->type);
+        $targetType = $this->preferredType();
 
         if (!file_exists($this->filepath)) {
             throw new Exception(_('Lost our file.'));
@@ -247,25 +260,25 @@ class ImageFile
     /**
      * Several obscure file types should be normalized to PNG on resize.
      *
-     * @param int $type
+     * @fixme consider flattening anything not GIF or JPEG to PNG
      * @return int
      */
-    function preferredType($type)
+    function preferredType()
     {
-        if($type == IMAGETYPE_BMP) {
+        if($this->type == IMAGETYPE_BMP) {
             //we don't want to save BMP... it's an inefficient, rare, antiquated format
             //save png instead
             return IMAGETYPE_PNG;
-        } else if($type == IMAGETYPE_WBMP) {
+        } else if($this->type == IMAGETYPE_WBMP) {
             //we don't want to save WBMP... it's a rare format that we can't guarantee clients will support
             //save png instead
             return IMAGETYPE_PNG;
-        } else if($type == IMAGETYPE_XBM) {
+        } else if($this->type == IMAGETYPE_XBM) {
             //we don't want to save XBM... it's a rare format that we can't guarantee clients will support
             //save png instead
             return IMAGETYPE_PNG;
         }
-        return $type;
+        return $this->type;
     }
 
     function unlink()
diff --git a/lib/linkheader.php b/lib/linkheader.php
new file mode 100644 (file)
index 0000000..a08fb67
--- /dev/null
@@ -0,0 +1,132 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Parse HTTP response for interesting Link: headers
+ *
+ * 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  Discovery
+ * @package   StatusNet
+ * @author    James Walker <james@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')) {
+    exit(1);
+}
+
+/**
+ * Class to represent Link: headers in an HTTP response
+ *
+ * Since these are a fairly important part of Hammer-stack discovery, they're
+ * reified and implemented here.
+ *
+ * @category  Discovery
+ * @package   StatusNet
+ * @author    James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ *
+ * @see       Discovery
+ */
+
+class LinkHeader
+{
+    var $href;
+    var $rel;
+    var $type;
+
+    /**
+     * Initialize from a string
+     *
+     * @param string $str Link: header value
+     *
+     * @return LinkHeader self
+     */
+
+    function __construct($str)
+    {
+        preg_match('/^<[^>]+>/', $str, $uri_reference);
+        //if (empty($uri_reference)) return;
+
+        $this->href = trim($uri_reference[0], '<>');
+        $this->rel  = array();
+        $this->type = null;
+
+        // remove uri-reference from header
+        $str = substr($str, strlen($uri_reference[0]));
+
+        // parse link-params
+        $params = explode(';', $str);
+
+        foreach ($params as $param) {
+            if (empty($param)) { 
+                continue;
+            }
+            list($param_name, $param_value) = explode('=', $param, 2);
+
+            $param_name  = trim($param_name);
+            $param_value = preg_replace('(^"|"$)', '', trim($param_value));
+
+            // for now we only care about 'rel' and 'type' link params
+            // TODO do something with the other links-params
+            switch ($param_name) {
+            case 'rel':
+                $this->rel = trim($param_value);
+                break;
+
+            case 'type':
+                $this->type = trim($param_value);
+            }
+        }
+    }
+
+    /**
+     * Given an HTTP response, return the requested Link: header
+     *
+     * @param HTTP_Request2_Response $response response to check
+     * @param string                 $rel      relationship to look for
+     * @param string                 $type     media type to look for
+     *
+     * @return LinkHeader discovered header, or null on failure
+     */
+
+    static function getLink($response, $rel=null, $type=null)
+    {
+        $headers = $response->getHeader('Link');
+        if ($headers) {
+            // Can get an array or string, so try to simplify the path
+            if (!is_array($headers)) {
+                $headers = array($headers);
+            }
+
+            foreach ($headers as $header) {
+                $lh = new LinkHeader($header);
+
+                if ((is_null($rel) || $lh->rel == $rel) &&
+                    (is_null($type) || $lh->type == $type)) {
+                    return $lh->href;
+                }
+            }
+        }
+        return null;
+    }
+}
index b545fbf26914e7fbc44d6e2c83468b6fa9630f0b..0fd0f45c7d623a035a93a7a58a55829a5a826d02 100644 (file)
@@ -73,7 +73,7 @@ class LoginGroupNav extends Widget
 
         $this->action->elementStart('ul', array('class' => 'nav'));
 
-        if (Event::handle('StartLoginGroupNav', array(&$this->action))) {
+        if (Event::handle('StartLoginGroupNav', array($this->action))) {
 
             $this->action->menuItem(common_local_url('login'),
                                     _('Login'),
@@ -87,7 +87,7 @@ class LoginGroupNav extends Widget
                                         $action_name === 'register');
             }
 
-            Event::handle('EndLoginGroupNav', array(&$this->action));
+            Event::handle('EndLoginGroupNav', array($this->action));
         }
 
         $this->action->elementEnd('ul');
index dd6a1a366e80ac75126a2040aa20774219151b54..9b6b7d698872065914fc468dab5d4d3c6a0b95bb 100644 (file)
@@ -121,7 +121,7 @@ function mail_notify_from()
 
         $domain = mail_domain();
 
-        $notifyfrom = '"'.common_config('site', 'name') .'" <noreply@'.$domain.'>';
+        $notifyfrom = '"'. str_replace('"', '\\"', common_config('site', 'name')) .'" <noreply@'.$domain.'>';
     }
 
     return $notifyfrom;
index 4bfc4d48d900b042020cadd0b1247f1535f87b1f..5e4e0f52a0e14bf1b4cc12988d6976d7934d89e0 100644 (file)
@@ -99,6 +99,21 @@ class ProfileAction extends OwnerDesignAction
         $this->showStatistics();
     }
 
+    /**
+     * Convenience function for common pattern of links to subscription/groups sections.
+     *
+     * @param string $actionClass
+     * @param string $title
+     * @param string $cssClass
+     */
+    private function statsSectionLink($actionClass, $title, $cssClass='')
+    {
+        $this->element('a', array('href' => common_local_url($actionClass,
+                                                             array('nickname' => $this->profile->nickname)),
+                                  'class' => $cssClass),
+                       $title);
+    }
+
     function showSubscriptions()
     {
         $profile = $this->profile->getSubscriptions(0, PROFILES_PER_MINILIST + 1);
@@ -106,7 +121,9 @@ class ProfileAction extends OwnerDesignAction
         $this->elementStart('div', array('id' => 'entity_subscriptions',
                                          'class' => 'section'));
         if (Event::handle('StartShowSubscriptionsMiniList', array($this))) {
-            $this->element('h2', null, _('Subscriptions'));
+            $this->elementStart('h2');
+            $this->statsSectionLink('subscriptions', _('Subscriptions'));
+            $this->elementEnd('h2');
 
             $cnt = 0;
 
@@ -120,10 +137,7 @@ class ProfileAction extends OwnerDesignAction
 
             if ($cnt > PROFILES_PER_MINILIST) {
                 $this->elementStart('p');
-                $this->element('a', array('href' => common_local_url('subscriptions',
-                                                                     array('nickname' => $this->profile->nickname)),
-                                          'class' => 'more'),
-                               _('All subscriptions'));
+                $this->statsSectionLink('subscriptions', _('All subscriptions'), 'more');
                 $this->elementEnd('p');
             }
 
@@ -141,7 +155,9 @@ class ProfileAction extends OwnerDesignAction
 
         if (Event::handle('StartShowSubscribersMiniList', array($this))) {
 
-            $this->element('h2', null, _('Subscribers'));
+            $this->elementStart('h2');
+            $this->statsSectionLink('subscribers', _('Subscribers'));
+            $this->elementEnd('h2');
 
             $cnt = 0;
 
@@ -155,10 +171,7 @@ class ProfileAction extends OwnerDesignAction
 
             if ($cnt > PROFILES_PER_MINILIST) {
                 $this->elementStart('p');
-                $this->element('a', array('href' => common_local_url('subscribers',
-                                                                     array('nickname' => $this->profile->nickname)),
-                                          'class' => 'more'),
-                               _('All subscribers'));
+                $this->statsSectionLink('subscribers', _('All subscribers'), 'more');
                 $this->elementEnd('p');
             }
 
@@ -170,10 +183,7 @@ class ProfileAction extends OwnerDesignAction
 
     function showStatistics()
     {
-        $subs_count   = $this->profile->subscriptionCount();
-        $subbed_count = $this->profile->subscriberCount();
         $notice_count = $this->profile->noticeCount();
-        $group_count  = $this->profile->getGroups()->N;
         $age_days     = (time() - strtotime($this->profile->created)) / 86400;
         if ($age_days < 1) {
             // Rather than extrapolating out to a bajillion...
@@ -186,57 +196,71 @@ class ProfileAction extends OwnerDesignAction
 
         $this->element('h2', null, _('Statistics'));
 
-        // Other stats...?
-        $this->elementStart('dl', 'entity_user-id');
-        $this->element('dt', null, _('User ID'));
-        $this->element('dd', null, $this->profile->id);
-        $this->elementEnd('dl');
-
-        $this->elementStart('dl', 'entity_member-since');
-        $this->element('dt', null, _('Member since'));
-        $this->element('dd', null, date('j M Y',
-                                        strtotime($this->profile->created)));
-        $this->elementEnd('dl');
-
-        $this->elementStart('dl', 'entity_subscriptions');
-        $this->elementStart('dt');
-        $this->element('a', array('href' => common_local_url('subscriptions',
-                                                             array('nickname' => $this->profile->nickname))),
-                       _('Subscriptions'));
-        $this->elementEnd('dt');
-        $this->element('dd', null, $subs_count);
-        $this->elementEnd('dl');
-
-        $this->elementStart('dl', 'entity_subscribers');
-        $this->elementStart('dt');
-        $this->element('a', array('href' => common_local_url('subscribers',
-                                                             array('nickname' => $this->profile->nickname))),
-                       _('Subscribers'));
-        $this->elementEnd('dt');
-        $this->element('dd', 'subscribers', $subbed_count);
-        $this->elementEnd('dl');
+        $profile = $this->profile;
+        $actionParams = array('nickname' => $profile->nickname);
+        $stats = array(
+            array(
+                'id' => 'user-id',
+                'label' => _('User ID'),
+                'value' => $profile->id,
+            ),
+            array(
+                'id' => 'member-since',
+                'label' => _('Member since'),
+                'value' => date('j M Y', strtotime($profile->created))
+            ),
+            array(
+                'id' => 'subscriptions',
+                'label' => _('Subscriptions'),
+                'link' => common_local_url('subscriptions', $actionParams),
+                'value' => $profile->subscriptionCount(),
+            ),
+            array(
+                'id' => 'subscribers',
+                'label' => _('Subscribers'),
+                'link' => common_local_url('subscribers', $actionParams),
+                'value' => $profile->subscriberCount(),
+            ),
+            array(
+                'id' => 'groups',
+                'label' => _('Groups'),
+                'link' => common_local_url('usergroups', $actionParams),
+                'value' => $profile->getGroups()->N,
+            ),
+            array(
+                'id' => 'notices',
+                'label' => _('Notices'),
+                'value' => $notice_count,
+            ),
+            array(
+                'id' => 'daily_notices',
+                // TRANS: Average count of posts made per day since account registration
+                'label' => _('Daily average'),
+                'value' => $daily_count
+            )
+        );
+
+        // Give plugins a chance to add stats entries
+        Event::handle('ProfileStats', array($profile, &$stats));
+
+        foreach ($stats as $row) {
+            $this->showStatsRow($row);
+        }
+        $this->elementEnd('div');
+    }
 
-        $this->elementStart('dl', 'entity_groups');
+    private function showStatsRow($row)
+    {
+        $this->elementStart('dl', 'entity_' . $row['id']);
         $this->elementStart('dt');
-        $this->element('a', array('href' => common_local_url('usergroups',
-                                                             array('nickname' => $this->profile->nickname))),
-                       _('Groups'));
+        if (!empty($row['link'])) {
+            $this->element('a', array('href' => $row['link']), $row['label']);
+        } else {
+            $this->text($row['label']);
+        }
         $this->elementEnd('dt');
-        $this->element('dd', 'groups', $group_count);
+        $this->element('dd', null, $row['value']);
         $this->elementEnd('dl');
-
-        $this->elementStart('dl', 'entity_notices');
-        $this->element('dt', null, _('Notices'));
-        $this->element('dd', null, $notice_count);
-        $this->elementEnd('dl');
-
-        $this->elementStart('dl', 'entity_daily_notices');
-        // TRANS: Average count of posts made per day since account registration
-        $this->element('dt', null, _('Daily average'));
-        $this->element('dd', null, $daily_count);
-        $this->elementEnd('dl');
-
-        $this->elementEnd('div');
     }
 
     function showGroups()
@@ -246,7 +270,9 @@ class ProfileAction extends OwnerDesignAction
         $this->elementStart('div', array('id' => 'entity_groups',
                                          'class' => 'section'));
         if (Event::handle('StartShowGroupsMiniList', array($this))) {
-            $this->element('h2', null, _('Groups'));
+            $this->elementStart('h2');
+            $this->statsSectionLink('usergroups', _('Groups'));
+            $this->elementEnd('h2');
 
             if ($groups) {
                 $gml = new GroupMiniList($groups, $this->profile, $this);
@@ -258,10 +284,7 @@ class ProfileAction extends OwnerDesignAction
 
             if ($cnt > GROUPS_PER_MINILIST) {
                 $this->elementStart('p');
-                $this->element('a', array('href' => common_local_url('usergroups',
-                                                                     array('nickname' => $this->profile->nickname)),
-                                          'class' => 'more'),
-                               _('All groups'));
+                $this->statsSectionLink('usergroups', _('All groups'), 'more');
                 $this->elementEnd('p');
             }
 
index 65a972e234681b6e8980565498af9ebe4bbf7402..5b59444bc2db80616203a3b24e0f9224416ff9c9 100644 (file)
@@ -268,6 +268,8 @@ abstract class QueueManager extends IoManager
             $this->connect('deluser', 'DelUserQueueHandler');
             $this->connect('feedimp', 'FeedImporter');
             $this->connect('actimp', 'ActivityImporter');
+            $this->connect('acctmove', 'AccountMover');
+            $this->connect('actmove', 'ActivityMover');
 
             // Broadcasting profile updates to OMB remote subscribers
             $this->connect('profile', 'ProfileQueueHandler');
index 0fc315e26e2ff9de63f86483061b50426d3f4058..7d9b02ded8606594815338c59ff16880f0aabd80 100644 (file)
@@ -28,6 +28,8 @@
 
 class UserActivityStream extends AtomUserNoticeFeed
 {
+    public $activities = array();
+
     function __construct($user, $indent = true)
     {
         parent::__construct($user, null, $indent);
@@ -45,10 +47,15 @@ class UserActivityStream extends AtomUserNoticeFeed
         usort($objs, 'UserActivityStream::compareObject');
 
         foreach ($objs as $obj) {
-            $act = $obj->asActivity();
+            $this->activities[] = $obj->asActivity();
+        }
+    }
+    
+    function renderEntries()
+    {
+        foreach ($this->activities as $act) {
             // Only show the author sub-element if it's different from default user
-            $str = $act->asString(false, ($act->actor->id != $this->user->uri));
-            $this->addEntryRaw($str);
+            $act->outputTo($this, false, ($act->actor->id != $this->user->uri));
         }
     }
 
index 2813f735eabad3efc1b2cebabfcc44cda81921b0..91c5fb413a94a999f42960017ce459153e33bb56 100644 (file)
@@ -242,7 +242,7 @@ class UserProfile extends Widget
             $this->out->elementEnd('div');
             return;
         }
-        if (Event::handle('StartProfilePageActionsSection', array(&$this->out, $this->profile))) {
+        if (Event::handle('StartProfilePageActionsSection', array($this->out, $this->profile))) {
 
             $cur = common_current_user();
 
@@ -250,13 +250,13 @@ class UserProfile extends Widget
             $this->out->element('h2', null, _('User actions'));
             $this->out->elementStart('ul');
 
-            if (Event::handle('StartProfilePageActionsElements', array(&$this->out, $this->profile))) {
+            if (Event::handle('StartProfilePageActionsElements', array($this->out, $this->profile))) {
                 if (empty($cur)) { // not logged in
-                    if (Event::handle('StartProfileRemoteSubscribe', array(&$this->out, $this->profile))) {
+                    if (Event::handle('StartProfileRemoteSubscribe', array($this->out, $this->profile))) {
                         $this->out->elementStart('li', 'entity_subscribe');
                         $this->showRemoteSubscribeLink();
                         $this->out->elementEnd('li');
-                        Event::handle('EndProfileRemoteSubscribe', array(&$this->out, $this->profile));
+                        Event::handle('EndProfileRemoteSubscribe', array($this->out, $this->profile));
                     }
                 } else {
                     if ($cur->id == $this->profile->id) { // your own page
@@ -376,13 +376,13 @@ class UserProfile extends Widget
                     }
                 }
 
-                Event::handle('EndProfilePageActionsElements', array(&$this->out, $this->profile));
+                Event::handle('EndProfilePageActionsElements', array($this->out, $this->profile));
             }
 
             $this->out->elementEnd('ul');
             $this->out->elementEnd('div');
 
-            Event::handle('EndProfilePageActionsSection', array(&$this->out, $this->profile));
+            Event::handle('EndProfilePageActionsSection', array($this->out, $this->profile));
         }
     }
 
index 9c6d9f3ab70044a4d8573ff713c38778fca51f88..40372b9d7a454c313fb7f7f3ea98cda4362cc412 100644 (file)
@@ -173,6 +173,13 @@ class XRD
                 switch($node->tagName) {
                 case 'Title':
                     $link['title'][] = $node->nodeValue;
+                    break;
+                case 'Property':
+                    $link['property'][] = array('type' => $node->getAttribute('type'),
+                                                'value' => $node->nodeValue);
+                    break;
+                default:
+                    common_log(LOG_NOTICE, "Unexpected tag name {$node->tagName} found in XRD file.");
                 }
             }
         }
index 855ed1ea892e4f847fdb6003a6d9f7296e502280..b59e0f78a40f3c2dab4a8eb43053f2d2b579f5c1 100644 (file)
@@ -99,7 +99,9 @@ class XrdAction extends Action
            
             $xrd->links[] = array('rel' => 'http://apinamespace.org/atom',
                                   'type' => 'application/atomsvc+xml',
-                                  'href' => common_local_url('ApiAtomService', array('id' => $nick)));
+                                  'href' => common_local_url('ApiAtomService', array('id' => $nick)),
+                                  'property' => array(array('type' => 'http://apinamespace.org/atom/username',
+                                                            'value' => $nick)));
 
             if (common_config('site', 'fancy')) {
                 $apiRoot = common_path('api/', true);
index 4ee287fb65b88f4bdf3a777f25dd1c95fb3c0ef2..777b50f7243d74f7244f84640cf163d902045b57 100644 (file)
@@ -214,7 +214,11 @@ class Bookmark extends Memcached_DataObject
         }
 
         if (is_string($rawtags)) {
-            $rawtags = preg_split('/[\s,]+/', $rawtags);
+            if (empty($rawtags)) {
+                $rawtags = array();
+            } else {
+                $rawtags = preg_split('/[\s,]+/', $rawtags);
+            }
         }
 
         $nb = new Bookmark();
@@ -274,10 +278,15 @@ class Bookmark extends Memcached_DataObject
 
         // Use user's preferences for short URLs, if possible
 
-        $user = User::staticGet('id', $profile->id);
+        try {
+            $user = User::staticGet('id', $profile->id);
 
-        $shortUrl = File_redirection::makeShort($url, 
-                                                empty($user) ? null : $user);
+            $shortUrl = File_redirection::makeShort($url, 
+                                                    empty($user) ? null : $user);
+        } catch (Exception $e) {
+            // Don't let this stop us.
+            $shortUrl = $url;
+        }
 
         $content = sprintf(_('"%s" %s %s %s'),
                            $title,
index 71e54d41f9127e8a6a5f67d41c0e0e73913213d0..3196bd94a7a622f6d0e186b698e0a8ea0d297040 100644 (file)
@@ -74,7 +74,7 @@ class CasAuthenticationPlugin extends AuthenticationPlugin
         return true;
     }
 
-    function onEndLoginGroupNav(&$action)
+    function onEndLoginGroupNav($action)
     {
         $action_name = $action->trimmed('action');
 
index 798009817c9805e8e7f1ca510d91f3e4eaf5b0ae..e877d300b6ea3610dfc6fe402dea9c7b516237c5 100644 (file)
@@ -446,11 +446,11 @@ class FacebookPlugin extends Plugin
     /*
      * Add a login tab for Facebook Connect
      *
-     * @param Action &action the current action
+     * @param Action $action the current action
      *
      * @return void
      */
-    function onEndLoginGroupNav(&$action)
+    function onEndLoginGroupNav($action)
     {
         if (self::hasKeys()) {
             $action_name = $action->trimmed('action');
@@ -470,11 +470,11 @@ class FacebookPlugin extends Plugin
     /*
      * Add a tab for managing Facebook Connect settings
      *
-     * @param Action &action the current action
+     * @param Action $action the current action
      *
      * @return void
      */
-    function onEndConnectSettingsNav(&$action)
+    function onEndConnectSettingsNav($action)
     {
         if (self::hasKeys()) {
             $action_name = $action->trimmed('action');
index 8b5d05e98359251aadf3e317a28779d3a9603628..000b214ff4e91b96d60c9df1a23245b963c737b1 100644 (file)
@@ -186,11 +186,11 @@ class FacebookBridgePlugin extends Plugin
      * Add a login tab for Facebook, but only if there's a Facebook
      * application defined for the plugin to use.
      *
-     * @param Action &action the current action
+     * @param Action $action the current action
      *
      * @return void
      */
-    function onEndLoginGroupNav(&$action)
+    function onEndLoginGroupNav($action)
     {
         $action_name = $action->trimmed('action');
 
@@ -252,11 +252,11 @@ class FacebookBridgePlugin extends Plugin
      * Add a tab for user-level Facebook settings if the user
      * has a link to Facebook
      *
-     * @param Action &action the current action
+     * @param Action $action the current action
      *
      * @return void
      */
-    function onEndConnectSettingsNav(&$action)
+    function onEndConnectSettingsNav($action)
     {
         if ($this->hasApplication()) {
             $action_name = $action->trimmed('action');
index 022c1e98b1a303b58a234a3160ce642c2f95f30e..7b2f27474173c02153fb28b437de1a5e3b1da80e 100644 (file)
@@ -257,12 +257,20 @@ class FacebookfinishloginAction extends Action
         $this->element('p', null,
                        _m('Create a new user with this nickname.'));
         $this->elementStart('ul', 'form_data');
+
+        // Hook point for captcha etc
+        Event::handle('StartRegistrationFormData', array($this));
+
         $this->elementStart('li');
         // TRANS: Field label.
         $this->input('newname', _m('New nickname'),
                      ($this->username) ? $this->username : '',
                      _m('1-64 lowercase letters or numbers, no punctuation or spaces'));
         $this->elementEnd('li');
+
+        // Hook point for captcha etc
+        Event::handle('EndRegistrationFormData', array($this));
+
         $this->elementEnd('ul');
         // TRANS: Submit button.
         $this->submit('create', _m('BUTTON','Create'));
index 0c8edce2b09a9035f720925276621e0677fc2f4a..961c3209166be6fdd3f66484f3aef3c479801b85 100644 (file)
@@ -16,6 +16,12 @@ jQuery(document).ready(function($){
     contentSelector : "#notices_primary ol.notices",
     itemSelector    : "#notices_primary ol.notices li"
     },function(){
-        SN.Init.Notices();
+        // Reply button and attachment magic need to be set up
+        // for each new notice.
+        // DO NOT run SN.Init.Notices() which will duplicate stuff.
+        $(this).find('.notice').each(function() {
+            SN.U.NoticeReplyTo($(this));
+            SN.U.NoticeWithAttachment($(this));
+        });
     });
 });
index ec31bb0863af40b06a27f6ab18ef408f7dceab8f..dd61f8b18af9a49f43b0edf2f1edfac6849fd9a2 100644 (file)
@@ -21,7 +21,7 @@
     // grab each selector option and see if any fail.
     function areSelectorsValid(opts){
       for (var key in opts){
-        if (key.indexOf && key.indexOf('Selector') && $(opts[key]).length === 0){
+        if (key.indexOf && (key.indexOf('Selector') != -1) && $(opts[key]).length === 0){
             debug('Your ' + key + ' found no elements.');    
             return false;
         } 
index cdcea5cd928559c91f49cf5488a66ff2fd4fec46..7aad0672bf995ad441c4a925a60440fc91162c48 100644 (file)
@@ -53,31 +53,6 @@ class NewMenuPlugin extends Plugin
 {
     public $loadCSS = false;
 
-    /**
-     * Load related modules when needed
-     *
-     * @param string $cls Name of the class to be loaded
-     *
-     * @return boolean hook value; true means continue processing, false means stop.
-     */
-
-    function onAutoload($cls)
-    {
-        $dir = dirname(__FILE__);
-
-        switch ($cls)
-        {
-        case 'HelloAction':
-            include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php';
-            return false;
-        case 'User_greeting_count':
-            include_once $dir . '/'.$cls.'.php';
-            return false;
-        default:
-            return true;
-        }
-    }
-
     /**
      * Modify the default menu
      *
@@ -279,19 +254,19 @@ class NewMenuPlugin extends Plugin
         return true;
     }
 
-    function onStartAccountSettingsNav(&$action)
+    function onStartAccountSettingsNav($action)
     {
         $this->_settingsMenu($action);
         return false;
     }
 
-    function onStartConnectSettingsNav(&$action)
+    function onStartConnectSettingsNav($action)
     {
         $this->_settingsMenu($action);
         return false;
     }
 
-    private function _settingsMenu(&$action)
+    private function _settingsMenu($action)
     {
         $actionName = $action->trimmed('action');
 
@@ -325,7 +300,7 @@ class NewMenuPlugin extends Plugin
                           _('Other options'),
                           $actionName == 'othersettings');
 
-        Event::handle('EndAccountSettingsNav', array(&$action));
+        Event::handle('EndAccountSettingsNav', array($action));
         
         if (common_config('xmpp', 'enabled')) {
             $action->menuItem(common_local_url('imsettings'),
@@ -346,7 +321,7 @@ class NewMenuPlugin extends Plugin
                           _('Authorized connected applications'),
                           $actionName == 'oauthconnectionsettings');
 
-        Event::handle('EndConnectSettingsNav', array(&$action));
+        Event::handle('EndConnectSettingsNav', array($action));
     }
 
     function onEndShowStyles($action)
index 86bd15e0b73491f67619e062e124e1c2d103fa08..59c18746f0d6892398d34d34d1798892307db64f 100644 (file)
@@ -992,17 +992,29 @@ class OStatusPlugin extends Plugin
         return false;
     }
 
-    function onStartGetProfileFromURI($uri, &$profile) {
+    function onStartGetProfileFromURI($uri, &$profile)
+    {
+        // Don't want to do Web-based discovery on our own server,
+        // so we check locally first.
 
-        // XXX: do discovery here instead (OStatus_profile::ensureProfileURI($uri))
+        $user = User::staticGet('uri', $uri);
+        
+        if (!empty($user)) {
+            $profile = $user->getProfile();
+            return false;
+        }
 
-        $oprofile = Ostatus_profile::staticGet('uri', $uri);
+        // Now, check remotely
 
-        if (!empty($oprofile) && !$oprofile->isGroup()) {
+        $oprofile = Ostatus_profile::ensureProfileURI($uri);
+
+        if (!empty($oprofile)) {
             $profile = $oprofile->localProfile();
             return false;
         }
 
+        // Still not a hit, so give up.
+
         return true;
     }
 
index e057deb144d981611face4b6eb00c8c6d76586a6..31d061e6a0148ab90522db25ce3c5f7857440e73 100644 (file)
@@ -39,11 +39,41 @@ class Magicsig extends Memcached_DataObject
 
     public $__table = 'magicsig';
 
+    /**
+     * Key to user.id/profile.id for the local user whose key we're storing.
+     *
+     * @var int
+     */
     public $user_id;
+
+    /**
+     * Flattened string representation of the key pair; callers should
+     * usually use $this->publicKey and $this->privateKey directly,
+     * which hold live Crypt_RSA key objects.
+     *
+     * @var string
+     */
     public $keypair;
+
+    /**
+     * Crypto algorithm used for this key; currently only RSA-SHA256 is supported.
+     *
+     * @var string
+     */
     public $alg;
 
+    /**
+     * Public RSA key; gets serialized in/out via $this->keypair string.
+     *
+     * @var Crypt_RSA
+     */
     public $publicKey;
+
+    /**
+     * PrivateRSA key; gets serialized in/out via $this->keypair string.
+     *
+     * @var Crypt_RSA
+     */
     public $privateKey;
 
     public function __construct($alg = 'RSA-SHA256')
@@ -51,6 +81,13 @@ class Magicsig extends Memcached_DataObject
         $this->alg = $alg;
     }
 
+    /**
+     * Fetch a Magicsig object from the cache or database on a field match.
+     *
+     * @param string $k
+     * @param mixed $v
+     * @return Magicsig
+     */
     public /*static*/ function staticGet($k, $v=null)
     {
         $obj =  parent::staticGet(__CLASS__, $k, $v);
@@ -103,6 +140,14 @@ class Magicsig extends Memcached_DataObject
         return array(false, false, false);
     }
 
+    /**
+     * Save this keypair into the database.
+     *
+     * Overloads default insert behavior to encode the live key objects
+     * as a flat string for storage.
+     *
+     * @return mixed
+     */
     function insert()
     {
         $this->keypair = $this->toString();
@@ -110,6 +155,14 @@ class Magicsig extends Memcached_DataObject
         return parent::insert();
     }
 
+    /**
+     * Generate a new keypair for a local user and store in the database.
+     *
+     * Warning: this can be very slow on systems without the GMP module.
+     * Runtimes of 20-30 seconds are not unheard-of.
+     *
+     * @param int $user_id id of local user we're creating a key for
+     */
     public function generate($user_id)
     {
         $rsa = new Crypt_RSA();
@@ -128,6 +181,12 @@ class Magicsig extends Memcached_DataObject
         $this->insert();
     }
 
+    /**
+     * Encode the keypair or public key as a string.
+     *
+     * @param boolean $full_pair set to false to leave out the private key.
+     * @return string
+     */
     public function toString($full_pair = true)
     {
         $mod = Magicsig::base64_url_encode($this->publicKey->modulus->toBytes());
@@ -140,6 +199,13 @@ class Magicsig extends Memcached_DataObject
         return 'RSA.' . $mod . '.' . $exp . $private_exp;
     }
 
+    /**
+     * Decode a string representation of an RSA public key or keypair
+     * as a Magicsig object which can be used to sign or verify.
+     *
+     * @param string $text
+     * @return Magicsig
+     */
     public static function fromString($text)
     {
         $magic_sig = new Magicsig();
@@ -168,6 +234,14 @@ class Magicsig extends Memcached_DataObject
         return $magic_sig;
     }
 
+    /**
+     * Fill out $this->privateKey or $this->publicKey with a Crypt_RSA object
+     * representing the give key (as mod/exponent pair).
+     *
+     * @param string $mod base64-encoded
+     * @param string $exp base64-encoded exponent
+     * @param string $type one of 'public' or 'private'
+     */
     public function loadKey($mod, $exp, $type = 'public')
     {
         common_log(LOG_DEBUG, "Adding ".$type." key: (".$mod .', '. $exp .")");
@@ -186,11 +260,22 @@ class Magicsig extends Memcached_DataObject
         }
     }
 
+    /**
+     * Returns the name of the crypto algorithm used for this key.
+     *
+     * @return string
+     */
     public function getName()
     {
         return $this->alg;
     }
 
+    /**
+     * Returns the name of a hash function to use for signing with this key.
+     *
+     * @return string
+     * @fixme is this used? doesn't seem to be called by name.
+     */
     public function getHash()
     {
         switch ($this->alg) {
@@ -200,24 +285,48 @@ class Magicsig extends Memcached_DataObject
         }
     }
 
+    /**
+     * Generate base64-encoded signature for the given byte string
+     * using our private key.
+     *
+     * @param string $bytes as raw byte string
+     * @return string base64-encoded signature
+     */
     public function sign($bytes)
     {
         $sig = $this->privateKey->sign($bytes);
         return Magicsig::base64_url_encode($sig);
     }
 
+    /**
+     *
+     * @param string $signed_bytes as raw byte string
+     * @param string $signature as base64
+     * @return boolean
+     */
     public function verify($signed_bytes, $signature)
     {
         $signature = Magicsig::base64_url_decode($signature);
         return $this->publicKey->verify($signed_bytes, $signature);
     }
 
-
+    /**
+     * URL-encoding-friendly base64 variant encoding.
+     *
+     * @param string $input
+     * @return string
+     */
     public static function base64_url_encode($input)
     {
         return strtr(base64_encode($input), '+/', '-_');
     }
 
+    /**
+     * URL-encoding-friendly base64 variant decoding.
+     *
+     * @param string $input
+     * @return string
+     */
     public static function base64_url_decode($input)
     {
         return base64_decode(strtr($input, '-_', '+/'));
index 9c0f014fc6464cea41cb2c2bbc09c4b1fea73531..303e177a5702ae02d7e195bc1f7ed0a877c67a5f 100644 (file)
@@ -331,6 +331,7 @@ class Ostatus_profile extends Memcached_DataObject
      * an acceptable response from the remote site.
      *
      * @param mixed $entry XML string, Notice, or Activity
+     * @param Profile $actor
      * @return boolean success
      */
     public function notifyActivity($entry, $actor)
@@ -1778,8 +1779,10 @@ class Ostatus_profile extends Memcached_DataObject
                 case 'mailto':
                     $rest = $match[2];
                     $oprofile = Ostatus_profile::ensureWebfinger($rest);
+                    break;
                 default:
-                    common_log("Unrecognized URI protocol for profile: $protocol ($uri)");
+                    common_log(LOG_WARNING,
+                               "Unrecognized URI protocol for profile: $protocol ($uri)");
                     break;
                 }
             }
diff --git a/plugins/OStatus/lib/discovery.php b/plugins/OStatus/lib/discovery.php
deleted file mode 100644 (file)
index 905ece2..0000000
+++ /dev/null
@@ -1,274 +0,0 @@
-<?php
-/**
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2010, StatusNet, Inc.
- *
- * A sample module to show best practices for StatusNet plugins
- *
- * 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/>.
- *
- * @package   StatusNet
- * @author    James Walker <james@status.net>
- * @copyright 2010 StatusNet, Inc.
- * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
- * @link      http://status.net/
- */
-
-/**
- * This class implements LRDD-based service discovery based on the "Hammer Draft"
- * (including webfinger)
- *
- * @see http://groups.google.com/group/webfinger/browse_thread/thread/9f3d93a479e91bbf
- */
-class Discovery
-{
-
-    const LRDD_REL = 'lrdd';
-    const PROFILEPAGE = 'http://webfinger.net/rel/profile-page';
-    const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from';
-    const HCARD = 'http://microformats.org/profile/hcard';
-
-    public $methods = array();
-
-    public function __construct()
-    {
-        $this->registerMethod('Discovery_LRDD_Host_Meta');
-        $this->registerMethod('Discovery_LRDD_Link_Header');
-        $this->registerMethod('Discovery_LRDD_Link_HTML');
-    }
-
-    public function registerMethod($class)
-    {
-        $this->methods[] = $class;
-    }
-
-    /**
-     * Given a "user id" make sure it's normalized to either a webfinger
-     * acct: uri or a profile HTTP URL.
-     */
-    public static function normalize($user_id)
-    {
-        if (substr($user_id, 0, 5) == 'http:' ||
-            substr($user_id, 0, 6) == 'https:' ||
-            substr($user_id, 0, 5) == 'acct:') {
-            return $user_id;
-        }
-
-        if (strpos($user_id, '@') !== FALSE) {
-            return 'acct:' . $user_id;
-        }
-
-        return 'http://' . $user_id;
-    }
-
-    public static function isWebfinger($user_id)
-    {
-        $uri = Discovery::normalize($user_id);
-
-        return (substr($uri, 0, 5) == 'acct:');
-    }
-
-    /**
-     * This implements the actual lookup procedure
-     */
-    public function lookup($id)
-    {
-        // Normalize the incoming $id to make sure we have a uri
-        $uri = $this->normalize($id);
-
-        foreach ($this->methods as $class) {
-            $links = call_user_func(array($class, 'discover'), $uri);
-            if ($link = Discovery::getService($links, Discovery::LRDD_REL)) {
-                // Load the LRDD XRD
-                if (!empty($link['template'])) {
-                    $xrd_uri = Discovery::applyTemplate($link['template'], $uri);
-                } else {
-                    $xrd_uri = $link['href'];
-                }
-
-                $xrd = $this->fetchXrd($xrd_uri);
-                if ($xrd) {
-                    return $xrd;
-                }
-            }
-        }
-
-        // TRANS: Exception.
-        throw new Exception(sprintf(_m('Unable to find services for %s.'),$id));
-    }
-
-    public static function getService($links, $service) {
-        if (!is_array($links)) {
-            return false;
-        }
-
-        foreach ($links as $link) {
-            if ($link['rel'] == $service) {
-                return $link;
-            }
-        }
-    }
-
-    public static function applyTemplate($template, $id)
-    {
-        $template = str_replace('{uri}', urlencode($id), $template);
-
-        return $template;
-    }
-
-    public static function fetchXrd($url)
-    {
-        try {
-            $client = new HTTPClient();
-            $response = $client->get($url);
-        } catch (HTTP_Request2_Exception $e) {
-            return false;
-        }
-
-        if ($response->getStatus() != 200) {
-            return false;
-        }
-
-        return XRD::parse($response->getBody());
-    }
-}
-
-interface Discovery_LRDD
-{
-    public function discover($uri);
-}
-
-class Discovery_LRDD_Host_Meta implements Discovery_LRDD
-{
-    public function discover($uri)
-    {
-        if (Discovery::isWebfinger($uri)) {
-            // We have a webfinger acct: - start with host-meta
-            list($name, $domain) = explode('@', $uri);
-        } else {
-            $domain = parse_url($uri, PHP_URL_HOST);
-        }
-
-        $url = 'http://'. $domain .'/.well-known/host-meta';
-
-        $xrd = Discovery::fetchXrd($url);
-
-        if ($xrd) {
-            if ($xrd->host != $domain) {
-                return false;
-            }
-
-            return $xrd->links;
-        }
-    }
-}
-
-class Discovery_LRDD_Link_Header implements Discovery_LRDD
-{
-    public function discover($uri)
-    {
-        try {
-            $client = new HTTPClient();
-            $response = $client->get($uri);
-        } catch (HTTP_Request2_Exception $e) {
-            return false;
-        }
-
-        if ($response->getStatus() != 200) {
-            return false;
-        }
-
-        $link_header = $response->getHeader('Link');
-        if (!$link_header) {
-            //            return false;
-        }
-
-        return array(Discovery_LRDD_Link_Header::parseHeader($link_header));
-    }
-
-    protected static function parseHeader($header)
-    {
-        $lh = new LinkHeader($header);
-
-        return array('href' => $lh->href,
-                     'rel'  => $lh->rel,
-                     'type' => $lh->type);
-    }
-}
-
-class Discovery_LRDD_Link_HTML implements Discovery_LRDD
-{
-    public function discover($uri)
-    {
-        try {
-            $client = new HTTPClient();
-            $response = $client->get($uri);
-        } catch (HTTP_Request2_Exception $e) {
-            return false;
-        }
-
-        if ($response->getStatus() != 200) {
-            return false;
-        }
-
-        return Discovery_LRDD_Link_HTML::parse($response->getBody());
-    }
-
-    public function parse($html)
-    {
-        $links = array();
-
-        preg_match('/<head(\s[^>]*)?>(.*?)<\/head>/is', $html, $head_matches);
-        $head_html = $head_matches[2];
-
-        preg_match_all('/<link\s[^>]*>/i', $head_html, $link_matches);
-
-        foreach ($link_matches[0] as $link_html) {
-            $link_url = null;
-            $link_rel = null;
-            $link_type = null;
-
-            preg_match('/\srel=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $rel_matches);
-            if ( isset($rel_matches[3]) ) {
-                $link_rel = $rel_matches[3];
-            } else if ( isset($rel_matches[1]) ) {
-                $link_rel = $rel_matches[1];
-            }
-
-            preg_match('/\shref=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $href_matches);
-            if ( isset($href_matches[3]) ) {
-                $link_uri = $href_matches[3];
-            } else if ( isset($href_matches[1]) ) {
-                $link_uri = $href_matches[1];
-            }
-
-            preg_match('/\stype=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $type_matches);
-            if ( isset($type_matches[3]) ) {
-                $link_type = $type_matches[3];
-            } else if ( isset($type_matches[1]) ) {
-                $link_type = $type_matches[1];
-            }
-
-            $links[] = array(
-                'href' => $link_url,
-                'rel' => $link_rel,
-                'type' => $link_type,
-            );
-        }
-
-        return $links;
-    }
-}
diff --git a/plugins/OStatus/lib/linkheader.php b/plugins/OStatus/lib/linkheader.php
deleted file mode 100644 (file)
index efa3f65..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-/**
- * @todo Add file header and documentation.
- */
-
-class LinkHeader
-{
-    var $href;
-    var $rel;
-    var $type;
-
-    function __construct($str)
-    {
-        preg_match('/^<[^>]+>/', $str, $uri_reference);
-        //if (empty($uri_reference)) return;
-
-        $this->href = trim($uri_reference[0], '<>');
-        $this->rel = array();
-        $this->type = null;
-
-        // remove uri-reference from header
-        $str = substr($str, strlen($uri_reference[0]));
-
-        // parse link-params
-        $params = explode(';', $str);
-
-        foreach ($params as $param) {
-            if (empty($param)) continue;
-            list($param_name, $param_value) = explode('=', $param, 2);
-            $param_name = trim($param_name);
-            $param_value = preg_replace('(^"|"$)', '', trim($param_value));
-
-            // for now we only care about 'rel' and 'type' link params
-            // TODO do something with the other links-params
-            switch ($param_name) {
-            case 'rel':
-                $this->rel = trim($param_value);
-                break;
-
-            case 'type':
-                $this->type = trim($param_value);
-            }
-        }
-    }
-
-    static function getLink($response, $rel=null, $type=null)
-    {
-        $headers = $response->getHeader('Link');
-        if ($headers) {
-            // Can get an array or string, so try to simplify the path
-            if (!is_array($headers)) {
-                $headers = array($headers);
-            }
-
-            foreach ($headers as $header) {
-                $lh = new LinkHeader($header);
-
-                if ((is_null($rel) || $lh->rel == $rel) &&
-                    (is_null($type) || $lh->type == $type)) {
-                    return $lh->href;
-                }
-            }
-        }
-        return null;
-    }
-}
index 03e6f7c665426b2ff013ea2c34b18dd2f4b36980..384506280d45d2f40e5af85ebb83221398ed1107 100644 (file)
@@ -80,21 +80,53 @@ class MagicEnvelope
         throw new Exception(_m('Unable to locate signer public key.'));
     }
 
+    /**
+     * The current MagicEnvelope spec as used in StatusNet 0.9.7 and later
+     * includes both the original data and some signing metadata fields as
+     * the input plaintext for the signature hash.
+     *
+     * @param array $env
+     * @return string
+     */
+    public function signingText($env) {
+        return implode('.', array($env['data'], // this field is pre-base64'd
+                            Magicsig::base64_url_encode($env['data_type']),
+                            Magicsig::base64_url_encode($env['encoding']),
+                            Magicsig::base64_url_encode($env['alg'])));
+    }
 
+    /**
+     *
+     * @param <type> $text
+     * @param <type> $mimetype
+     * @param <type> $keypair
+     * @return array: associative array of envelope properties
+     * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
+     */
     public function signMessage($text, $mimetype, $keypair)
     {
         $signature_alg = Magicsig::fromString($keypair);
         $armored_text = Magicsig::base64_url_encode($text);
-
-        return array(
+        $env = array(
             'data' => $armored_text,
             'encoding' => MagicEnvelope::ENCODING,
             'data_type' => $mimetype,
-            'sig' => $signature_alg->sign($armored_text),
+            'sig' => '',
             'alg' => $signature_alg->getName()
         );
+
+        $env['sig'] = $signature_alg->sign($this->signingText($env));
+
+        return $env;
     }
 
+    /**
+     * Create an <me:env> XML representation of the envelope.
+     *
+     * @param array $env associative array with envelope data
+     * @return string representation of XML document
+     * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
+     */
     public function toXML($env) {
         $xs = new XMLStringer();
         $xs->startXML();
@@ -110,6 +142,16 @@ class MagicEnvelope
         return $string;
     }
 
+    /**
+     * Extract the contained XML payload, and insert a copy of the envelope
+     * signature data as an <me:provenance> section.
+     *
+     * @param array $env associative array with envelope data
+     * @return string representation of modified XML document
+     *
+     * @fixme in case of XML parsing errors, this will spew to the error log or output
+     * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
+     */
     public function unfold($env)
     {
         $dom = new DOMDocument();
@@ -136,6 +178,14 @@ class MagicEnvelope
         return $dom->saveXML();
     }
 
+    /**
+     * Find the author URI referenced in the given Atom entry.
+     *
+     * @param string $text string containing Atom entry XML
+     * @return mixed URI string or false if XML parsing fails, or null if no author URI can be found
+     *
+     * @fixme XML parsing failures will spew to error logs/output
+     */
     public function getAuthor($text) {
         $doc = new DOMDocument();
         if (!$doc->loadXML($text)) {
@@ -153,11 +203,30 @@ class MagicEnvelope
         }
     }
 
+    /**
+     * Check if the author in the Atom entry fragment claims to match
+     * the given identifier URI.
+     *
+     * @param string $text string containing Atom entry XML
+     * @param string $signer_uri
+     * @return boolean
+     */
     public function checkAuthor($text, $signer_uri)
     {
         return ($this->getAuthor($text) == $signer_uri);
     }
 
+    /**
+     * Attempt to verify cryptographic signing for parsed envelope data.
+     * Requires network access to retrieve public key referenced by the envelope signer.
+     *
+     * Details of failure conditions are dumped to output log and not exposed to caller.
+     *
+     * @param array $env array representation of magic envelope data, as returned from MagicEnvelope::parse()
+     * @return boolean
+     *
+     * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
+     */
     public function verify($env)
     {
         if ($env['alg'] != 'RSA-SHA256') {
@@ -187,15 +256,35 @@ class MagicEnvelope
             return false;
         }
 
-        return $verifier->verify($env['data'], $env['sig']);
+        return $verifier->verify($this->signingText($env), $env['sig']);
     }
 
+    /**
+     * Extract envelope data from an XML document containing an <me:env> or <me:provenance> element.
+     *
+     * @param string XML source
+     * @return mixed associative array of envelope data, or false on unrecognized input
+     *
+     * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
+     * @fixme will spew errors to logs or output in case of XML parse errors
+     * @fixme may give fatal errors if some elements are missing or invalid XML
+     * @fixme calling DOMDocument::loadXML statically triggers warnings in strict mode
+     */
     public function parse($text)
     {
         $dom = DOMDocument::loadXML($text);
         return $this->fromDom($dom);
     }
 
+    /**
+     * Extract envelope data from an XML document containing an <me:env> or <me:provenance> element.
+     *
+     * @param DOMDocument $dom
+     * @return mixed associative array of envelope data, or false on unrecognized input
+     *
+     * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
+     * @fixme may give fatal errors if some elements are missing
+     */
     public function fromDom($dom)
     {
         $env_element = $dom->getElementsByTagNameNS(MagicEnvelope::NS, 'env')->item(0);
@@ -218,3 +307,24 @@ class MagicEnvelope
         );
     }
 }
+
+/**
+ * Variant of MagicEnvelope using the earlier signature form listed in the MagicEnvelope
+ * spec in early 2010; this was used in StatusNet up through 0.9.6, so for backwards compatiblity
+ * we still need to accept and sometimes send this format.
+ */
+class MagicEnvelopeCompat extends MagicEnvelope {
+
+    /**
+     * StatusNet through 0.9.6 used an earlier version of the MagicEnvelope spec
+     * which used only the input data, without the additional fields, as the plaintext
+     * for signing.
+     *
+     * @param array $env
+     * @return string
+     */
+    public function signingText($env) {
+        return $env['data'];
+    }
+}
+
index 963da650840ecf869fe1bc868779f3ad67d1508a..2f5772a84497eac45d0cf86cdb73faea50899301 100644 (file)
@@ -38,10 +38,12 @@ class Salmon
     /**
      * Sign and post the given Atom entry as a Salmon message.
      *
-     * @fixme pass through the actor for signing?
+     * Side effects: may generate a keypair on-demand for the given user,
+     * which can be very slow on some systems.
      *
      * @param string $endpoint_uri
-     * @param string $xml
+     * @param string $xml string representation of payload
+     * @param Profile $actor local user profile whose keys to sign with
      * @return boolean success
      */
     public function post($endpoint_uri, $xml, $actor)
@@ -50,34 +52,65 @@ class Salmon
             return false;
         }
 
-        try {
-            $xml = $this->createMagicEnv($xml, $actor);
-        } catch (Exception $e) {
-            common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage());
-            return false;
-        }
-
-        $headers = array('Content-Type: application/magic-envelope+xml');
+        foreach ($this->formatClasses() as $class) {
+            try {
+                $envelope = $this->createMagicEnv($xml, $actor, $class);
+            } catch (Exception $e) {
+                common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage());
+                return false;
+            }
+    
+            $headers = array('Content-Type: application/magic-envelope+xml');
+    
+            try {
+                $client = new HTTPClient();
+                $client->setBody($envelope);
+                $response = $client->post($endpoint_uri, $headers);
+            } catch (HTTP_Request2_Exception $e) {
+                common_log(LOG_ERR, "Salmon ($class) post to $endpoint_uri failed: " . $e->getMessage());
+                continue;
+            }
+            if ($response->getStatus() != 200) {
+                common_log(LOG_ERR, "Salmon ($class) at $endpoint_uri returned status " .
+                    $response->getStatus() . ': ' . $response->getBody());
+                continue;
+            }
 
-        try {
-            $client = new HTTPClient();
-            $client->setBody($xml);
-            $response = $client->post($endpoint_uri, $headers);
-        } catch (HTTP_Request2_Exception $e) {
-            common_log(LOG_ERR, "Salmon post to $endpoint_uri failed: " . $e->getMessage());
-            return false;
-        }
-        if ($response->getStatus() != 200) {
-            common_log(LOG_ERR, "Salmon at $endpoint_uri returned status " .
-                $response->getStatus() . ': ' . $response->getBody());
-            return false;
+            // Success!
+            return true;
         }
-        return true;
+        return false;
+    }
+
+    /**
+     * List the magic envelope signature class variants in the order we try them.
+     * Multiples are needed for backwards-compat with StatusNet prior to 0.9.7,
+     * which used a draft version of the magic envelope spec.
+     */
+    protected function formatClasses() {
+        return array('MagicEnvelope', 'MagicEnvelopeCompat');
     }
 
-    public function createMagicEnv($text, $actor)
+    /**
+     * Encode the given string as a signed MagicEnvelope XML document,
+     * using the keypair for the given local user profile.
+     *
+     * Side effects: will create and store a keypair on-demand if one
+     * hasn't already been generated for this user. This can be very slow
+     * on some systems.
+     *
+     * @param string $text XML fragment to sign, assumed to be Atom
+     * @param Profile $actor Profile of a local user to use as signer
+     * @param string $class to override the magic envelope signature version, pass a MagicEnvelope subclass here
+     *
+     * @return string XML string representation of magic envelope
+     *
+     * @throws Exception on bad profile input or key generation problems
+     * @fixme if signing fails, this seems to return the original text without warning. Is there a reason for this?
+     */
+    public function createMagicEnv($text, $actor, $class='MagicEnvelope')
     {
-        $magic_env = new MagicEnvelope();
+        $magic_env = new $class();
 
         $user = User::staticGet('id', $actor->id);
         if ($user->id) {
@@ -101,12 +134,32 @@ class Salmon
         return $magic_env->toXML($env);
     }
 
+    /**
+     * Check if the given magic envelope is well-formed and correctly signed.
+     * Needs to have network access to fetch public keys over the web.
+     * Both current and back-compat signature formats will be checked.
+     *
+     * Side effects: exceptions and caching updates may occur during network
+     * fetches.
+     *
+     * @param string $text XML fragment of magic envelope
+     * @return boolean
+     *
+     * @throws Exception on bad profile input or key generation problems
+     * @fixme could hit fatal errors or spew output on invalid XML
+     */
     public function verifyMagicEnv($text)
     {
-        $magic_env = new MagicEnvelope();
+        foreach ($this->formatClasses() as $class) {
+            $magic_env = new $class();
 
-        $env = $magic_env->parse($text);
+            $env = $magic_env->parse($text);
+
+            if ($magic_env->verify($env)) {
+                return true;
+            }
+        }
 
-        return $magic_env->verify($env);
+        return false;
     }
 }
diff --git a/plugins/OStatus/tests/MagicEnvelopeTest.php b/plugins/OStatus/tests/MagicEnvelopeTest.php
new file mode 100644 (file)
index 0000000..5ee0362
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+    print "This script must be run from the command line\n";
+    exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+define('STATUSNET', true);
+
+require_once INSTALLDIR . '/lib/common.php';
+
+class MagicEnvelopeTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Test that MagicEnvelope builds the correct plaintext for signing.
+     * @dataProvider provider
+     */
+    public function testSignatureText($env, $expected)
+    {
+        $magic = new MagicEnvelope;
+        $text = $magic->signingText($env);
+
+        $this->assertEquals($expected, $text, "'$text' should be '$expected'");
+    }
+
+    static public function provider()
+    {
+        return array(
+            array(
+                // Sample case given in spec:
+                // http://salmon-protocol.googlecode.com/svn/trunk/draft-panzer-magicsig-00.html#signing
+                array(
+                    'data' => 'Tm90IHJlYWxseSBBdG9t',
+                    'data_type' => 'application/atom+xml',
+                    'encoding' => 'base64url',
+                    'alg' => 'RSA-SHA256'
+                ),
+                'Tm90IHJlYWxseSBBdG9t.YXBwbGljYXRpb24vYXRvbSt4bWw=.YmFzZTY0dXJs.UlNBLVNIQTI1Ng=='
+            )
+        );
+    }
+
+
+    /**
+     * Test that MagicEnvelope builds the correct plaintext for signing.
+     * @dataProvider provider
+     */
+    public function testSignatureTextCompat($env, $expected)
+    {
+        // Our old code didn't add the extra fields, just used the armored text.
+        $alt = $env['data'];
+
+        $magic = new MagicEnvelopeCompat;
+        $text = $magic->signingText($env);
+
+        $this->assertEquals($alt, $text, "'$text' should be '$alt'");
+    }
+
+}
index 7888ec7c5d5bb7edb676f2f31ca9637e74fa5fdb..1dce588dfe70d3c69888636983dc8bb5d294a131 100644 (file)
@@ -72,6 +72,8 @@ class OStatusTester extends TestBase
         $base = 'test' . mt_rand(1, 1000000);
         $this->pub = new SNTestClient($this->a, 'pub' . $base, 'pw-' . mt_rand(1, 1000000), $timeout);
         $this->sub = new SNTestClient($this->b, 'sub' . $base, 'pw-' . mt_rand(1, 1000000), $timeout);
+
+        $this->group = 'group' . $base;
     }
 
     function run()
@@ -163,6 +165,39 @@ class OStatusTester extends TestBase
         $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
     }
 
+    function testCreateGroup()
+    {
+        $this->groupUrl = $this->pub->createGroup($this->group);
+        $this->assertTrue(!empty($this->groupUrl));
+    }
+
+    function testJoinGroup()
+    {
+        #$this->assertFalse($this->sub->inGroup($this->groupUrl));
+        $this->sub->joinGroup($this->groupUrl);
+        #$this->assertTrue($this->sub->inGroup($this->groupUrl));
+    }
+
+    function testLocalGroupPost()
+    {
+        $post = $this->pub->post("Group post from local to !{$this->group}, should go out over push.");
+        $this->assertNotEqual('', $post);
+        $this->sub->assertReceived($post);
+    }
+
+    function testRemoteGroupPost()
+    {
+        $post = $this->sub->post("Group post from remote to !{$this->group}, should come in over salmon.");
+        $this->assertNotEqual('', $post);
+        $this->pub->assertReceived($post);
+    }
+
+    function testLeaveGroup()
+    {
+        #$this->assertTrue($this->sub->inGroup($this->groupUrl));
+        $this->sub->leaveGroup($this->groupUrl);
+        #$this->assertFalse($this->sub->inGroup($this->groupUrl));
+    }
 }
 
 class SNTestClient extends TestBase
@@ -534,6 +569,63 @@ class SNTestClient extends TestBase
         return false;
     }
 
+    /**
+     * Create a group on this site.
+     *
+     * @param string $nickname
+     * @param array $options
+     * @return string: profile URL for the group
+     */
+    function createGroup($nickname, $options=array()) {
+        $this->log("Creating group as %s on %s: %s",
+                   $this->username,
+                   $this->basepath,
+                   $nickname);
+
+        $data = $this->api('statusnet/groups/create', 'json',
+            array_merge(array('nickname' => $nickname), $options));
+        $url = $data['url'];
+
+        if ($url) {
+            $this->log('  created as %s', $url);
+        } else {
+            $this->log('  failed? %s', var_export($data, true));
+        }
+        return $url;
+    }
+
+    function groupInfo($nickname) {
+        $data = $this->api('statusnet/groups/show', 'json', array(
+            'id' => $nickname
+        ));
+    }
+
+    /**
+     * Join a group.
+     *
+     * @param string $group nickname or URL
+     */
+    function joinGroup($group) {
+        $this->post('join ' . $group);
+    }
+
+    /**
+     * Leave a group.
+     *
+     * @param string $group nickname or URL
+     */
+    function leaveGroup($group) {
+        $this->post('drop ' . $group);
+    }
+
+    /**
+     *
+     * @param string $nickname
+     * @return
+     */
+    function inGroup($nickname) {
+        // @todo
+    }
 }
 
 // @fixme switch to commandline.inc?
diff --git a/plugins/OStatus/tests/slap.php b/plugins/OStatus/tests/slap.php
new file mode 100644 (file)
index 0000000..b5f9d3e
--- /dev/null
@@ -0,0 +1,92 @@
+#!/usr/bin/env php
+<?php
+/*
+ * StatusNet - a distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * 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/>.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+
+$longoptions = array('verify', 'slap=', 'notice=');
+
+$helptext = <<<END_OF_HELP
+slap.php [options]
+
+Test generation and sending of magic envelopes for Salmon slaps.
+
+     --notice=N   generate entry for this notice number
+     --verify     send signed magic envelope to Tuomas Koski's test service
+     --slap=<url> send signed Salmon slap to the destination endpoint
+
+
+END_OF_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+if (!have_option('--notice')) {
+    print "$helptext";
+    exit(1);
+}
+
+$notice_id = get_option_value('--notice');
+
+$notice = Notice::staticGet('id', $notice_id);
+$profile = $notice->getProfile();
+$entry = $notice->asAtomEntry(true);
+
+echo "== Original entry ==\n\n";
+print $entry;
+print "\n\n";
+
+$salmon = new Salmon();
+$envelope = $salmon->createMagicEnv($entry, $profile);
+
+echo "== Signed envelope ==\n\n";
+print $envelope;
+print "\n\n";
+
+echo "== Testing local verification ==\n\n";
+$ok = $salmon->verifyMagicEnv($envelope);
+if ($ok) {
+    print "OK\n\n";
+} else {
+    print "FAIL\n\n";
+}
+
+if (have_option('--verify')) {
+    $url = 'http://www.madebymonsieur.com/ostatus_discovery/magic_env/validate/';
+    echo "== Testing remote verification ==\n\n";
+    print "Sending for verification to $url ...\n";
+
+    $client = new HTTPClient();
+    $response = $client->post($url, array(), array('magic_env' => $envelope));
+
+    print $response->getStatus() . "\n\n";
+    print $response->getBody() . "\n\n";
+}
+
+if (have_option('--slap')) {
+    $url = get_option_value('--slap');
+    echo "== Remote salmon slap ==\n\n";
+    print "Sending signed Salmon slap to $url ...\n";
+
+    $ok = $salmon->post($url, $entry, $profile);
+    if ($ok) {
+        print "OK\n\n";
+    } else {
+        print "FAIL\n\n";
+    }
+}
index d5a8c1bf029a450c0bb58d17a933469ec54cba94..2635cb7c9d09211d7b7c2bc26bf60526f586fde1 100644 (file)
@@ -248,11 +248,11 @@ class OpenIDPlugin extends Plugin
      *
      * If we're in openidOnly mode, we disable the menu for all other login.
      *
-     * @param Action &$action Action being executed
+     * @param Action $action Action being executed
      *
      * @return boolean hook return
      */
-    function onStartLoginGroupNav(&$action)
+    function onStartLoginGroupNav($action)
     {
         if (common_config('site', 'openidonly')) {
             $this->showOpenIDLoginTab($action);
@@ -268,11 +268,11 @@ class OpenIDPlugin extends Plugin
     /**
      * Menu item for login
      *
-     * @param Action &$action Action being executed
+     * @param Action $action Action being executed
      *
      * @return boolean hook return
      */
-    function onEndLoginGroupNav(&$action)
+    function onEndLoginGroupNav($action)
     {
         $this->showOpenIDLoginTab($action);
 
@@ -318,11 +318,11 @@ class OpenIDPlugin extends Plugin
     /**
      * Menu item for OpenID settings
      *
-     * @param Action &$action Action being executed
+     * @param Action $action Action being executed
      *
      * @return boolean hook return
      */
-    function onEndAccountSettingsNav(&$action)
+    function onEndAccountSettingsNav($action)
     {
         $action_name = $action->trimmed('action');
 
index 8d498d05490b65dec6b746cbaf9a5b941b62557c..dfabdd79036fd9d898fef7b6459ca344270e7cb7 100644 (file)
@@ -118,11 +118,24 @@ class FinishopenidloginAction extends Action
         $this->element('p', null,
                        _m('Create a new user with this nickname.'));
         $this->elementStart('ul', 'form_data');
+
+        // Hook point for captcha etc
+        Event::handle('StartRegistrationFormData', array($this));
+
         $this->elementStart('li');
         $this->input('newname', _m('New nickname'),
                      ($this->username) ? $this->username : '',
                      _m('1-64 lowercase letters or numbers, no punctuation or spaces'));
         $this->elementEnd('li');
+        $this->elementStart('li');
+        $this->input('email', _('Email'), $this->getEmail(),
+                     _('Used only for updates, announcements, '.
+                       'and password recovery'));
+        $this->elementEnd('li');
+
+        // Hook point for captcha etc
+        Event::handle('EndRegistrationFormData', array($this));
+
         $this->elementStart('li');
         $this->element('input', array('type' => 'checkbox',
                                       'id' => 'license',
@@ -180,6 +193,39 @@ class FinishopenidloginAction extends Action
         $this->elementEnd('form');
     }
 
+    /**
+     * Get specified e-mail from the form, or the OpenID sreg info, or the
+     * invite code.
+     *
+     * @return string
+     */
+    function getEmail()
+    {
+        $email = $this->trimmed('email');
+        if (!empty($email)) {
+            return $email;
+        }
+
+        // Pull from openid thingy
+        list($display, $canonical, $sreg) = $this->getSavedValues();
+        if (!empty($sreg['email'])) {
+            return $sreg['email'];
+        }
+
+        // Terrible hack for invites...
+        if (common_config('site', 'inviteonly')) {
+            $code = $_SESSION['invitecode'];
+            if ($code) {
+                $invite = Invitation::staticGet($code);
+
+                if ($invite && $invite->address_type == 'email') {
+                    return $invite->address;
+                }
+            }
+        }
+        return '';
+    }
+
     function tryLogin()
     {
         $consumer = oid_consumer();
@@ -347,11 +393,7 @@ class FinishopenidloginAction extends Action
             $fullname = '';
         }
 
-        if (!empty($sreg['email']) && Validate::email($sreg['email'], common_config('email', 'check_domain'))) {
-            $email = $sreg['email'];
-        } else {
-            $email = '';
-        }
+        $email = $this->getEmail();
 
         # XXX: add language
         # XXX: add timezone
index 6c0ef37d519b4f44d5d4b0636069ec6964fbe71e..980b7beb6e3f9fd362c36da0f97c912eb724d05b 100644 (file)
@@ -235,4 +235,24 @@ class RequireValidatedEmailPlugin extends Plugin
         }
         return true;
     }
+
+    /**
+     * Prevent unvalidated folks from creating spam groups.
+     *
+     * @param Profile $profile User profile we're checking
+     * @param string $right rights key
+     * @param boolean $result if overriding, set to true/false has right
+     * @return boolean hook result value
+     */
+    function onUserRightsCheck(Profile $profile, $right, &$result)
+    {
+        if ($right == Right::CREATEGROUP) {
+            $user = User::staticGet('id', $profile->id);
+            if ($user && !$this->validated($user)) {
+                $result = false;
+                return false;
+            }
+        }
+        return true;
+    }
 }
index 578ef6607c5fc846d3156f5f25ab15652425a28f..38a4c40d481aeea69e3f5a15e426221f65d7acb6 100644 (file)
@@ -93,15 +93,16 @@ class SubMirrorPlugin extends Plugin
     }
 
     /**
-     * Menu item for settings
+     * Menu item for personal subscriptions/groups area
      *
-     * @param Action &$action Action being executed
+     * @param Widget $widget Widget being executed
      *
      * @return boolean hook return
      */
 
-    function onEndAccountSettingsNav(&$action)
+    function onEndSubGroupNav($widget)
     {
+        $action = $widget->out;
         $action_name = $action->trimmed('action');
 
         $action->menuItem(common_local_url('mirrorsettings'),
@@ -163,4 +164,37 @@ class SubMirrorPlugin extends Plugin
         }
         return true;
     }
+
+    /**
+     * Add a count of mirrored feeds into a user's profile sidebar stats.
+     *
+     * @param Profile $profile
+     * @param array $stats
+     * @return boolean hook return value
+     */
+    function onProfileStats($profile, &$stats)
+    {
+        $cur = common_current_user();
+        if (!empty($cur) && $cur->id == $profile->id) {
+            $mirror = new SubMirror();
+            $mirror->subscriber = $profile->id;
+            $entry = array(
+                'id' => 'mirrors',
+                'label' => _m('Mirrored feeds'),
+                'link' => common_local_url('mirrorsettings'),
+                'value' => $mirror->count(),
+            );
+
+            $insertAt = count($stats);
+            foreach ($stats as $i => $row) {
+                if ($row['id'] == 'groups') {
+                    // Slip us in after them.
+                    $insertAt = $i + 1;
+                    break;
+                }
+            }
+            array_splice($stats, $insertAt, 0, array($entry));
+        }
+        return true;
+    }
 }
index a828b26feb68b07399662f83904f399af1a0cd8b..20e1807b3de286ed4e14aa2c3ba8532dd9fddae2 100644 (file)
@@ -102,4 +102,10 @@ class MirrorSettingsAction extends AccountSettingsAction
     function handlePost()
     {
     }
+
+    function showLocalNav()
+    {
+        $nav = new SubGroupNav($this, common_current_user());
+        $nav->show();
+    }
 }
index b2dce6f1c092148c7b2502fb6370bc1676723762..048daad98f4d1315bb53cebad71af498d58875a2 100644 (file)
@@ -137,11 +137,11 @@ class TwitterBridgePlugin extends Plugin
     /*
      * Add a login tab for 'Sign in with Twitter'
      *
-     * @param Action &action the current action
+     * @param Action $action the current action
      *
      * @return void
      */
-    function onEndLoginGroupNav(&$action)
+    function onEndLoginGroupNav($action)
     {
         $action_name = $action->trimmed('action');
 
@@ -160,11 +160,11 @@ class TwitterBridgePlugin extends Plugin
     /**
      * Add the Twitter Settings page to the Connect Settings menu
      *
-     * @param Action &$action The calling page
+     * @param Action $action The calling page
      *
      * @return boolean hook return
      */
-    function onEndConnectSettingsNav(&$action)
+    function onEndConnectSettingsNav($action)
     {
         if (self::hasKeys()) {
             $action_name = $action->trimmed('action');
index 28b40cc4f47b3ab56f1921f6f8605de20d1df38f..972fa527d41340fac906c14482bb2064237002d3 100644 (file)
@@ -241,7 +241,7 @@ class TwitterauthorizationAction extends Action
 
             $this->twuid = $twitter_user->id;
             $this->tw_fields = array("screen_name" => $twitter_user->screen_name,
-                                     "name" => $twitter_user->name);
+                                     "fullname" => $twitter_user->name);
             $this->access_token = $atok;
             $this->tryLogin();
         }
@@ -373,7 +373,7 @@ class TwitterauthorizationAction extends Action
         $this->hidden('access_token_secret', $this->access_token->secret);
         $this->hidden('twuid', $this->twuid);
         $this->hidden('tw_fields_screen_name', $this->tw_fields['screen_name']);
-        $this->hidden('tw_fields_name', $this->tw_fields['name']);
+        $this->hidden('tw_fields_name', $this->tw_fields['fullname']);
 
         $this->elementStart('fieldset');
         $this->hidden('token', common_session_token());
@@ -382,11 +382,24 @@ class TwitterauthorizationAction extends Action
         $this->element('p', null,
                        _m('Create a new user with this nickname.'));
         $this->elementStart('ul', 'form_data');
+
+        // Hook point for captcha etc
+        Event::handle('StartRegistrationFormData', array($this));
+
         $this->elementStart('li');
         $this->input('newname', _m('New nickname'),
                      ($this->username) ? $this->username : '',
                      _m('1-64 lowercase letters or numbers, no punctuation or spaces'));
         $this->elementEnd('li');
+        $this->elementStart('li');
+        $this->input('email', _('Email'), $this->getEmail(),
+                     _('Used only for updates, announcements, '.
+                       'and password recovery'));
+        $this->elementEnd('li');
+
+        // Hook point for captcha etc
+        Event::handle('EndRegistrationFormData', array($this));
+
         $this->elementEnd('ul');
         $this->submit('create', _m('Create'));
         $this->elementEnd('fieldset');
@@ -411,6 +424,32 @@ class TwitterauthorizationAction extends Action
         $this->elementEnd('form');
     }
 
+    /**
+     * Get specified e-mail from the form, or the invite code.
+     *
+     * @return string
+     */
+    function getEmail()
+    {
+        $email = $this->trimmed('email');
+        if (!empty($email)) {
+            return $email;
+        }
+
+        // Terrible hack for invites...
+        if (common_config('site', 'inviteonly')) {
+            $code = $_SESSION['invitecode'];
+            if ($code) {
+                $invite = Invitation::staticGet($code);
+
+                if ($invite && $invite->address_type == 'email') {
+                    return $invite->address;
+                }
+            }
+        }
+        return '';
+    }
+
     function message($msg)
     {
         $this->message_text = $msg;
@@ -462,7 +501,7 @@ class TwitterauthorizationAction extends Action
             return;
         }
 
-        $fullname = trim($this->tw_fields['name']);
+        $fullname = trim($this->tw_fields['fullname']);
 
         $args = array('nickname' => $nickname, 'fullname' => $fullname);
 
@@ -470,6 +509,11 @@ class TwitterauthorizationAction extends Action
             $args['code'] = $invite->code;
         }
 
+        $email = $this->getEmail();
+        if (!empty($email)) {
+            $args['email'] = $email;
+        }
+
         $user = User::register($args);
 
         if (empty($user)) {
@@ -602,8 +646,8 @@ class TwitterauthorizationAction extends Action
 
     function bestNewNickname()
     {
-        if (!empty($this->tw_fields['name'])) {
-            $nickname = $this->nicknamize($this->tw_fields['name']);
+        if (!empty($this->tw_fields['fullname'])) {
+            $nickname = $this->nicknamize($this->tw_fields['fullname']);
             if ($this->isNewNickname($nickname)) {
                 return $nickname;
             }
index fc7698841ecf70e35568bbb4774ba615dfb7baa4..a375717ccd59dc175d2f878cdc2c64ab75506d62 100644 (file)
@@ -121,12 +121,12 @@ class UserFlagPlugin extends Plugin
     /**
      * Add a 'flag' button to profile page
      *
-     * @param Action  &$action The action being called
+     * @param Action  $action The action being called
      * @param Profile $profile Profile being shown
      *
      * @return boolean hook result
      */
-    function onEndProfilePageActionsElements(&$action, $profile)
+    function onEndProfilePageActionsElements($action, $profile)
     {
         $this->showFlagButton($action, $profile,
                               array('action' => 'showstream',
diff --git a/scripts/moveuser.php b/scripts/moveuser.php
new file mode 100644 (file)
index 0000000..17b3e0f
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010 StatusNet, Inc.
+ *
+ * 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/>.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+
+$shortoptions = 'i:n:r:w:y';
+$longoptions = array('id=', 'nickname=', 'remote=', 'password=');
+
+$helptext = <<<END_OF_MOVEUSER_HELP
+moveuser.php [options]
+Move a local user to a remote account.
+
+  -i --id       ID of user to move
+  -n --nickname nickname of the user to move
+  -r --remote   Full ID of remote users
+  -w --password Password of remote user
+  -y --yes      do not wait for confirmation
+
+Remote user identity must be a Webfinger (nickname@example.com) or 
+an HTTP or HTTPS URL (http://example.com/social/site/user/nickname).
+
+END_OF_MOVEUSER_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+try {
+
+    $user = getUser();
+
+    $remote = get_option_value('r', 'remote');
+
+    if (empty($remote)) {
+        show_help();
+        exit(1);
+    }
+
+    $password = get_option_value('w', 'password');
+
+    if (!have_option('y', 'yes')) {
+        print "WARNING: EXPERIMENTAL FEATURE! Moving accounts will delete data from the source site.\n";
+        print "\n";
+        print "About to PERMANENTLY move user '{$user->nickname}' to $remote. Are you sure? [y/N] ";
+        $response = fgets(STDIN);
+        if (strtolower(trim($response)) != 'y') {
+            print "Aborting.\n";
+            exit(0);
+        }
+    }
+
+    $qm = QueueManager::get();
+
+    $qm->enqueue(array($user, $remote, $password), 'acctmove');
+
+} catch (Exception $e) {
+    print $e->getMessage()."\n";
+    exit(1);
+}
index 378478d741d1cd3ce205a206ab8aaa7df0f6923b..c2817a460241a584e89fb28d6040ceb0a6ce505e 100644 (file)
@@ -382,6 +382,29 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase
         }
     }
 
+    public function testExample10()
+    {
+        global $_example10;
+        $dom = new DOMDocument();
+        $dom->loadXML($_example10);
+
+        // example 10 is a PuSH item of a post on a group feed, as generated
+        // by 0.9.7 code after migration away from <activity:actor> to <author>
+        $feed = $dom->documentElement;
+        $entry = $dom->getElementsByTagName('entry')->item(0);
+        $expected = 'http://lazarus.local/mublog/user/557';
+
+        // Reading just the entry alone should pick up its own <author>
+        // as the actor.
+        $act = new Activity($entry);
+        $this->assertEquals($act->actor->id, $expected);
+
+        // Reading the entry in feed context used to be buggy, picking up
+        // the feed's <activity:subject> which referred to the group.
+        // It should now be returning the expected author entry...
+        $act = new Activity($entry, $feed);
+        $this->assertEquals($act->actor->id, $expected);
+    }
 }
 
 $_example1 = <<<EXAMPLE1
@@ -792,3 +815,93 @@ $_example9 = <<<EXAMPLE9
     </entry>
 </feed>
 EXAMPLE9;
+
+// Sample PuSH entry from a group feed in 0.9.7
+// Old <activity:actor> has been removed from entries in this version.
+// A bug in the order of input processing meant that we were incorrectly
+// reading the feed's <activity:subject> instead of the entry's <author>,
+// causing the entry to get rejected as malformed (groups can't post on
+// their own; we want to see the actual author's info here).
+$_example10 = <<<EXAMPLE10
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/">
+ <generator uri="http://status.net" version="0.9.7alpha1">StatusNet</generator>
+ <id>http://lazarus.local/mublog/api/statusnet/groups/timeline/22.atom</id>
+ <title>grouptest316173 timeline</title>
+ <subtitle>Updates from grouptest316173 on Blaguette!</subtitle>
+ <logo>http://lazarus.local/mublog/theme/default/default-avatar-profile.png</logo>
+ <updated>2011-01-06T22:44:18+00:00</updated>
+<author>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/group</activity:object-type>
+ <uri>http://lazarus.local/mublog/group/22/id</uri>
+ <name>grouptest316173</name>
+ <link rel="alternate" type="text/html" href="http://lazarus.local/mublog/group/22/id"/>
+ <link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://lazarus.local/mublog/theme/default/default-avatar-profile.png"/>
+ <link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://lazarus.local/mublog/theme/default/default-avatar-stream.png"/>
+ <link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://lazarus.local/mublog/theme/default/default-avatar-mini.png"/>
+ <poco:preferredUsername>grouptest316173</poco:preferredUsername>
+ <poco:displayName>grouptest316173</poco:displayName>
+</author>
+<activity:subject>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/group</activity:object-type>
+ <id>http://lazarus.local/mublog/group/22/id</id>
+ <title>grouptest316173</title>
+ <link rel="alternate" type="text/html" href="http://lazarus.local/mublog/group/22/id"/>
+ <link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://lazarus.local/mublog/theme/default/default-avatar-profile.png"/>
+ <link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://lazarus.local/mublog/theme/default/default-avatar-stream.png"/>
+ <link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://lazarus.local/mublog/theme/default/default-avatar-mini.png"/>
+ <poco:preferredUsername>grouptest316173</poco:preferredUsername>
+ <poco:displayName>grouptest316173</poco:displayName>
+</activity:subject>
+ <link href="http://lazarus.local/mublog/group/grouptest316173" rel="alternate" type="text/html"/>
+ <link href="http://lazarus.local/mublog/main/push/hub" rel="hub"/>
+ <link href="http://lazarus.local/mublog/main/salmon/group/22" rel="salmon"/>
+ <link href="http://lazarus.local/mublog/main/salmon/group/22" rel="http://salmon-protocol.org/ns/salmon-replies"/>
+ <link href="http://lazarus.local/mublog/main/salmon/group/22" rel="http://salmon-protocol.org/ns/salmon-mention"/>
+ <link href="http://lazarus.local/mublog/api/statusnet/groups/timeline/22.atom" rel="self" type="application/atom+xml"/>
+ <statusnet:group_info member_count="2"></statusnet:group_info>
+<entry>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+ <id>http://lazarus.local/mublog/notice/1243</id>
+ <title>Group post from local to !grouptest316173, should go out over push.</title>
+ <content type="html">Group post from local to !&lt;span class=&quot;vcard&quot;&gt;&lt;a href=&quot;http://lazarus.local/mublog/group/22/id&quot; class=&quot;url&quot;&gt;&lt;span class=&quot;fn nickname&quot;&gt;grouptest316173&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;, should go out over push.</content>
+ <link rel="alternate" type="text/html" href="http://lazarus.local/mublog/notice/1243"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <published>2011-01-06T22:44:18+00:00</published>
+ <updated>2011-01-06T22:44:18+00:00</updated>
+ <author>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+  <uri>http://lazarus.local/mublog/user/557</uri>
+  <name>Pubtest316173 Smith</name>
+  <link rel="alternate" type="text/html" href="http://lazarus.local/mublog/pubtest316173"/>
+  <link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://lazarus.local/mublog/theme/default/default-avatar-profile.png"/>
+  <link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://lazarus.local/mublog/theme/default/default-avatar-stream.png"/>
+  <link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://lazarus.local/mublog/theme/default/default-avatar-mini.png"/>
+  <poco:preferredUsername>pubtest316173</poco:preferredUsername>
+  <poco:displayName>Pubtest316173 Smith</poco:displayName>
+  <poco:note>Stub account for OStatus tests.</poco:note>
+  <poco:urls>
+   <poco:type>homepage</poco:type>
+   <poco:value>http://example.org/pubtest316173</poco:value>
+   <poco:primary>true</poco:primary>
+  </poco:urls>
+ </author>
+ <link rel="ostatus:conversation" href="http://lazarus.local/mublog/conversation/1131"/>
+ <link rel="ostatus:attention" href="http://lazarus.local/mublog/group/22/id"/>
+ <link rel="mentioned" href="http://lazarus.local/mublog/group/22/id"/>
+ <category term="grouptest316173"></category>
+ <source>
+  <id>http://lazarus.local/mublog/api/statuses/user_timeline/557.atom</id>
+  <title>Pubtest316173 Smith</title>
+  <link rel="alternate" type="text/html" href="http://lazarus.local/mublog/pubtest316173"/>
+  <link rel="self" type="application/atom+xml" href="http://lazarus.local/mublog/api/statuses/user_timeline/557.atom"/>
+  <link rel="license" href="http://creativecommons.org/licenses/by/3.0/"/>
+  <icon>http://lazarus.local/mublog/theme/default/default-avatar-profile.png</icon>
+  <updated>2011-01-06T22:44:18+00:00</updated>
+ </source>
+ <link rel="self" type="application/atom+xml" href="http://lazarus.local/mublog/api/statuses/show/1243.atom"/>
+ <link rel="edit" type="application/atom+xml" href="http://lazarus.local/mublog/api/statuses/show/1243.atom"/>
+ <statusnet:notice_info local_id="1243" source="api"></statusnet:notice_info>
+</entry>
+</feed>
+EXAMPLE10;