]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch '0.9.x' of git@gitorious.org:statusnet/mainline into 0.9.x
authorEvan Prodromou <evan@status.net>
Thu, 7 Jan 2010 07:23:30 +0000 (23:23 -0800)
committerEvan Prodromou <evan@status.net>
Thu, 7 Jan 2010 07:23:30 +0000 (23:23 -0800)
38 files changed:
actions/apigroupleave.php
actions/leavegroup.php
actions/twitapisearchatom.php
classes/Avatar.php
classes/Config.php
classes/Fave.php
classes/File_to_post.php
classes/Group_block.php
classes/Group_inbox.php
classes/Group_member.php
classes/Memcached_DataObject.php
classes/Notice_inbox.php
classes/Notice_tag.php
classes/Profile.php
classes/Profile_role.php
classes/Queue_item.php
classes/Subscription.php
doc-src/sms
lib/command.php
lib/htmloutputter.php
lib/jsonsearchresultslist.php
lib/schema.php
plugins/LdapAuthorization/LdapAuthorizationPlugin.php
plugins/Minify/MinifyPlugin.php
plugins/OpenID/User_openid_trustroot.php
plugins/RSSCloud/LoggingAggregator.php [new file with mode: 0644]
plugins/RSSCloud/README [new file with mode: 0644]
plugins/RSSCloud/RSSCloudNotifier.php [new file with mode: 0644]
plugins/RSSCloud/RSSCloudPlugin.php [new file with mode: 0644]
plugins/RSSCloud/RSSCloudQueueHandler.php [new file with mode: 0755]
plugins/RSSCloud/RSSCloudRequestNotify.php [new file with mode: 0644]
plugins/RSSCloud/RSSCloudSubscription.php [new file with mode: 0644]
plugins/Recaptcha/RecaptchaPlugin.php
plugins/UserFlag/UserFlagPlugin.php
plugins/UserFlag/User_flag_profile.php
scripts/console.php
scripts/stopdaemons.sh
theme/cloudy/css/display.css

index 514a3a557da4e923dbe419e9cf150607c92cbfe2..5627bfc14643a5bb2ae6f78a5d9528c6d9008fac 100644 (file)
@@ -108,7 +108,7 @@ class ApiGroupLeaveAction extends ApiAuthAction
         $member = new Group_member();
 
         $member->group_id   = $this->group->id;
-        $member->profile_id = $this->auth->id;
+        $member->profile_id = $this->auth_user->id;
 
         if (!$member->find(true)) {
             $this->serverError(_('You are not a member of this group.'));
@@ -118,12 +118,12 @@ class ApiGroupLeaveAction extends ApiAuthAction
         $result = $member->delete();
 
         if (!$result) {
-            common_log_db_error($member, 'INSERT', __FILE__);
+            common_log_db_error($member, 'DELETE', __FILE__);
             $this->serverError(
                 sprintf(
-                    _('Could not remove user %s to group %s.'),
+                    _('Could not remove user %s from group %s.'),
                     $this->user->nickname,
-                    $this->$group->nickname
+                    $this->group->nickname
                 )
             );
             return;
index 08fce150980eb406fce36bf46ceff307bf66ba32..90c85e1a4e6e602eed57d7b552745564f8c41ae2 100644 (file)
@@ -123,8 +123,8 @@ class LeavegroupAction extends Action
         $result = $member->delete();
 
         if (!$result) {
-            common_log_db_error($member, 'INSERT', __FILE__);
-            $this->serverError(sprintf(_('Could not remove user %s to group %s'),
+            common_log_db_error($member, 'DELETE', __FILE__);
+            $this->serverError(sprintf(_('Could not remove user %s from group %s'),
                                        $cur->nickname, $this->group->nickname));
         }
 
index 1cb8d7efe6d507ced64fa7597e3683eefac4daee..baed2a0c7c24107cb15731489fc312974ea267c6 100644 (file)
@@ -208,7 +208,14 @@ class TwitapisearchatomAction extends ApiAction
         $this->showFeed();
 
         foreach ($notices as $n) {
-            $this->showEntry($n);
+
+            $profile = $n->getProfile();
+
+            // Don't show notices from deleted users
+
+            if (!empty($profile)) {
+                $this->showEntry($n);
+            }
         }
 
         $this->endAtom();
index 8d6424e8b2d1f53c5ccec912404ccde6ee7b26d9..91bde0f0401b6dc144b48a6ba434c312c80c967c 100644 (file)
@@ -37,7 +37,7 @@ class Avatar extends Memcached_DataObject
         }
     }
 
-    function &pkeyGet($kv)
+    function pkeyGet($kv)
     {
         return Memcached_DataObject::pkeyGet('Avatar', $kv);
     }
index 6d914ca1f6e6a39b7e87c025be556c7e9a097d42..43b99587fa14971c2c66a32790d548da10f4e6c6 100644 (file)
@@ -120,7 +120,7 @@ class Config extends Memcached_DataObject
         return $result;
     }
 
-    function &pkeyGet($kv)
+    function pkeyGet($kv)
     {
         return Memcached_DataObject::pkeyGet('Config', $kv);
     }
index 11e876ff19dcc1294aae6ddb8dfda6e7486eda11..8113c8e1668a508251323f117468da25d119f7f6 100644 (file)
@@ -32,7 +32,7 @@ class Fave extends Memcached_DataObject
         return $fave;
     }
 
-    function &pkeyGet($kv)
+    function pkeyGet($kv)
     {
         return Memcached_DataObject::pkeyGet('Fave', $kv);
     }
index e3db91b205ab597f7c46dfdcd36f0dd01f953ed8..72a42b0880a46269ac22758c37798e2b8d411e8d 100644 (file)
@@ -62,7 +62,7 @@ class File_to_post extends Memcached_DataObject
         }
     }
 
-    function &pkeyGet($kv)
+    function pkeyGet($kv)
     {
         return Memcached_DataObject::pkeyGet('File_to_post', $kv);
     }
index de2cf5f6eb12c694a34d059f447e3ce4add972fd..9f4d592956eaf497ee7258a11ac049e66921af84 100644 (file)
@@ -40,7 +40,7 @@ class Group_block extends Memcached_DataObject
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
 
-    function &pkeyGet($kv)
+    function pkeyGet($kv)
     {
         return Memcached_DataObject::pkeyGet('Group_block', $kv);
     }
index 1af7439f7f749e668824773aa6363012e77e33f2..2a0787e387dc8ea087060dfc447cf661121d0e74 100644 (file)
@@ -20,7 +20,7 @@ class Group_inbox extends Memcached_DataObject
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
 
-    function &pkeyGet($kv)
+    function pkeyGet($kv)
     {
         return Memcached_DataObject::pkeyGet('Group_inbox', $kv);
     }
index 3c23a991f05f272e5f610d3bf62b724e83a343b8..069b2c7a1c75c4150a2a0263473ad60e9069868d 100644 (file)
@@ -21,7 +21,7 @@ class Group_member extends Memcached_DataObject
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
 
-    function &pkeyGet($kv)
+    function pkeyGet($kv)
     {
         return Memcached_DataObject::pkeyGet('Group_member', $kv);
     }
index 15ca3482183e45d5667a70ad1cdba5ae27eda38b..4e3cc5678867d26e6d2f6cf47b9cf1b9b1a51433 100644 (file)
@@ -90,17 +90,16 @@ class Memcached_DataObject extends DB_DataObject
             unset($i);
         }
         $i = Memcached_DataObject::getcached($cls, $k, $v);
-        if ($i !== false) { // false == cache miss
-            return $i;
-        } else {
+        if ($i === false) { // false == cache miss
             $i = DB_DataObject::factory($cls);
             if (empty($i)) {
-                return false;
+                $i = false;
+                return $i;
             }
             $result = $i->get($k, $v);
             if ($result) {
+                // Hit!
                 $i->encache();
-                return $i;
             } else {
                 // save the fact that no such row exists
                 $c = self::memcache();
@@ -108,12 +107,16 @@ class Memcached_DataObject extends DB_DataObject
                     $ck = self::cachekey($cls, $k, $v);
                     $c->set($ck, null);
                 }
-                return false;
+                $i = false;
             }
         }
+        return $i;
     }
 
-    function &pkeyGet($cls, $kv)
+    /**
+     * @fixme Should this return false on lookup fail to match staticGet?
+     */
+    function pkeyGet($cls, $kv)
     {
         $i = Memcached_DataObject::multicache($cls, $kv);
         if ($i !== false) { // false == cache miss
index d3ddad656a7aae8492f1bee4d80afd3959193b92..e350e6e2f8469b0281f5edff0656cfc741f61db8 100644 (file)
@@ -101,7 +101,7 @@ class Notice_inbox extends Memcached_DataObject
         return $ids;
     }
 
-    function &pkeyGet($kv)
+    function pkeyGet($kv)
     {
         return Memcached_DataObject::pkeyGet('Notice_inbox', $kv);
     }
index 02740280f5d402f959e9889303bcf847cd7091d7..79231f0b0c0777749c09cf91da451192ed55cb2f 100644 (file)
@@ -96,7 +96,7 @@ class Notice_tag extends Memcached_DataObject
         }
     }
 
-    function &pkeyGet($kv)
+    function pkeyGet($kv)
     {
         return Memcached_DataObject::pkeyGet('Notice_tag', $kv);
     }
index 03196447b891e31ef5b09e7af9d97b6942cf24dd..25d908dbf93c22c1a24fb41cbd5a204d2478e825 100644 (file)
@@ -504,6 +504,7 @@ class Profile extends Memcached_DataObject
                          'Reply',
                          'Group_member',
                          );
+        Event::handle('ProfileDeleteRelated', array($this, &$related));
 
         foreach ($related as $cls) {
             $inst = new $cls();
index afa7fb74e49e0e5902c725fd3af246d0c2077b57..74aca3730501777d5ef9d8afdc8e05b91a90caf4 100644 (file)
@@ -43,7 +43,7 @@ class Profile_role extends Memcached_DataObject
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
 
-    function &pkeyGet($kv)
+    function pkeyGet($kv)
     {
         return Memcached_DataObject::pkeyGet('Profile_role', $kv);
     }
index 295c321b57d4615fa5a139b9e14b9752fd19a4e3..9c673540d746a6a25d55a62ba6fffbe8d8b17b46 100644 (file)
@@ -55,7 +55,7 @@ class Queue_item extends Memcached_DataObject
         return null;
     }
 
-    function &pkeyGet($kv)
+    function pkeyGet($kv)
     {
         return Memcached_DataObject::pkeyGet('Queue_item', $kv);
     }
index fedfd5f19eeec5212a898fb967a00371dd23f56a..faf1331cda11565eec0655ea4a266079ceca0618 100644 (file)
@@ -46,7 +46,7 @@ class Subscription extends Memcached_DataObject
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
     
-    function &pkeyGet($kv)
+    function pkeyGet($kv)
     {
         return Memcached_DataObject::pkeyGet('Subscription', $kv);
     }
index 1a3064318fa7f6ce42dbd8814e08617a01505ea7..2c20921c35ec8d3832916e693dc3a05db05e37a3 100644 (file)
@@ -56,13 +56,4 @@ You can use the following commands with %%site.name%%.
 * sub &lt;nickname&gt; - same as 'follow'
 * unsub &lt;nickname&gt; - same as 'leave'
 * last &lt;nickname&gt; - same as 'get'
-* on &lt;nickname&gt; - not yet implemented.
-* off &lt;nickname&gt; - not yet implemented.
-* nudge &lt;nickname&gt; - not yet implemented.
-* invite &lt;phone number&gt; - not yet implemented.
-* track &lt;word&gt; - not yet implemented.
-* untrack &lt;word&gt; - not yet implemented.
-* track off - not yet implemented.
-* untrack all - not yet implemented.
-* tracks - not yet implemented.
-* tracking - not yet implemented.
+* nudge &lt;nickname&gt; - remind a user to update.
index 67140c3485f7cd56770d0feea56932865dfda03e..ad2e0bb975d999d806ec41f4a8c492b2a9f95c3b 100644 (file)
@@ -742,42 +742,42 @@ class HelpCommand extends Command
     function execute($channel)
     {
         $channel->output($this->user,
-                         _("Commands:\n".
-                           "on - turn on notifications\n".
-                           "off - turn off notifications\n".
-                           "help - show this help\n".
-                           "follow <nickname> - subscribe to user\n".
-                           "groups - lists the groups you have joined\n".
-                           "subscriptions - list the people you follow\n".
-                           "subscribers - list the people that follow you\n".
-                           "leave <nickname> - unsubscribe from user\n".
-                           "d <nickname> <text> - direct message to user\n".
-                           "get <nickname> - get last notice from user\n".
-                           "whois <nickname> - get profile info on user\n".
-                           "fav <nickname> - add user's last notice as a 'fave'\n".
-                           "fav #<notice_id> - add notice with the given id as a 'fave'\n".
-                           "repeat #<notice_id> - repeat a notice with a given id\n".
-                           "repeat <nickname> - repeat the last notice from user\n".
-                           "reply #<notice_id> - reply to notice with a given id\n".
-                           "reply <nickname> - reply to the last notice from user\n".
-                           "join <group> - join group\n".
-                           "login - Get a link to login to the web interface\n".
-                           "drop <group> - leave group\n".
-                           "stats - get your stats\n".
-                           "stop - same as 'off'\n".
-                           "quit - same as 'off'\n".
-                           "sub <nickname> - same as 'follow'\n".
-                           "unsub <nickname> - same as 'leave'\n".
-                           "last <nickname> - same as 'get'\n".
-                           "on <nickname> - not yet implemented.\n".
-                           "off <nickname> - not yet implemented.\n".
-                           "nudge <nickname> - remind a user to update.\n".
-                           "invite <phone number> - not yet implemented.\n".
-                           "track <word> - not yet implemented.\n".
-                           "untrack <word> - not yet implemented.\n".
-                           "track off - not yet implemented.\n".
-                           "untrack all - not yet implemented.\n".
-                           "tracks - not yet implemented.\n".
-                           "tracking - not yet implemented.\n"));
+                         _("Commands:")."\n".
+                         _("on - turn on notifications")."\n".
+                         _("off - turn off notifications")."\n".
+                         _("help - show this help")."\n".
+                         _("follow <nickname> - subscribe to user")."\n".
+                         _("groups - lists the groups you have joined")."\n".
+                         _("subscriptions - list the people you follow")."\n".
+                         _("subscribers - list the people that follow you")."\n".
+                         _("leave <nickname> - unsubscribe from user")."\n".
+                         _("d <nickname> <text> - direct message to user")."\n".
+                         _("get <nickname> - get last notice from user")."\n".
+                         _("whois <nickname> - get profile info on user")."\n".
+                         _("fav <nickname> - add user's last notice as a 'fave'")."\n".
+                         _("fav #<notice_id> - add notice with the given id as a 'fave'")."\n".
+                         _("repeat #<notice_id> - repeat a notice with a given id")."\n".
+                         _("repeat <nickname> - repeat the last notice from user")."\n".
+                         _("reply #<notice_id> - reply to notice with a given id")."\n".
+                         _("reply <nickname> - reply to the last notice from user")."\n".
+                         _("join <group> - join group")."\n".
+                         #_("login - Get a link to login to the web interface")."\n".
+                         _("drop <group> - leave group")."\n".
+                         _("stats - get your stats")."\n".
+                         _("stop - same as 'off'")."\n".
+                         _("quit - same as 'off'")."\n".
+                         _("sub <nickname> - same as 'follow'")."\n".
+                         _("unsub <nickname> - same as 'leave'")."\n".
+                         _("last <nickname> - same as 'get'")."\n".
+                         #_("on <nickname> - not yet implemented.")."\n".
+                         #_("off <nickname> - not yet implemented.")."\n".
+                         _("nudge <nickname> - remind a user to update.")."\n");
+                         #_("invite <phone number> - not yet implemented.")."\n".
+                         #_("track <word> - not yet implemented.")."\n".
+                         #_("untrack <word> - not yet implemented.")."\n".
+                         #_("track off - not yet implemented.")."\n".
+                         #_("untrack all - not yet implemented.")."\n".
+                         #_("tracks - not yet implemented.")."\n".
+                         #_("tracking - not yet implemented.")."\n"
     }
 }
index 2091c6e2ca4d60fd4020f2ca91cfbcb09558c909..31660ce954982e365be8e68e05d179bda72cadbd 100644 (file)
@@ -352,7 +352,7 @@ class HTMLOutputter extends XMLOutputter
     {
         if(Event::handle('StartScriptElement', array($this,&$src,&$type))) {
             $url = parse_url($src);
-            if( empty($url->scheme) && empty($url->host) && empty($url->query) && empty($url->fragment))
+            if( empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment']))
             {
                 $src = common_path($src) . '?version=' . STATUSNET_VERSION;
             }
index 569bfa87344c8997149d39c37ae70f139980d91a..0d72ddf7ab48184e41c371407ae9efefe6c940ea 100644 (file)
@@ -105,8 +105,14 @@ class JSONSearchResultsList
                 break;
             }
 
-            $item = new ResultItem($this->notice);
-            array_push($this->results, $item);
+            $profile = $this->notice->getProfile();
+
+            // Don't show notices from deleted users
+
+            if (!empty($profile)) {
+                $item = new ResultItem($this->notice);
+                array_push($this->results, $item);
+            }
         }
 
         $time_end           = microtime(true);
index 6fe442d56bbfcef5227f7b09196d3005b9ba7d07..a7f64ebed10cfce99fb780756a6fffa78831df83 100644 (file)
@@ -528,6 +528,10 @@ class Schema
             $sql .= " auto_increment ";
         }
 
+        if (!empty($cd->extra)) {
+            $sql .= "{$cd->extra} ";
+        }
+
         return $sql;
     }
 }
index 7673e61efbd4fe7a158f8f0980cae441a2a682b2..e5e22c0ddeab1dbce49cae5443166fc87bfdcac9 100644 (file)
@@ -52,7 +52,6 @@ class LdapAuthorizationPlugin extends AuthorizationPlugin
     public $attributes = array();
 
     function onInitializePlugin(){
-        parent::onInitializePlugin();
         if(!isset($this->host)){
             throw new Exception("must specify a host");
         }
index 71fade19a5767a41fd6b080f2aa01be3f8e6dd1b..718bfd163530d92db7ed03c16acd14de55d04cb2 100644 (file)
@@ -84,7 +84,7 @@ class MinifyPlugin extends Plugin
 
     function onStartScriptElement($action,&$src,&$type) {
         $url = parse_url($src);
-        if( empty($url->scheme) && empty($url->host) && empty($url->query) && empty($url->fragment))
+        if( empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment']))
         {
             $src = $this->minifyUrl($src);
         }
index 44288945be2fcaf3a4bdb908b1fc0da9117df1e3..0b411b8f7f11ba9e10cfa932cb58407809dc0f18 100644 (file)
@@ -22,7 +22,7 @@ class User_openid_trustroot extends Memcached_DataObject
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
 
-    function &pkeyGet($kv)
+    function pkeyGet($kv)
     {
         return Memcached_DataObject::pkeyGet('User_openid_trustroot', $kv);
     }
diff --git a/plugins/RSSCloud/LoggingAggregator.php b/plugins/RSSCloud/LoggingAggregator.php
new file mode 100644 (file)
index 0000000..e37eed1
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+/**
+ * This test class pretends to be an RSS aggregator. It logs notifications
+ * from the cloud.
+ *
+ * PHP version 5
+ *
+ * @category Plugin
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://status.net/
+ *
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, 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/>.
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * Dummy aggregator that acts as a proper notification handler. It
+ * doesn't do anything but respond correctly when notified via
+ * REST.  Mostly, this is just and action I used to develop the plugin
+ * and easily test things end-to-end. I'm leaving it in here as it
+ * may be useful for developing the plugin further.
+ *
+ * @category Plugin
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://status.net/
+ **/
+class LoggingAggregatorAction extends Action
+{
+
+    var $challenge = null;
+    var $url       = null;
+
+    /**
+     * Initialization.
+     *
+     * @param array $args Web and URL arguments
+     *
+     * @return boolean false if user doesn't exist
+     */
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        $this->url       = $this->arg('url');
+        $this->challenge = $this->arg('challenge');
+
+        common_debug("args = " . var_export($this->args, true));
+        common_debug('url = ' . $this->url . ' challenge = ' . $this->challenge);
+
+        return true;
+    }
+
+    /**
+     * Handle the request
+     *
+     * @param array $args $_REQUEST data (unused)
+     *
+     * @return void
+     */
+
+    function handle($args)
+    {
+        parent::handle($args);
+
+        if (empty($this->url)) {
+            $this->showError('Hey, you have to provide a url parameter.');
+            return;
+        }
+
+        if (!empty($this->challenge)) {
+
+            // must be a GET
+
+            if ($_SERVER['REQUEST_METHOD'] != 'GET') {
+                $this->showError('This resource requires an HTTP GET.');
+                return;
+            }
+
+            header('Content-Type: text/xml');
+            echo $this->challenge;
+
+        } else {
+
+            // must be a POST
+
+            if ($_SERVER['REQUEST_METHOD'] != 'POST') {
+                $this->showError('This resource requires an HTTP POST.');
+                return;
+            }
+
+            header('Content-Type: text/xml');
+            Echo "<notifyResult success='true' msg='Thanks for the update.' />\n";
+        }
+
+        $this->ip = $_SERVER['REMOTE_ADDR'];
+
+        common_log(LOG_INFO, 'RSSCloud Logging Aggregator - ' .
+                   $this->ip . ' claims the feed at ' .
+                   $this->url . ' has been updated.');
+    }
+
+    /**
+     * Show an XML error when things go badly
+     *
+     * @param string $msg the error message
+     *
+     * @return void
+     */
+
+    function showError($msg)
+    {
+        header('HTTP/1.1 400 Bad Request');
+        header('Content-Type: text/xml');
+        echo "<?xml version='1.0'?>\n";
+        echo "<notifyResult success='false' msg='$msg' />\n";
+    }
+
+}
\ No newline at end of file
diff --git a/plugins/RSSCloud/README b/plugins/RSSCloud/README
new file mode 100644 (file)
index 0000000..1237e3e
--- /dev/null
@@ -0,0 +1,54 @@
+This plugin enables RSSCloud (http://rsscloud.org/) publishing and
+subscription handling for RSS 2.0 profile feeds (i.e:
+http://SITE/PATH/api/statuses/user_timeline/USERNAME.rss). When the
+plugin is enabled, StatusNet acts as both the publisher and hub ('writer' and
+'cloud' in RSSCloud parlance), but only for local StatusNet feeds. It's
+not possible to use it as a general purpose hub -- for instance you can't
+subscribe and get updates to a Wordpress feed from StatusNet using this
+plugin.
+
+To use the plugin, add the following to your config.php:
+
+    addPlugin('RSSCloud');
+
+Enabling the plugin will add a <cloud> element to your RSS 2.0 profile feeds
+that looks like this:
+
+    <cloud domain="SITE" port="80" path="/main/rsscloud/request_notify"
+    registerProcedure="" protocol="http-post"/>
+
+Aggregators may subscribe by sending a proper REST RSSCloud subscription
+request (the optional 'domain' parameter with challenge is supported).
+Subscribing aggregators will be notified ('pinged') when users they have
+subscribed to post new notices. Currently, REST is the only protocol
+supported for notifications.
+
+Deamon
+------
+
+There's also a daemon for offline processing of queued notices with
+RSSCloud destinations, which will start automatically if/when you run
+scripts/startdaemons.sh.
+
+Notes
+-----
+
+- Again, only RSS 2.0 profile feeds may be subscribed to, and they have
+  to be the ones with user names in them, like:
+      http://SITE/PATH/api/statuses/user_timeline/USERNAME.rss
+- Subscriptions are deleted after three notification failures in a row
+  (not sure this is optimal).
+- The plugin includes a dummy LoggingAggregator class that can be used
+  for end-to-end testing.  You probably don't want to mess with it.
+
+TODO
+----
+
+- Figure out why the RSSCloudSubcription can't ->delete() or ->update()
+- Support pinging via XML-RPC and SOAP
+- Automatically delete subscriptions? Point of reference: Dave's hub
+  implementation auto-deletes them after 25 hours. WordPress never deletes them.
+- Support additional feed URL addresses for the same feed (e.g.: by numeric ID,
+  ?user_id=xxx, etc.)
+- Support additional feeds that make sense (e.g: replies)?
+- Possibly use "rssCloud" (like Dave) instead of "RSSCloud" everywhere
diff --git a/plugins/RSSCloud/RSSCloudNotifier.php b/plugins/RSSCloud/RSSCloudNotifier.php
new file mode 100644 (file)
index 0000000..d454691
--- /dev/null
@@ -0,0 +1,240 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class to ping an rssCloud endpoint when a feed has been updated
+ *
+ * PHP version 5
+ *
+ * LICENCE: 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  Plugin
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * Class for notifying cloud-enabled RSS aggregators that StatusNet
+ * feeds have been updated.
+ *
+ * @category Plugin
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://status.net/
+ **/
+class RSSCloudNotifier
+{
+    const MAX_FAILURES = 3;
+
+    /**
+     * Send an HTTP GET to the notification handler with a
+     * challenge string to see if it repsonds correctly.
+     *
+     * @param string $endpoint URL of the notification handler
+     * @param string $feed     the feed being subscribed to
+     *
+     * @return boolean success
+     */
+    function challenge($endpoint, $feed)
+    {
+        $code   = common_confirmation_code(128);
+        $params = array('url' => $feed, 'challenge' => $code);
+        $url    = $endpoint . '?' . http_build_query($params);
+
+        try {
+            $client   = new HTTPClient();
+            $response = $client->get($url);
+        } catch (HTTP_Request2_Exception $e) {
+            common_log(LOG_INFO,
+                       'RSSCloud plugin - failure testing notify handler ' .
+                       $endpoint . ' - '  . $e->getMessage());
+            return false;
+        }
+
+        // Check response is betweet 200 and 299 and body contains challenge data
+
+        $status = $response->getStatus();
+        $body   = $response->getBody();
+
+        if ($status >= 200 && $status < 300) {
+
+            // NOTE: the spec says that the body must contain the string
+            // challenge.  It doesn't say that the body must contain the
+            // challenge string ONLY, although that seems to be the way
+            // the other implementors have interpreted it.
+
+            if (strpos($body, $code) !== false) {
+                common_log(LOG_INFO, 'RSSCloud plugin - ' .
+                           "success testing notify handler:  $endpoint");
+                return true;
+            } else {
+                common_log(LOG_INFO, 'RSSCloud plugin - ' .
+                          'challenge/repsonse failed for notify handler ' .
+                           $endpoint);
+                common_debug('body = ' . var_export($body, true));
+                return false;
+            }
+        } else {
+            common_log(LOG_INFO, 'RSSCloud plugin - ' .
+                       "failure testing notify handler:  $endpoint " .
+                       ' - got HTTP ' . $status);
+            common_debug('body = ' . var_export($body, true));
+            return false;
+        }
+    }
+
+    /**
+     * HTTP POST a notification that a feed has been updated
+     * ('ping the cloud').
+     *
+     * @param String $endpoint URL of the notification handler
+     * @param String $feed     the feed being subscribed to
+     *
+     * @return boolean success
+     */
+    function postUpdate($endpoint, $feed)
+    {
+
+        $headers  = array();
+        $postdata = array('url' => $feed);
+
+        try {
+            $client   = new HTTPClient();
+            $response = $client->post($endpoint, $headers, $postdata);
+        } catch (HTTP_Request2_Exception $e) {
+            common_log(LOG_INFO, 'RSSCloud plugin - failure notifying ' .
+                       $endpoint . ' that feed ' . $feed .
+                       ' has changed: ' . $e->getMessage());
+            return false;
+        }
+
+        $status = $response->getStatus();
+
+        if ($status >= 200 && $status < 300) {
+            common_log(LOG_INFO, 'RSSCloud plugin - success notifying ' .
+                       $endpoint . ' that feed ' . $feed . ' has changed.');
+            return true;
+        } else {
+            common_log(LOG_INFO, 'RSSCloud plugin - failure notifying ' .
+                       $endpoint . ' that feed ' . $feed .
+                       ' has changed: got HTTP ' . $status);
+            return false;
+        }
+    }
+
+    /**
+     * Notify all subscribers to a profile feed that it has changed.
+     *
+     * @param Profile $profile the profile whose feed has been
+     *        updated
+     *
+     * @return boolean success
+     */
+    function notify($profile)
+    {
+        $feed = common_path('api/statuses/user_timeline/') .
+          $profile->nickname . '.rss';
+
+        $cloudSub = new RSSCloudSubscription();
+
+        $cloudSub->subscribed = $profile->id;
+
+        if ($cloudSub->find()) {
+            while ($cloudSub->fetch()) {
+                $result = $this->postUpdate($cloudSub->url, $feed);
+                if ($result == false) {
+                    $this->handleFailure($cloudSub);
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Handle problems posting cloud notifications. Increment the failure
+     * count, or delete the subscription if the maximum number of failures
+     * is exceeded.
+     *
+     * XXX: Redo with proper DB_DataObject methods once I figure out what
+     * what the problem is with pluginized DB_DataObjects. -Z
+     *
+     * @param RSSCloudSubscription $cloudSub the subscription in question
+     *
+     * @return boolean success
+     */
+    function handleFailure($cloudSub)
+    {
+        $failCnt = $cloudSub->failures + 1;
+
+        if ($failCnt == self::MAX_FAILURES) {
+
+            common_log(LOG_INFO,
+                       'Deleting RSSCloud subcription ' .
+                       '(max failure count reached), profile: ' .
+                       $cloudSub->subscribed .
+                       ' handler: ' .
+                       $cloudSub->url);
+
+            // XXX: WTF! ->delete() doesn't work. Clearly, there are some issues with
+            // the DB_DataObject, or my understanding of it.  Have to drop into SQL.
+
+            // $result = $cloudSub->delete();
+
+            $qry = 'DELETE from rsscloud_subscription' .
+              ' WHERE subscribed = ' . $cloudSub->subscribed .
+              ' AND url = \'' . $cloudSub->url . '\'';
+
+            $result = $cloudSub->query($qry);
+
+            if (!$result) {
+                common_log_db_error($cloudSub, 'DELETE', __FILE__);
+                common_log(LOG_ERR, 'Could not delete RSSCloud subscription.');
+            }
+
+        } else {
+
+            common_debug('Updating failure count on RSSCloud subscription. ' .
+                         $failCnt);
+
+            $failCnt = $cloudSub->failures + 1;
+
+            // XXX: ->update() not working either, gar!
+
+            $qry = 'UPDATE rsscloud_subscription' .
+              ' SET failures = ' . $failCnt .
+              ' WHERE subscribed = ' . $cloudSub->subscribed .
+              ' AND url = \'' . $cloudSub->url . '\'';
+
+            $result = $cloudSub->query($qry);
+
+            if (!$result) {
+                common_log_db_error($cloudsub, 'UPDATE', __FILE__);
+                common_log(LOG_ERR,
+                           'Could not update failure ' .
+                           'count on RSSCloud subscription');
+            }
+        }
+    }
+
+}
+
diff --git a/plugins/RSSCloud/RSSCloudPlugin.php b/plugins/RSSCloud/RSSCloudPlugin.php
new file mode 100644 (file)
index 0000000..4b9812a
--- /dev/null
@@ -0,0 +1,279 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin to support RSSCloud
+ *
+ * PHP version 5
+ *
+ * LICENCE: 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  Plugin
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * Plugin class for adding RSSCloud capabilities to StatusNet
+ *
+ * @category Plugin
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://status.net/
+ **/
+
+class RSSCloudPlugin extends Plugin
+{
+    /**
+     * Our friend, the constructor
+     *
+     * @return void
+     */
+    function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Setup the info for the subscription handler. Allow overriding
+     * to point at another cloud hub (not currently used).
+     *
+     * @return void
+     */
+
+    function onInitializePlugin()
+    {
+        $this->domain   = common_config('rsscloud', 'domain');
+        $this->port     = common_config('rsscloud', 'port');
+        $this->path     = common_config('rsscloud', 'path');
+        $this->funct    = common_config('rsscloud', 'function');
+        $this->protocol = common_config('rsscloud', 'protocol');
+
+        // set defaults
+
+        $local_server = parse_url(common_path('main/rsscloud/request_notify'));
+
+        if (empty($this->domain)) {
+            $this->domain = $local_server['host'];
+        }
+
+        if (empty($this->port)) {
+            $this->port = '80';
+        }
+
+        if (empty($this->path)) {
+            $this->path = $local_server['path'];
+        }
+
+        if (empty($this->funct)) {
+            $this->funct = '';
+        }
+
+        if (empty($this->protocol)) {
+            $this->protocol = 'http-post';
+        }
+    }
+
+    /**
+     * Add RSSCloud-related paths to the router table
+     *
+     * Hook for RouterInitialized event.
+     *
+     * @param Mapper &$m URL parser and mapper
+     *
+     * @return boolean hook return
+     */
+
+    function onRouterInitialized(&$m)
+    {
+        $m->connect('/main/rsscloud/request_notify',
+                    array('action' => 'RSSCloudRequestNotify'));
+
+        // XXX: This is just for end-to-end testing. Uncomment if you need to pretend
+        //      to be a cloud hub for some reason.
+        //$m->connect('/main/rsscloud/notify',
+        //            array('action' => 'LoggingAggregator'));
+
+        return true;
+    }
+
+    /**
+     * Automatically load the actions and libraries used by
+     * the RSSCloud plugin
+     *
+     * @param Class $cls the class
+     *
+     * @return boolean hook return
+     *
+     */
+
+    function onAutoload($cls)
+    {
+        switch ($cls)
+        {
+        case 'RSSCloudSubscription':
+            include_once INSTALLDIR . '/plugins/RSSCloud/RSSCloudSubscription.php';
+            return false;
+        case 'RSSCloudNotifier':
+            include_once INSTALLDIR . '/plugins/RSSCloud/RSSCloudNotifier.php';
+            return false;
+        case 'RSSCloudRequestNotifyAction':
+        case 'LoggingAggregatorAction':
+            include_once INSTALLDIR . '/plugins/RSSCloud/' .
+              mb_substr($cls, 0, -6) . '.php';
+            return false;
+        default:
+            return true;
+        }
+    }
+
+    /**
+     * Add a <cloud> element to the RSS feed (after the rss <channel>
+     * element is started).
+     *
+     * @param Action $action the ApiAction
+     *
+     * @return void
+     */
+
+    function onStartApiRss($action)
+    {
+        if (get_class($action) == 'ApiTimelineUserAction') {
+
+            $attrs = array('domain'            => $this->domain,
+                           'port'              => $this->port,
+                           'path'              => $this->path,
+                           'registerProcedure' => $this->funct,
+                           'protocol'          => $this->protocol);
+
+            // Dipping into XMLWriter to avoid a full end element (</cloud>).
+
+            $action->xw->startElement('cloud');
+            foreach ($attrs as $name => $value) {
+                $action->xw->writeAttribute($name, $value);
+            }
+
+            $action->xw->endElement();
+        }
+    }
+
+    /**
+     * Add an RSSCloud queue item for each notice
+     *
+     * @param Notice $notice      the notice
+     * @param array  &$transports the list of transports (queues)
+     *
+     * @return boolean hook return
+     */
+
+    function onStartEnqueueNotice($notice, &$transports)
+    {
+        array_push($transports, 'rsscloud');
+        return true;
+    }
+
+    /**
+     * broadcast the message when not using queuehandler
+     *
+     * @param Notice &$notice the notice
+     * @param array  $queue   destination queue
+     *
+     * @return boolean hook return
+     */
+
+    function onUnqueueHandleNotice(&$notice, $queue)
+    {
+        if (($queue == 'rsscloud') && ($this->_isLocal($notice))) {
+
+            common_debug('broadcasting rssCloud bound notice ' . $notice->id);
+
+            $profile = $notice->getProfile();
+
+            $notifier = new RSSCloudNotifier();
+            $notifier->notify($profile);
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Determine whether the notice was locally created
+     *
+     * @param Notice $notice the notice in question
+     *
+     * @return boolean locality
+     */
+
+    function _isLocal($notice)
+    {
+        return ($notice->is_local == Notice::LOCAL_PUBLIC ||
+                $notice->is_local == Notice::LOCAL_NONPUBLIC);
+    }
+
+    /**
+     * Create the rsscloud_subscription table if it's not
+     * already in the DB
+     *
+     * @return boolean hook return
+     */
+
+    function onCheckSchema()
+    {
+        $schema = Schema::get();
+        $schema->ensureTable('rsscloud_subscription',
+                             array(new ColumnDef('subscribed', 'integer',
+                                                 null, false, 'PRI'),
+                                   new ColumnDef('url', 'varchar',
+                                                 '255', false, 'PRI'),
+                                   new ColumnDef('failures', 'integer',
+                                                 null, false, null, 0),
+                                   new ColumnDef('created', 'datetime',
+                                                 null, false),
+                                   new ColumnDef('modified', 'timestamp',
+                                                 null, false, null,
+                                                 'CURRENT_TIMESTAMP',
+                                                 'on update CURRENT_TIMESTAMP')
+                                   ));
+         return true;
+    }
+
+    /**
+     * Add RSSCloudQueueHandler to the list of valid daemons to
+     * start
+     *
+     * @param array $daemons the list of daemons to run
+     *
+     * @return boolean hook return
+     *
+     */
+
+    function onGetValidDaemons($daemons)
+    {
+        array_push($daemons, INSTALLDIR .
+                   '/plugins/RSSCloud/RSSCloudQueueHandler.php');
+        return true;
+    }
+
+}
+
diff --git a/plugins/RSSCloud/RSSCloudQueueHandler.php b/plugins/RSSCloud/RSSCloudQueueHandler.php
new file mode 100755 (executable)
index 0000000..693dd27
--- /dev/null
@@ -0,0 +1,78 @@
+#!/usr/bin/env php
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, 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::';
+$longoptions = array('id::');
+
+$helptext = <<<END_OF_ENJIT_HELP
+Daemon script for pushing new notices to RSSCloud subscribers.
+
+    -i --id           Identity (default none)
+
+END_OF_ENJIT_HELP;
+
+require_once INSTALLDIR . '/scripts/commandline.inc';
+require_once INSTALLDIR . '/lib/queuehandler.php';
+require_once INSTALLDIR . '/plugins/RSSCloud/RSSCloudNotifier.php';
+require_once INSTALLDIR . '/plugins/RSSCloud/RSSCloudSubscription.php';
+
+class RSSCloudQueueHandler extends QueueHandler
+{
+    var $notifier = null;
+
+    function transport()
+    {
+        return 'rsscloud';
+    }
+
+    function start()
+    {
+        $this->log(LOG_INFO, "INITIALIZE");
+        $this->notifier = new RSSCloudNotifier();
+        return true;
+    }
+
+    function handle_notice($notice)
+    {
+        $profile = $notice->getProfile();
+        return $this->notifier->notify($profile);
+    }
+
+    function finish()
+    {
+    }
+
+}
+
+if (have_option('i')) {
+    $id = get_option_value('i');
+} else if (have_option('--id')) {
+    $id = get_option_value('--id');
+} else if (count($args) > 0) {
+    $id = $args[0];
+} else {
+    $id = null;
+}
+
+$handler = new RSSCloudQueueHandler($id);
+
+$handler->runOnce();
diff --git a/plugins/RSSCloud/RSSCloudRequestNotify.php b/plugins/RSSCloud/RSSCloudRequestNotify.php
new file mode 100644 (file)
index 0000000..d76c08d
--- /dev/null
@@ -0,0 +1,347 @@
+<?php
+/**
+ * Action to let RSSCloud aggregators request update notification when
+ * user profile feeds change.
+ *
+ * PHP version 5
+ *
+ * @category Plugin
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://status.net/
+ *
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, 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/>.
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * Action class to handle RSSCloud notification (subscription) requests
+ *
+ * @category Plugin
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://status.net/
+ **/
+
+class RSSCloudRequestNotifyAction extends Action
+{
+    /**
+     * Initialization.
+     *
+     * @param array $args Web and URL arguments
+     *
+     * @return boolean false if user doesn't exist
+     */
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        $this->ip   = $_SERVER['REMOTE_ADDR'];
+        $this->port = $this->arg('port');
+        $this->path = $this->arg('path');
+
+        if ($this->path[0] != '/') {
+            $this->path = '/' . $this->path;
+        }
+
+        $this->protocol  = $this->arg('protocol');
+        $this->procedure = $this->arg('notifyProcedure');
+        $this->domain    = $this->arg('domain');
+
+        $this->feeds = $this->getFeeds();
+
+        return true;
+    }
+
+    /**
+     * Handle the request
+     *
+     * Checks for all the required parameters for a subscription,
+     * validates that the feed being subscribed to is real, and then
+     * saves the subsctiption.
+     *
+     * @param array $args $_REQUEST data (unused)
+     *
+     * @return void
+     */
+
+    function handle($args)
+    {
+        parent::handle($args);
+
+        if ($_SERVER['REQUEST_METHOD'] != 'POST') {
+            $this->showResult(false, 'Request must be POST.');
+            return;
+        }
+
+        $missing = array();
+
+        if (empty($this->port)) {
+            $missing[] = 'port';
+        }
+
+        if (empty($this->path)) {
+            $missing[] = 'path';
+        }
+
+        if (empty($this->protocol)) {
+            $missing[] = 'protocol';
+        } else if (strtolower($this->protocol) != 'http-post') {
+            $msg = 'Only http-post notifications are supported at this time.';
+            $this->showResult(false, $msg);
+            return;
+        }
+
+        if (!isset($this->procedure)) {
+            $missing[] = 'notifyProcedure';
+        }
+
+        if (!empty($missing)) {
+            $msg = 'The following parameters were missing from the request body: ' .
+                implode(', ', $missing) . '.';
+            $this->showResult(false, $msg);
+            return;
+        }
+
+        if (empty($this->feeds)) {
+            $msg = 'You must provide at least one valid profile feed url ' .
+              '(url1, url2, url3 ... urlN).';
+            $this->showResult(false, $msg);
+            return;
+        }
+
+        // We have to validate everything before saving anything.
+        // We only return one success or failure no matter how
+        // many feeds the subscriber is trying to subscribe to
+
+        foreach ($this->feeds as $feed) {
+
+            if (!$this->validateFeed($feed)) {
+
+                $nh = $this->getNotifyUrl();
+                common_log(LOG_WARNING,
+                           "RSSCloud plugin - $nh tried to subscribe to invalid feed: $feed");
+
+                $msg = 'Feed subscription failed - Not a valid feed.';
+                $this->showResult(false, $msg);
+                return;
+            }
+
+            if (!$this->testNotificationHandler($feed)) {
+                $msg = 'Feed subscription failed - ' .
+                'notification handler doesn\'t respond correctly.';
+                $this->showResult(false, $msg);
+                return;
+            }
+
+        }
+
+        foreach ($this->feeds as $feed) {
+            $this->saveSubscription($feed);
+        }
+
+        // XXX: What to do about deleting stale subscriptions?
+        // 25 hours seems harsh. WordPress doesn't ever remove
+        // subscriptions.
+
+        $msg = 'Thanks for the subscription. ' .
+          'When the feed(s) update(s) we\'ll notify you.';
+
+        $this->showResult(true, $msg);
+    }
+
+    /**
+     * Validate that the requested feed is one we serve
+     * up via RSSCloud.
+     *
+     * @param string $feed the feed in question
+     *
+     * @return void
+     */
+
+    function validateFeed($feed)
+    {
+        $user = $this->userFromFeed($feed);
+
+        if (empty($user)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Pull all of the urls (url1, url2, url3...urlN) that
+     * the subscriber wants to subscribe to.
+     *
+     * @return array $feeds the list of feeds
+     */
+
+    function getFeeds()
+    {
+        $feeds = array();
+
+        while (list($key, $feed) = each($this->args)) {
+            if (preg_match('/^url\d*$/', $key)) {
+                $feeds[] = $feed;
+            }
+        }
+
+        return $feeds;
+    }
+
+    /**
+     * Test that a notification handler is there and is reponding
+     * correctly.  This is called before adding a subscription.
+     *
+     * @param string $feed the feed to verify
+     *
+     * @return boolean success result
+     */
+
+    function testNotificationHandler($feed)
+    {
+        $notifyUrl = $this->getNotifyUrl();
+
+        $notifier = new RSSCloudNotifier();
+
+        if (isset($this->domain)) {
+
+            // 'domain' param set, so we have to use GET and send a challenge
+
+            common_log(LOG_INFO,
+                       'RSSCloud plugin - Testing notification handler with challenge: ' .
+                       $notifyUrl);
+            return $notifier->challenge($notifyUrl, $feed);
+
+        } else {
+            common_log(LOG_INFO, 'RSSCloud plugin - Testing notification handler: ' .
+                       $notifyUrl);
+
+            return $notifier->postUpdate($notifyUrl, $feed);
+        }
+    }
+
+    /**
+     * Build the URL for the notification handler based on the
+     * parameters passed in with the subscription request.
+     *
+     * @return string notification handler url
+     */
+
+    function getNotifyUrl()
+    {
+        if (isset($this->domain)) {
+            return 'http://' . $this->domain . ':' . $this->port . $this->path;
+        } else {
+            return 'http://' . $this->ip . ':' . $this->port . $this->path;
+        }
+    }
+
+    /**
+     * Uses the nickname part of the subscribed feed URL to figure out
+     * whethere there's really a user with such a feed.  Used to
+     * validate feeds before adding a subscription.
+     *
+     * @param string $feed the feed in question
+     *
+     * @return boolean success
+     */
+
+    function userFromFeed($feed)
+    {
+        // We only do profile feeds
+
+        $path  = common_path('api/statuses/user_timeline/');
+        $valid = '%^' . $path . '(?<nickname>.*)\.rss$%';
+
+        if (preg_match($valid, $feed, $matches)) {
+            $user = User::staticGet('nickname', $matches['nickname']);
+            if (!empty($user)) {
+                return $user;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Save an RSSCloud subscription
+     *
+     * @param string $feed a valid profile feed
+     *
+     * @return boolean success result
+     */
+
+    function saveSubscription($feed)
+    {
+        $user = $this->userFromFeed($feed);
+
+        $notifyUrl = $this->getNotifyUrl();
+
+        $sub = RSSCloudSubscription::getSubscription($user->id, $notifyUrl);
+
+        if ($sub) {
+            common_log(LOG_INFO, "RSSCloud plugin - $notifyUrl refreshed subscription" .
+                         " to user $user->nickname (id: $user->id).");
+        } else {
+
+            $sub = new RSSCloudSubscription();
+
+            $sub->subscribed = $user->id;
+            $sub->url        = $notifyUrl;
+            $sub->created    = common_sql_now();
+
+            if (!$sub->insert()) {
+                common_log_db_error($sub, 'INSERT', __FILE__);
+                return false;
+            }
+
+            common_log(LOG_INFO, "RSSCloud plugin - $notifyUrl subscribed" .
+                       " to user $user->nickname (id: $user->id)");
+        }
+
+        return true;
+    }
+
+    /**
+     * Show an XML message indicating the subscription
+     * was successful or failed.
+     *
+     * @param boolean $success whether it was good or bad
+     * @param string  $msg     the message to output
+     *
+     * @return boolean success result
+     */
+
+    function showResult($success, $msg)
+    {
+        $this->startXML();
+        $this->elementStart('notifyResult',
+                            array('success' => ($success) ? 'true' : 'false',
+                                  'msg'     => $msg));
+        $this->endXML();
+    }
+
+}
+
diff --git a/plugins/RSSCloud/RSSCloudSubscription.php b/plugins/RSSCloud/RSSCloudSubscription.php
new file mode 100644 (file)
index 0000000..396c604
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, 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/>.
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+    exit(1);
+}
+
+/**
+ * Table Definition for rsscloud_subscription
+ */
+
+require_once INSTALLDIR . '/classes/Memcached_DataObject.php';
+
+class RSSCloudSubscription extends Memcached_DataObject {
+
+    var $__table='rsscloud_subscription'; // table name
+    var $subscribed;                      // int    primary key user id
+    var $url;                             // string primary key
+    var $failures;                        // int
+    var $created;                         // datestamp()
+    var $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP
+
+    function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('DataObjects_Grp',$k,$v); }
+
+    function table()
+    {
+
+        $db = $this->getDatabaseConnection();
+        $dbtype = $db->phptype;
+
+        $cols = array('subscribed' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+                      'url'        => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+                      'failures'   => DB_DATAOBJECT_INT,
+                      'created'    => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
+                      'modified'  => ($dbtype == 'mysql' || $dbtype == 'mysqli') ?
+                      DB_DATAOBJECT_MYSQLTIMESTAMP + DB_DATAOBJECT_NOTNULL :
+                      DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME
+                      );
+
+        return $cols;
+    }
+
+    function keys()
+    {
+        return array('subscribed' => 'N', 'url' => 'N');
+    }
+
+    static function getSubscription($subscribed, $url)
+    {
+        $sub = new RSSCloudSubscription();
+        $sub->whereAdd("subscribed = $subscribed");
+        $sub->whereAdd("url = '$url'");
+        $sub->limit(1);
+
+        if ($sub->find()) {
+            $sub->fetch();
+            return $sub;
+        }
+
+        return false;
+    }
+
+}
index db118dbb810dcf5ce946fc5b066e68ef7ff64ecc..3665214f85710c603f811763c78e08a7e7afe5e1 100644 (file)
@@ -62,9 +62,8 @@ class RecaptchaPlugin extends Plugin
 
     function onEndRegistrationFormData($action)
     {
-        $action->style('#recaptcha_area{float:left;}');
         $action->elementStart('li');
-        $action->raw('<label for="recaptcha_area">Captcha</label>');
+        $action->raw('<label for="recaptcha">Captcha</label>');
         if($this->checkssl() === true) {
             $action->raw(recaptcha_get_html($this->public_key), null, true);
         } else { 
index 602a5bfa881d20c6739b71269a09fcc6b87f2803..a33869c19ea2be076c4db748a100a1fb9e268b3f 100644 (file)
@@ -102,20 +102,20 @@ class UserFlagPlugin extends Plugin
 
     function onAutoload($cls)
     {
-        switch ($cls)
+        switch (strtolower($cls))
         {
-        case 'FlagprofileAction':
-        case 'AdminprofileflagAction':
-        case 'ClearflagAction':
+        case 'flagprofileaction':
+        case 'adminprofileflagaction':
+        case 'clearflagaction':
             include_once INSTALLDIR.'/plugins/UserFlag/' .
               strtolower(mb_substr($cls, 0, -6)) . '.php';
             return false;
-        case 'FlagProfileForm':
-        case 'ClearFlagForm':
+        case 'flagprofileform':
+        case 'clearflagform':
             include_once INSTALLDIR.'/plugins/UserFlag/' . strtolower($cls . '.php');
             return false;
-        case 'User_flag_profile':
-            include_once INSTALLDIR.'/plugins/UserFlag/'.$cls.'.php';
+        case 'user_flag_profile':
+            include_once INSTALLDIR.'/plugins/UserFlag/'.ucfirst(strtolower($cls)).'.php';
             return false;
         default:
             return true;
@@ -258,4 +258,39 @@ class UserFlagPlugin extends Plugin
         }
         return true;
     }
+
+    /**
+     * Ensure that flag entries for a profile are deleted
+     * along with the profile when deleting users.
+     * This prevents breakage of the admin profile flag UI.
+     *
+     * @param Profile $profile
+     * @param array &$related list of related tables; entries
+     *              with matching profile_id will be deleted.
+     *
+     * @return boolean hook result
+     */
+
+    function onProfileDeleteRelated($profile, &$related)
+    {
+        $related[] = 'user_flag_profile';
+        return true;
+    }
+
+    /**
+     * Ensure that flag entries created by a user are deleted
+     * when that user gets deleted.
+     *
+     * @param User $user
+     * @param array &$related list of related tables; entries
+     *              with matching user_id will be deleted.
+     *
+     * @return boolean hook result
+     */
+
+    function onUserDeleteRelated($user, &$related)
+    {
+        $related[] = 'user_flag_profile';
+        return true;
+    }
 }
index 6bf47071b26b84f2bfdeb9e95b07db7a42634d4c..bc4251cf7f11010d62128c4d196e0cd9602c86c7 100644 (file)
@@ -108,7 +108,7 @@ class User_flag_profile extends Memcached_DataObject
      * @return User_flag_profile found object or null
      */
 
-    function &pkeyGet($kv)
+    function pkeyGet($kv)
     {
         return Memcached_DataObject::pkeyGet('User_flag_profile', $kv);
     }
index 329caf472407a4764d5b017a2dfedc5f9cba0cb3..8b62a3a96783d7a7442326f33007d4f859e8d4f3 100755 (executable)
@@ -128,6 +128,8 @@ function console_help()
 if (CONSOLE_INTERACTIVE) {
     print "StatusNet interactive PHP console... type ctrl+D or enter 'exit' to exit.\n";
     $prompt = common_config('site', 'name') . '> ';
+} else {
+    $prompt = '';
 }
 while (!feof(STDIN)) {
     $line = read_input_line($prompt);
index 90e7331ca476c0736169e03f161e1e58f5ae6c5e..c790f1f349715f04c77c958887490ab239418951 100755 (executable)
@@ -25,7 +25,7 @@ DIR=`php $SDIR/getpiddir.php`
 
 for f in jabberhandler ombhandler publichandler smshandler pinghandler \
         xmppconfirmhandler xmppdaemon twitterhandler facebookhandler \
-        twitterstatusfetcher synctwitterfriends pluginhandler; do
+        twitterstatusfetcher synctwitterfriends pluginhandler rsscloudhandler; do
 
        FILES="$DIR/$f.*.pid"
        for ff in "$FILES" ; do
index 92d54497737b7779eeddba33b303783cbf72d562..a27bd74b801638e77e090b122e597e2713b11c9a 100644 (file)
@@ -242,6 +242,7 @@ margin-right:-47px;
 
 #header {
 width:100%;
+height:10.5em;
 position:relative;
 float:left;
 padding-top:18px;
@@ -1000,7 +1001,7 @@ float:left;
 font-size:0.95em;
 margin-left:59px;
 min-width:60%;
-max-width:66%;
+max-width:62%;
 }
 #showstream .notice div.entry-content,
 #shownotice .notice div.entry-content {
@@ -1517,12 +1518,13 @@ min-width:0;
 #subscribers.user_in #content,
 #showgroup.user_in #content,
 #conversation.user_in #content,
-#siteadminpanel #content,
-#designadminpanel #content,
-#useradminpanel #content,
-#pathsadminpanel #content,
-#adminprofileflag #content {
-padding-top:170px;
+#attachment.user_in #content,
+#siteadminpanel.user_in #content,
+#designadminpanel.user_in #content,
+#useradminpanel.user_in #content,
+#pathsadminpanel.user_in #content,
+#adminprofileflag.user_in #content {
+padding-top:12.5em;
 }
 
 #profilesettings #form_notice,