]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch 'testing' into 0.9.x
authorEvan Prodromou <evan@status.net>
Sun, 31 Jan 2010 20:27:58 +0000 (15:27 -0500)
committerEvan Prodromou <evan@status.net>
Sun, 31 Jan 2010 20:27:58 +0000 (15:27 -0500)
Conflicts:
actions/apioauthauthorize.php

47 files changed:
EVENTS.txt
README
actions/apioauthauthorize.php
actions/geocode.php
actions/public.php
actions/robotstxt.php [new file with mode: 0644]
actions/rsd.php [new file with mode: 0644]
actions/showstream.php
classes/Memcached_DataObject.php
classes/Notice.php
classes/Profile_role.php
classes/Session.php
classes/User.php
classes/status_network.ini
db/rc3torc4.sql
index.php
lib/api.php
lib/common.php
lib/default.php
lib/distribqueuehandler.php
lib/jabber.php
lib/jabberqueuehandler.php
lib/liberalstomp.php
lib/mysqlschema.php [new file with mode: 0644]
lib/ombqueuehandler.php
lib/pgsqlschema.php [new file with mode: 0644]
lib/publicqueuehandler.php
lib/router.php
lib/schema.php
lib/stompqueuemanager.php
lib/util.php
plugins/Adsense/AdsensePlugin.php [new file with mode: 0644]
plugins/GeonamesPlugin.php
plugins/MemcachePlugin.php
plugins/OpenX/OpenXPlugin.php [new file with mode: 0644]
plugins/Realtime/RealtimePlugin.php
plugins/Realtime/realtimeupdate.js
plugins/UserLimitPlugin.php [new file with mode: 0644]
scripts/delete_status_network.sh
scripts/queuedaemon.php
scripts/sendemail.php [new file with mode: 0755]
scripts/settag.php [new file with mode: 0644]
scripts/setup.cfg.sample
scripts/setup_status_network.sh
theme/base/css/display.css
theme/default/css/display.css
theme/identica/css/display.css

index 1ed670697b7570506662bbf617f0754b25329cde..6bf12bf13fb15db9ea94507971c635266db383ee 100644 (file)
@@ -699,3 +699,18 @@ StartShowContentLicense: Showing the default license for content
 
 EndShowContentLicense: Showing the default license for content
 - $action: the current action
+
+StartUserRegister: When a new user is being registered
+- &$profile: new profile data (no ID)
+- &$user: new user account (no ID or URI)
+
+EndUserRegister: When a new user has been registered
+- &$profile: new profile data
+- &$user: new user account
+
+StartRobotsTxt: Before outputting the robots.txt page
+- &$action: RobotstxtAction being shown
+
+EndRobotsTxt: After the default robots.txt page (good place for customization)
+- &$action: RobotstxtAction being shown
+
diff --git a/README b/README
index f83873ca84487803a276de5fbc71cc6b4b9270df..4e576dcdd358094931c186c17f9a42a030853e95 100644 (file)
--- a/README
+++ b/README
@@ -2,8 +2,8 @@
 README
 ------
 
-StatusNet 0.9.0 ("Stand") Beta 3
-20 Jan 2010
+StatusNet 0.9.0 ("Stand") Beta 4
+27 Jan 2010
 
 This is the README file for StatusNet (formerly Laconica), the Open
 Source microblogging platform. It includes installation instructions,
@@ -597,26 +597,19 @@ server is probably a good idea for high-volume sites.
    needs as a parameter the install path; if you run it from the
    StatusNet dir, "." should suffice.
 
-This will run eight (for now) queue handlers:
+This will run the queue handlers:
 
+* queuedaemon.php - polls for queued items for inbox processing and
+  pushing out to OMB, SMS, XMPP, etc.
 * xmppdaemon.php - listens for new XMPP messages from users and stores
-  them as notices in the database.
-* jabberqueuehandler.php - sends queued notices in the database to
-  registered users who should receive them.
-* publicqueuehandler.php - sends queued notices in the database to
-  public feed listeners.
-* ombqueuehandler.php - sends queued notices to OpenMicroBlogging
-  recipients on foreign servers.
-* smsqueuehandler.php - sends queued notices to SMS-over-email addresses
-  of registered users.
-* xmppconfirmhandler.php - sends confirmation messages to registered
-  users.
-
-Note that these queue daemons are pretty raw, and need your care. In
-particular, they leak memory, and you may want to restart them on a
-regular (daily or so) basis with a cron job. Also, if they lose
-the connection to the XMPP server for too long, they'll simply die. It
-may be a good idea to use a daemon-monitoring service, like 'monit',
+  them as notices in the database; also pulls queued XMPP output from
+  queuedaemon.php to push out to clients.
+
+These two daemons will automatically restart in most cases of failure
+including memory leaks (if a memory_limit is set), but may still die
+or behave oddly if they lose connections to the XMPP or queue servers.
+
+It may be a good idea to use a daemon-monitoring service, like 'monit',
 to check their status and keep them running.
 
 All the daemons write their process IDs (pids) to /var/run/ by
@@ -626,7 +619,7 @@ daemons.
 Since version 0.8.0, it's now possible to use a STOMP server instead of
 our kind of hacky home-grown DB-based queue solution. See the "queues"
 config section below for how to configure to use STOMP. As of this
-writing, the software has been tested with ActiveMQ (
+writing, the software has been tested with ActiveMQ.
 
 Sitemaps
 --------
@@ -712,10 +705,12 @@ subdirectory to add a new language to your system. You'll need to
 compile the ".po" files into ".mo" files, however.
 
 Contributions of translation information to StatusNet are very easy:
-you can use the Web interface at http://status.net/pootle/ to add one
+you can use the Web interface at TranslateWiki.net to add one
 or a few or lots of new translations -- or even new languages. You can
 also download more up-to-date .po files there, if you so desire.
 
+For info on helping with translations, see http://status.net/wiki/Translations
+
 Backups
 -------
 
@@ -1501,6 +1496,20 @@ interface. It also makes the user's profile the root URL.
 enabled: Whether to run in "single user mode". Default false.
 nickname: nickname of the single user.
 
+robotstxt
+---------
+
+We put out a default robots.txt file to guide the processing of
+Web crawlers. See http://www.robotstxt.org/ for more information
+on the format of this file.
+
+crawldelay: if non-empty, this value is provided as the Crawl-Delay:
+            for the robots.txt file. see http://ur1.ca/l5a0
+            for more information. Default is zero, no explicit delay.
+disallow: Array of (virtual) directories to disallow. Default is 'main',
+          'search', 'message', 'settings', 'admin'. Ignored when site
+          is private, in which case the entire site ('/') is disallowed.
+
 Plugins
 =======
 
index eebc926ee285a154bedd9c6b443f8e61a615a12f..dec0dc9f6cf6318640b13b214a71087abf998dec 100644 (file)
@@ -303,8 +303,9 @@ class ApiOauthAuthorizeAction extends ApiOauthAction
         $access = ($this->app->access_type & Oauth_application::$writeAccess) ?
           'access and update' : 'access';
 
-        $msg = _("The application <strong>%1$s</strong> by <strong>%2$s</strong> would like " .
-                 "the ability to <strong>%3$s</strong> your account data.");
+        $msg = _('The application <strong>%1$s</strong> by ' .
+                 '<strong>%2$s</strong> would like the ability ' .
+                 'to <strong>%3$s</strong> your account data.');
 
         $this->raw(sprintf($msg,
                            $this->app->name,
index 9671d2c2769f4feb615688dadf37634e1c06d3d0..e883c6ce4162950a4d0b0e23842cb78bbe015741 100644 (file)
@@ -42,6 +42,10 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
  */
 class GeocodeAction extends Action
 {
+    var $lat = null;
+    var $lon = null;
+    var $location = null;
+
     function prepare($args)
     {
         parent::prepare($args);
@@ -52,12 +56,7 @@ class GeocodeAction extends Action
         }
         $this->lat = $this->trimmed('lat');
         $this->lon = $this->trimmed('lon');
-        $location = Location::fromLatLon($this->lat, $this->lon);
-        if ($location) {
-            $this->location = Location::fromId($location->location_id, $location->location_ns);
-            $this->lat = $this->location->lat;
-            $this->lon = $this->location->lon;
-        }
+        $this->location = Location::fromLatLon($this->lat, $this->lon);
         return true;
     }
 
@@ -95,4 +94,3 @@ class GeocodeAction extends Action
         return true;
     }
 }
-?>
index 982dfde15700863f6fff8da111a7f04376fdb070..50278bfcedab55a80c9fe2c2ed359ad79399e727 100644 (file)
@@ -131,12 +131,20 @@ class PublicAction extends Action
             return _('Public timeline');
         }
     }
-    
+
     function extraHead()
     {
         parent::extraHead();
         $this->element('meta', array('http-equiv' => 'X-XRDS-Location',
                                            'content' => common_local_url('publicxrds')));
+
+        $rsd = common_local_url('rsd');
+
+        // RSD, http://tales.phrasewise.com/rfc/rsd
+
+        $this->element('link', array('rel' => 'EditURI',
+                                     'type' => 'application/rsd+xml',
+                                     'href' => $rsd));
     }
 
     /**
diff --git a/actions/robotstxt.php b/actions/robotstxt.php
new file mode 100644 (file)
index 0000000..5131097
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * robots.txt generator
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://status.net/
+ *
+ * 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);
+}
+
+/**
+ * Prints out a static robots.txt
+ *
+ * @category Action
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://status.net/
+ */
+
+class RobotstxtAction extends Action
+{
+    /**
+     * Handles requests
+     *
+     * Since this is a relatively static document, we
+     * don't do a prepare()
+     *
+     * @param array $args GET, POST, and URL params; unused.
+     *
+     * @return void
+     */
+
+    function handle($args)
+    {
+        if (Event::handle('StartRobotsTxt', array($this))) {
+
+            header('Content-Type: text/plain');
+
+            print "User-Agent: *\n";
+
+            if (common_config('site', 'private')) {
+
+                print "Disallow: /\n";
+
+            } else {
+
+                $disallow = common_config('robotstxt', 'disallow');
+
+                foreach ($disallow as $dir) {
+                    print "Disallow: /$dir/\n";
+                }
+
+                $crawldelay = common_config('robotstxt', 'crawldelay');
+
+                if (!empty($crawldelay)) {
+                    print "Crawl-delay: " . $crawldelay . "\n";
+                }
+            }
+
+            Event::handle('EndRobotsTxt', array($this));
+        }
+    }
+
+    /**
+     * Return true; this page doesn't touch the DB.
+     *
+     * @param array $args other arguments
+     *
+     * @return boolean is read only action?
+     */
+
+    function isReadOnly($args)
+    {
+        return true;
+    }
+}
diff --git a/actions/rsd.php b/actions/rsd.php
new file mode 100644 (file)
index 0000000..f88bf2e
--- /dev/null
@@ -0,0 +1,226 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008-2010, StatusNet, Inc.
+ *
+ * Really Simple Discovery (RSD) for API access
+ *
+ * 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 API
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://status.net/
+ *
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * RSD action class
+ *
+ * Really Simple Discovery (RSD) is a simple (to a fault, maybe)
+ * discovery tool for blog APIs.
+ *
+ * http://tales.phrasewise.com/rfc/rsd
+ *
+ * Anil Dash suggested that RSD be used for services that implement
+ * the Twitter API:
+ *
+ * http://dashes.com/anil/2009/12/the-twitter-api-is-finished.html
+ *
+ * It's in use now for WordPress.com blogs:
+ *
+ * http://matt.wordpress.com/xmlrpc.php?rsd
+ *
+ * I (evan@status.net) have tried to stay faithful to the premise of
+ * RSD, while adding information useful to StatusNet client developers.
+ * In particular:
+ *
+ * - There is a link from each user's profile page to their personal
+ *   RSD feed. A personal rsd.xml includes a 'blogID' element that is
+ *   their username.
+ * - There is a link from the public root to '/rsd.xml', a public RSD
+ *   feed. It's identical to the personal rsd except it doesn't include
+ *   a blogId.
+ * - I've added a setting to the API to indicate that OAuth support is
+ *   available.
+ *
+ * @category API
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://status.net/
+ */
+
+class RsdAction extends Action
+{
+    /**
+     * Optional attribute for the personal rsd.xml file.
+     */
+
+    var $user = null;
+
+    /**
+     * Prepare the action for use.
+     *
+     * Check for a nickname; redirect if non-canonical; if
+     * not provided, assume public rsd.xml.
+     *
+     * @param array $args GET, POST, and URI arguments.
+     *
+     * @return boolean success flag
+     */
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        // optional argument
+
+        $nickname_arg = $this->arg('nickname');
+
+        if (empty($nickname_arg)) {
+            $this->user = null;
+        } else {
+            $nickname = common_canonical_nickname($nickname_arg);
+
+            // Permanent redirect on non-canonical nickname
+
+            if ($nickname_arg != $nickname) {
+                common_redirect(common_local_url('rsd',
+                                                 array('nickname' => $nickname)),
+                                301);
+                return false;
+            }
+
+            $this->user = User::staticGet('nickname', $nickname);
+
+            if (empty($this->user)) {
+                $this->clientError(_('No such user.'), 404);
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Action handler.
+     *
+     * Outputs the XML format for an RSD file. May include
+     * personal information if this is a personal file
+     * (based on whether $user attribute is set).
+     *
+     * @param array $args array of arguments
+     *
+     * @return nothing
+     */
+
+    function handle($args)
+    {
+        header('Content-Type: application/rsd+xml');
+
+        $this->startXML();
+
+        $rsdNS = 'http://archipelago.phrasewise.com/rsd';
+        $this->elementStart('rsd', array('version' => '1.0',
+                                         'xmlns' => $rsdNS));
+        $this->elementStart('service');
+        $this->element('engineName', null, _('StatusNet'));
+        $this->element('engineLink', null, 'http://status.net/');
+        $this->elementStart('apis');
+        if (Event::handle('StartRsdListApis', array($this, $this->user))) {
+
+            $blogID   = (empty($this->user)) ? '' : $this->user->nickname;
+            $apiAttrs = array('name' => 'Twitter',
+                              'preferred' => 'true',
+                              'apiLink' => $this->_apiRoot(),
+                              'blogID' => $blogID);
+
+            $this->elementStart('api', $apiAttrs);
+            $this->elementStart('settings');
+            $this->element('docs', null,
+                           'http://status.net/wiki/TwitterCompatibleAPI');
+            $this->element('setting', array('name' => 'OAuth'),
+                           'true');
+            $this->elementEnd('settings');
+            $this->elementEnd('api');
+            Event::handle('EndRsdListApis', array($this, $this->user));
+        }
+        $this->elementEnd('apis');
+        $this->elementEnd('service');
+        $this->elementEnd('rsd');
+
+        $this->endXML();
+
+        return true;
+    }
+
+    /**
+     * Returns last-modified date for use in caching
+     *
+     * Per-user rsd.xml is dated to last change of user
+     * (in case of nickname change); public has no date.
+     *
+     * @return string date of last change of this page
+     */
+
+    function lastModified()
+    {
+        if (!empty($this->user)) {
+            return $this->user->modified;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Flag to indicate if this action is read-only
+     *
+     * It is; it doesn't change the DB.
+     *
+     * @param array $args ignored
+     *
+     * @return boolean true
+     */
+
+    function isReadOnly($args)
+    {
+        return true;
+    }
+
+    /**
+     * Return current site's API root
+     *
+     * Varies based on URL parameters, like if fancy URLs are
+     * turned on.
+     *
+     * @return string API root URI for this site
+     */
+
+    private function _apiRoot()
+    {
+        if (common_config('site', 'fancy')) {
+            return common_path('api/', true);
+        } else {
+            return common_path('index.php/api/', true);
+        }
+    }
+}
index c529193860ff886d02b937514f9f8474b9b2af3b..07cc68b765cb216e86025f52b2fc4d2390230f0a 100644 (file)
@@ -178,6 +178,15 @@ class ShowstreamAction extends ProfileAction
         $this->element('link', array('rel' => 'microsummary',
                                      'href' => common_local_url('microsummary',
                                                                 array('nickname' => $this->profile->nickname))));
+
+        $rsd = common_local_url('rsd',
+                                array('nickname' => $this->profile->nickname));
+
+        // RSD, http://tales.phrasewise.com/rfc/rsd
+        $this->element('link', array('rel' => 'EditURI',
+                                     'type' => 'application/rsd+xml',
+                                     'href' => $rsd));
+
     }
 
     function showProfile()
index 2cc6377f83f1a5b38545d4faeb6709792fbfe721..ab65c30ce28579a8684b4e9fa8d9cc8124d2b833 100644 (file)
@@ -147,6 +147,7 @@ class Memcached_DataObject extends DB_DataObject
     {
         $result = parent::insert();
         if ($result) {
+            $this->fixupTimestamps();
             $this->encache(); // in case of cached negative lookups
         }
         return $result;
@@ -159,6 +160,7 @@ class Memcached_DataObject extends DB_DataObject
         }
         $result = parent::update($orig);
         if ($result) {
+            $this->fixupTimestamps();
             $this->encache();
         }
         return $result;
@@ -366,7 +368,7 @@ class Memcached_DataObject extends DB_DataObject
     }
 
     /**
-     * sends query to database - this is the private one that must work 
+     * sends query to database - this is the private one that must work
      *   - internal functions use this rather than $this->query()
      *
      * Overridden to do logging.
@@ -428,7 +430,7 @@ class Memcached_DataObject extends DB_DataObject
         //
         // WARNING WARNING if we end up actually using multiple DBs at a time
         // we'll need some fancier logic here.
-        if (!$exists && !empty($_DB_DATAOBJECT['CONNECTIONS'])) {
+        if (!$exists && !empty($_DB_DATAOBJECT['CONNECTIONS']) && php_sapi_name() == 'cli') {
             foreach ($_DB_DATAOBJECT['CONNECTIONS'] as $index => $conn) {
                 if (!empty($conn)) {
                     $conn->disconnect();
@@ -529,4 +531,51 @@ class Memcached_DataObject extends DB_DataObject
 
         return $c->delete($cacheKey);
     }
+
+    function fixupTimestamps()
+    {
+        // Fake up timestamp columns
+        $columns = $this->table();
+        foreach ($columns as $name => $type) {
+            if ($type & DB_DATAOBJECT_MYSQLTIMESTAMP) {
+                $this->$name = common_sql_now();
+            }
+        }
+    }
+
+    function debugDump()
+    {
+        common_debug("debugDump: " . common_log_objstring($this));
+    }
+
+    function raiseError($message, $type = null, $behaviour = null)
+    {
+        throw new ServerException("DB_DataObject error [$type]: $message");
+    }
+
+    static function cacheGet($keyPart)
+    {
+        $c = self::memcache();
+
+        if (empty($c)) {
+            return false;
+        }
+
+        $cacheKey = common_cache_key($keyPart);
+
+        return $c->get($cacheKey);
+    }
+
+    static function cacheSet($keyPart, $value)
+    {
+        $c = self::memcache();
+
+        if (empty($c)) {
+            return false;
+        }
+
+        $cacheKey = common_cache_key($keyPart);
+
+        return $c->set($cacheKey, $value);
+    }
 }
index 0966697e215431f05724a047433397c76799490b..42878d94f1aed2fc7da3c7355d41e9e64ac11268 100644 (file)
@@ -140,7 +140,7 @@ class Notice extends Memcached_DataObject
         foreach(array_unique($hashtags) as $hashtag) {
             /* elide characters we don't want in the tag */
             $this->saveTag($hashtag);
-            self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, $tag->tag);
+            self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, $hashtag);
         }
         return true;
     }
@@ -326,9 +326,7 @@ class Notice extends Memcached_DataObject
         # XXX: someone clever could prepend instead of clearing the cache
         $notice->blowOnInsert();
 
-        $qm = QueueManager::get();
-
-        $qm->enqueue($notice, 'distrib');
+        $notice->distribute();
 
         return $notice;
     }
@@ -1374,8 +1372,6 @@ class Notice extends Memcached_DataObject
         }
 
         $reply->free();
-
-        return $ids;
     }
 
     function clearRepeats()
@@ -1445,4 +1441,31 @@ class Notice extends Memcached_DataObject
 
         $gi->free();
     }
+
+    function distribute()
+    {
+        if (common_config('queue', 'inboxes')) {
+            // If there's a failure, we want to _force_
+            // distribution at this point.
+            try {
+                $qm = QueueManager::get();
+                $qm->enqueue($this, 'distrib');
+            } catch (Exception $e) {
+                // If the exception isn't transient, this
+                // may throw more exceptions as DQH does
+                // its own enqueueing. So, we ignore them!
+                try {
+                    $handler = new DistribQueueHandler();
+                    $handler->handle($this);
+                } catch (Exception $e) {
+                    common_log(LOG_ERR, "emergency redistribution resulted in " . $e->getMessage());
+                }
+                // Re-throw so somebody smarter can handle it.
+                throw $e;
+            }
+        } else {
+            $handler = new DistribQueueHandler();
+            $handler->handle($this);
+        }
+    }
 }
index 74aca3730501777d5ef9d8afdc8e05b91a90caf4..bf2c453ed0bf719f9cc00d5ff679c5cac1f6d708 100644 (file)
@@ -48,6 +48,7 @@ class Profile_role extends Memcached_DataObject
         return Memcached_DataObject::pkeyGet('Profile_role', $kv);
     }
 
+    const OWNER         = 'owner';
     const MODERATOR     = 'moderator';
     const ADMINISTRATOR = 'administrator';
     const SANDBOXED     = 'sandboxed';
index 79a69a96ea3dc11b1f93672890f1687f2e263fa0..2422f8b68ecf975dd3794a051917f6fc495a7a02 100644 (file)
@@ -64,8 +64,12 @@ class Session extends Memcached_DataObject
         $session = Session::staticGet('id', $id);
 
         if (empty($session)) {
+            self::logdeb("Couldn't find '$id'");
             return '';
         } else {
+            self::logdeb("Found '$id', returning " .
+                         strlen($session->session_data) .
+                         " chars of data");
             return (string)$session->session_data;
         }
     }
@@ -77,14 +81,24 @@ class Session extends Memcached_DataObject
         $session = Session::staticGet('id', $id);
 
         if (empty($session)) {
+            self::logdeb("'$id' doesn't yet exist; inserting.");
             $session = new Session();
 
             $session->id           = $id;
             $session->session_data = $session_data;
             $session->created      = common_sql_now();
 
-            return $session->insert();
+            $result = $session->insert();
+
+            if (!$result) {
+                common_log_db_error($session, 'INSERT', __FILE__);
+                self::logdeb("Failed to insert '$id'.");
+            } else {
+                self::logdeb("Successfully inserted '$id' (result = $result).");
+            }
+            return $result;
         } else {
+            self::logdeb("'$id' already exists; updating.");
             if (strcmp($session->session_data, $session_data) == 0) {
                 self::logdeb("Not writing session '$id'; unchanged");
                 return true;
@@ -95,7 +109,16 @@ class Session extends Memcached_DataObject
 
                 $session->session_data = $session_data;
 
-                return $session->update($orig);
+                $result = $session->update($orig);
+
+                if (!$result) {
+                    common_log_db_error($session, 'UPDATE', __FILE__);
+                    self::logdeb("Failed to update '$id'.");
+                } else {
+                    self::logdeb("Successfully updated '$id' (result = $result).");
+                }
+
+                return $result;
             }
         }
     }
@@ -106,8 +129,17 @@ class Session extends Memcached_DataObject
 
         $session = Session::staticGet('id', $id);
 
-        if (!empty($session)) {
-            return $session->delete();
+        if (empty($session)) {
+            self::logdeb("Can't find '$id' to delete.");
+        } else {
+            $result = $session->delete();
+            if (!$result) {
+                common_log_db_error($session, 'DELETE', __FILE__);
+                self::logdeb("Failed to delete '$id'.");
+            } else {
+                self::logdeb("Successfully deleted '$id' (result = $result).");
+            }
+            return $result;
         }
     }
 
@@ -132,7 +164,10 @@ class Session extends Memcached_DataObject
 
         $session->free();
 
+        self::logdeb("Found " . count($ids) . " ids to delete.");
+
         foreach ($ids as $id) {
+            self::logdeb("Destroying session '$id'.");
             self::destroy($id);
         }
     }
index 6ea975202d2c732427a930a54451e647263605f3..022044aac117ba366d374937ff164964a2bd9de5 100644 (file)
@@ -209,8 +209,6 @@ class User extends Memcached_DataObject
 
         $profile = new Profile();
 
-        $profile->query('BEGIN');
-
         if(!empty($email))
         {
             $email = common_canonical_email($email);
@@ -220,7 +218,7 @@ class User extends Memcached_DataObject
         $profile->nickname = $nickname;
         if(! User::allowed_nickname($nickname)){
             common_log(LOG_WARNING, sprintf("Attempted to register a nickname that is not allowed: %s", $profile->nickname),
-                           __FILE__);
+                       __FILE__);
         }
         $profile->profileurl = common_profile_url($nickname);
 
@@ -248,16 +246,8 @@ class User extends Memcached_DataObject
 
         $profile->created = common_sql_now();
 
-        $id = $profile->insert();
-
-        if (empty($id)) {
-            common_log_db_error($profile, 'INSERT', __FILE__);
-            return false;
-        }
-
         $user = new User();
 
-        $user->id = $id;
         $user->nickname = $nickname;
 
         if (!empty($password)) { // may not have a password for OpenID users
@@ -282,109 +272,126 @@ class User extends Memcached_DataObject
         $user->inboxed = 1;
 
         $user->created = common_sql_now();
-        $user->uri = common_user_uri($user);
 
-        $result = $user->insert();
+        if (Event::handle('StartUserRegister', array(&$user, &$profile))) {
 
-        if (!$result) {
-            common_log_db_error($user, 'INSERT', __FILE__);
-            return false;
-        }
+            $profile->query('BEGIN');
 
-        // Everyone gets an inbox
+            $id = $profile->insert();
 
-        $inbox = new Inbox();
-
-        $inbox->user_id = $user->id;
-        $inbox->notice_ids = '';
-
-        $result = $inbox->insert();
+            if (empty($id)) {
+                common_log_db_error($profile, 'INSERT', __FILE__);
+                return false;
+            }
 
-        if (!$result) {
-            common_log_db_error($inbox, 'INSERT', __FILE__);
-            return false;
-        }
+            $user->id = $id;
+            $user->uri = common_user_uri($user);
 
-        // Everyone is subscribed to themself
+            $result = $user->insert();
 
-        $subscription = new Subscription();
-        $subscription->subscriber = $user->id;
-        $subscription->subscribed = $user->id;
-        $subscription->created = $user->created;
+            if (!$result) {
+                common_log_db_error($user, 'INSERT', __FILE__);
+                return false;
+            }
 
-        $result = $subscription->insert();
+            // Everyone gets an inbox
 
-        if (!$result) {
-            common_log_db_error($subscription, 'INSERT', __FILE__);
-            return false;
-        }
+            $inbox = new Inbox();
 
-        if (!empty($email) && !$user->email) {
+            $inbox->user_id = $user->id;
+            $inbox->notice_ids = '';
 
-            $confirm = new Confirm_address();
-            $confirm->code = common_confirmation_code(128);
-            $confirm->user_id = $user->id;
-            $confirm->address = $email;
-            $confirm->address_type = 'email';
+            $result = $inbox->insert();
 
-            $result = $confirm->insert();
             if (!$result) {
-                common_log_db_error($confirm, 'INSERT', __FILE__);
+                common_log_db_error($inbox, 'INSERT', __FILE__);
                 return false;
             }
-        }
 
-        if (!empty($code) && $user->email) {
-            $user->emailChanged();
-        }
+            // Everyone is subscribed to themself
 
-        // Default system subscription
+            $subscription = new Subscription();
+            $subscription->subscriber = $user->id;
+            $subscription->subscribed = $user->id;
+            $subscription->created = $user->created;
 
-        $defnick = common_config('newuser', 'default');
+            $result = $subscription->insert();
 
-        if (!empty($defnick)) {
-            $defuser = User::staticGet('nickname', $defnick);
-            if (empty($defuser)) {
-                common_log(LOG_WARNING, sprintf("Default user %s does not exist.", $defnick),
-                           __FILE__);
-            } else {
-                $defsub = new Subscription();
-                $defsub->subscriber = $user->id;
-                $defsub->subscribed = $defuser->id;
-                $defsub->created = $user->created;
+            if (!$result) {
+                common_log_db_error($subscription, 'INSERT', __FILE__);
+                return false;
+            }
 
-                $result = $defsub->insert();
+            if (!empty($email) && !$user->email) {
+
+                $confirm = new Confirm_address();
+                $confirm->code = common_confirmation_code(128);
+                $confirm->user_id = $user->id;
+                $confirm->address = $email;
+                $confirm->address_type = 'email';
+
+                $result = $confirm->insert();
 
                 if (!$result) {
-                    common_log_db_error($defsub, 'INSERT', __FILE__);
+                    common_log_db_error($confirm, 'INSERT', __FILE__);
                     return false;
                 }
             }
-        }
 
-        $profile->query('COMMIT');
+            if (!empty($code) && $user->email) {
+                $user->emailChanged();
+            }
 
-        if (!empty($email) && !$user->email) {
-            mail_confirm_address($user, $confirm->code, $profile->nickname, $email);
-        }
+            // Default system subscription
 
-        // Welcome message
+            $defnick = common_config('newuser', 'default');
 
-        $welcome = common_config('newuser', 'welcome');
+            if (!empty($defnick)) {
+                $defuser = User::staticGet('nickname', $defnick);
+                if (empty($defuser)) {
+                    common_log(LOG_WARNING, sprintf("Default user %s does not exist.", $defnick),
+                               __FILE__);
+                } else {
+                    $defsub = new Subscription();
+                    $defsub->subscriber = $user->id;
+                    $defsub->subscribed = $defuser->id;
+                    $defsub->created = $user->created;
 
-        if (!empty($welcome)) {
-            $welcomeuser = User::staticGet('nickname', $welcome);
-            if (empty($welcomeuser)) {
-                common_log(LOG_WARNING, sprintf("Welcome user %s does not exist.", $defnick),
-                           __FILE__);
-            } else {
-                $notice = Notice::saveNew($welcomeuser->id,
-                                          sprintf(_('Welcome to %1$s, @%2$s!'),
-                                                  common_config('site', 'name'),
-                                                  $user->nickname),
-                                          'system');
+                    $result = $defsub->insert();
+
+                    if (!$result) {
+                        common_log_db_error($defsub, 'INSERT', __FILE__);
+                        return false;
+                    }
+                }
+            }
+
+            $profile->query('COMMIT');
 
+            if (!empty($email) && !$user->email) {
+                mail_confirm_address($user, $confirm->code, $profile->nickname, $email);
+            }
+
+            // Welcome message
+
+            $welcome = common_config('newuser', 'welcome');
+
+            if (!empty($welcome)) {
+                $welcomeuser = User::staticGet('nickname', $welcome);
+                if (empty($welcomeuser)) {
+                    common_log(LOG_WARNING, sprintf("Welcome user %s does not exist.", $defnick),
+                               __FILE__);
+                } else {
+                    $notice = Notice::saveNew($welcomeuser->id,
+                                              sprintf(_('Welcome to %1$s, @%2$s!'),
+                                                      common_config('site', 'name'),
+                                                      $user->nickname),
+                                              'system');
+
+                }
             }
+
+            Event::handle('EndUserRegister', array(&$profile, &$user));
         }
 
         return $user;
@@ -925,4 +932,30 @@ class User extends Memcached_DataObject
             return $share;
         }
     }
+
+    static function siteOwner()
+    {
+        $owner = self::cacheGet('user:site_owner');
+
+        if ($owner === false) { // cache miss
+
+            $pr = new Profile_role();
+
+            $pr->role = Profile_role::OWNER;
+
+            $pr->orderBy('created');
+
+            $pr->limit(0, 1);
+
+            if ($pr->fetch($true)) {
+                $owner = User::staticGet('id', $pr->profile_id);
+            } else {
+                $owner = null;
+            }
+
+            self::cacheSet('user:site_owner', $owner);
+        }
+
+        return $owner;
+    }
 }
index 8123265e46c879fd6ff043a3c498d40aca82aef9..adb71cba77ad1c06d850614583ddff2dcd726f47 100644 (file)
@@ -11,6 +11,7 @@ theme = 2
 logo = 2
 created = 142
 modified = 384
+tags = 34
 
 [status_network__keys]
 nickname = K
index 8342c4bc6fa3c3bc973fad72251001f09de02f35..917c1f1c411ec1300824e8a5bac69eb6e06a2967 100644 (file)
@@ -15,7 +15,9 @@ alter table queue_item rename to queue_item_old;
 alter table queue_item_new rename to queue_item;
 
 alter table consumer
-    add consumer_secret varchar(255) not null comment 'secret value',
+    add consumer_secret varchar(255) not null comment 'secret value';
+
+alter table token
     add verifier varchar(255) comment 'verifier string for OAuth 1.0a',
     add verified_callback varchar(255) comment 'verified callback URL for OAuth 1.0a';
 
index b5edc0f947b4e2c0ddb9d1e92a282e64db8db00d..06ff9900fd5bff24466be4cbd6ee5ffaeb87ac6e 100644 (file)
--- a/index.php
+++ b/index.php
@@ -146,12 +146,27 @@ function formatBacktraceLine($n, $line)
     return $out;
 }
 
-function checkMirror($action_obj, $args)
+function setupRW()
 {
     global $config;
 
     static $alwaysRW = array('session', 'remember_me');
 
+    // We ensure that these tables always are used
+    // on the master DB
+
+    $config['db']['database_rw'] = $config['db']['database'];
+    $config['db']['ini_rw'] = INSTALLDIR.'/classes/statusnet.ini';
+
+    foreach ($alwaysRW as $table) {
+        $config['db']['table_'.$table] = 'rw';
+    }
+}
+
+function checkMirror($action_obj, $args)
+{
+    global $config;
+
     if (common_config('db', 'mirror') && $action_obj->isReadOnly($args)) {
         if (is_array(common_config('db', 'mirror'))) {
             // "load balancing", ha ha
@@ -162,16 +177,6 @@ function checkMirror($action_obj, $args)
             $mirror = common_config('db', 'mirror');
         }
 
-        // We ensure that these tables always are used
-        // on the master DB
-
-        $config['db']['database_rw'] = $config['db']['database'];
-        $config['db']['ini_rw'] = INSTALLDIR.'/classes/statusnet.ini';
-
-        foreach ($alwaysRW as $table) {
-            $config['db']['table_'.$table] = 'rw';
-        }
-
         // everyone else uses the mirror
 
         $config['db']['database'] = $mirror;
@@ -237,9 +242,13 @@ function main()
 
     PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError');
 
+    // Make sure RW database is setup
+
+    setupRW();
+
     // XXX: we need a little more structure in this script
 
-    // get and cache current user
+    // get and cache current user (may hit RW!)
 
     $user = common_current_user();
 
@@ -276,8 +285,9 @@ function main()
     if (!$user && common_config('site', 'private')
         && !isLoginAction($action)
         && !preg_match('/rss$/', $action)
-        && !preg_match('/^Api/', $action)
-    ) {
+        && $action != 'robotstxt'
+        && !preg_match('/^Api/', $action)) {
+
         // set returnto
         $rargs =& common_copy_args($args);
         unset($rargs['action']);
index 825262b4c18ea1bb8b99d846edc63b8428e2f313..987f2cc1bfe99c35292d1cd50e51f6a0477089ff 100644 (file)
@@ -299,7 +299,7 @@ class ApiAction extends Action
             }
         }
 
-        if ($include_user) {
+        if ($include_user && $profile) {
             # Don't get notice (recursive!)
             $twitter_user = $this->twitterUserArray($profile, false);
             $twitter_status['user'] = $twitter_user;
index ada48b339d5020c34d60e274e49385ed4d5eda2e..b482464aacca346cf90218117e846449e2dd848f 100644 (file)
@@ -22,7 +22,7 @@ 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_VERSION', '0.9.0beta3');
+define('STATUSNET_VERSION', '0.9.0beta4');
 define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility
 
 define('STATUSNET_CODENAME', 'Stand');
@@ -115,6 +115,10 @@ function __autoload($cls)
 require_once 'Validate.php';
 require_once 'markdown.php';
 
+// XXX: other formats here
+
+define('NICKNAME_FMT', VALIDATE_NUM.VALIDATE_ALPHA_LOWER);
+
 require_once INSTALLDIR.'/lib/util.php';
 require_once INSTALLDIR.'/lib/action.php';
 require_once INSTALLDIR.'/lib/mail.php';
@@ -136,6 +140,3 @@ try {
     exit;
 }
 
-// XXX: other formats here
-
-define('NICKNAME_FMT', VALIDATE_NUM.VALIDATE_ALPHA_LOWER);
index 64fb7a786f9734de01d2067100b013c4aef6feeb..437f350ddae38a179dfc8f0bfb6dcc3de0dc6c1b 100644 (file)
@@ -84,9 +84,12 @@ $default =
               'control_channel' => '/topic/statusnet-control', // broadcasts to all queue daemons
               'stomp_username' => null,
               'stomp_password' => null,
+              'stomp_persistent' => true, // keep items across queue server restart, if persistence is enabled
+              'stomp_manual_failover' => true, // if multiple servers are listed, treat them as separate (enqueue on one randomly, listen on all)
               'monitor' => null, // URL to monitor ping endpoint (work in progress)
               'softlimit' => '90%', // total size or % of memory_limit at which to restart queue threads gracefully
               'debug_memory' => false, // true to spit memory usage to log
+              'inboxes' => true, // true to do inbox distribution & output queueing from in background via 'distrib' queue
               ),
         'license' =>
         array('type' => 'cc', # can be 'cc', 'allrightsreserved', 'private'
@@ -269,4 +272,8 @@ $default =
         'singleuser' =>
         array('enabled' => false,
               'nickname' => null),
+        'robotstxt' =>
+        array('crawldelay' => 0,
+              'disallow' => array('main', 'settings', 'admin', 'search', 'message')
+              ),
         );
index f458d238da97c77cdd6006ee821de09ab361c36b..4477468d0a38d523c26163b3afcc987805bf3617 100644 (file)
@@ -62,23 +62,60 @@ class DistribQueueHandler
     {
         // XXX: do we need to change this for remote users?
 
-        $notice->saveTags();
+        try {
+            $notice->saveTags();
+        } catch (Exception $e) {
+            $this->logit($notice, $e);
+        }
 
-        $groups = $notice->saveGroups();
+        try {
+            $groups = $notice->saveGroups();
+        } catch (Exception $e) {
+            $this->logit($notice, $e);
+        }
 
-        $recipients = $notice->saveReplies();
+        try {
+            $recipients = $notice->saveReplies();
+        } catch (Exception $e) {
+            $this->logit($notice, $e);
+        }
 
-        $notice->addToInboxes($groups, $recipients);
+        try {
+            $notice->addToInboxes($groups, $recipients);
+        } catch (Exception $e) {
+            $this->logit($notice, $e);
+        }
 
-        $notice->saveUrls();
+        try {
+            $notice->saveUrls();
+        } catch (Exception $e) {
+            $this->logit($notice, $e);
+        }
 
-        Event::handle('EndNoticeSave', array($notice));
+        try {
+            Event::handle('EndNoticeSave', array($notice));
+            // Enqueue for other handlers
+        } catch (Exception $e) {
+            $this->logit($notice, $e);
+        }
 
-        // Enqueue for other handlers
-
-        common_enqueue_notice($notice);
+        try {
+            common_enqueue_notice($notice);
+        } catch (Exception $e) {
+            $this->logit($notice, $e);
+        }
 
         return true;
     }
+    
+    protected function logit($notice, $e)
+    {
+        common_log(LOG_ERR, "Distrib queue exception saving notice $notice->id: " .
+            $e->getMessage() . ' ' .
+            str_replace("\n", " ", $e->getTraceAsString()));
+
+        // We'll still return true so we don't get stuck in a loop
+        // trying to run a bad insert over and over...
+    }
 }
 
index b6b23521bdf4e8a11088c94d4dfdc983e2849872..e1bf06ba661bfc8a8e29bdc30637c5f3d8b22032 100644 (file)
@@ -358,7 +358,7 @@ function jabber_broadcast_notice($notice)
         common_log(LOG_WARNING, 'Refusing to broadcast notice with ' .
                    'unknown profile ' . common_log_objstring($notice),
                    __FILE__);
-        return false;
+        return true; // not recoverable; discard.
     }
 
     $msg   = jabber_format_notice($profile, $notice);
@@ -437,7 +437,7 @@ function jabber_public_notice($notice)
             common_log(LOG_WARNING, 'Refusing to broadcast notice with ' .
                        'unknown profile ' . common_log_objstring($notice),
                        __FILE__);
-            return false;
+            return true; // not recoverable; discard.
         }
 
         $msg   = jabber_format_notice($profile, $notice);
index 83471f2df7d1789a9fc5c766e73e6cf092b730f9..d6b4b7416a6cdb4320d02d17464e1c8a32cb6420 100644 (file)
@@ -40,7 +40,7 @@ class JabberQueueHandler extends QueueHandler
         try {
             return jabber_broadcast_notice($notice);
         } catch (XMPPHP_Exception $e) {
-            $this->log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage());
+            common_log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage());
             return false;
         }
     }
index c9233843a4dfdb262cd7f1ea673967fb351020ff..3d38953fd2cac9b3770673a06876bd9b3de7c5bd 100644 (file)
@@ -33,6 +33,22 @@ class LiberalStomp extends Stomp
         return $this->_socket;
     }
 
+    /**
+     * Return the host we're currently connected to.
+     *
+     * @return string
+     */
+    function getServer()
+    {
+        $idx = $this->_currentHost;
+        if ($idx >= 0) {
+            $host = $this->_hosts[$idx];
+            return "$host[0]:$host[1]";
+        } else {
+            return '[unconnected]';
+        }
+    }
+
     /**
      * Make socket connection to the server
      * We also set the stream to non-blocking mode, since we'll be
@@ -71,10 +87,12 @@ class LiberalStomp extends Stomp
             // @fixme this sometimes hangs in blocking mode...
             // shouldn't we have been idle until we found there's more data?
             $read = fread($this->_socket, $rb);
-            if ($read === false) {
-                $this->_reconnect();
+            if ($read === false || ($read === '' && feof($this->_socket))) {
+                // @fixme possibly attempt an auto reconnect as old code?
+                throw new StompException("Error reading");
+                //$this->_reconnect();
                 // @fixme this will lose prior items
-                return $this->readFrames();
+                //return $this->readFrames();
             }
             $data .= $read;
             if (strpos($data, "\x00") !== false) {
diff --git a/lib/mysqlschema.php b/lib/mysqlschema.php
new file mode 100644 (file)
index 0000000..1f7c3d0
--- /dev/null
@@ -0,0 +1,537 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Database schema utilities
+ *
+ * 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  Database
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@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 representing the database schema
+ *
+ * A class representing the database schema. Can be used to
+ * manipulate the schema -- especially for plugins and upgrade
+ * utilities.
+ *
+ * @category Database
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@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 MysqlSchema extends Schema
+{
+    static $_single = null;
+    protected $conn = null;
+
+    /**
+     * Constructor. Only run once for singleton object.
+     */
+
+    protected function __construct()
+    {
+        // XXX: there should be an easier way to do this.
+        $user = new User();
+
+        $this->conn = $user->getDatabaseConnection();
+
+        $user->free();
+
+        unset($user);
+    }
+
+    /**
+     * Main public entry point. Use this to get
+     * the singleton object.
+     *
+     * @return Schema the (single) Schema object
+     */
+
+    static function get()
+    {
+        if (empty(self::$_single)) {
+            self::$_single = new Schema();
+        }
+        return self::$_single;
+    }
+
+    /**
+     * Returns a TableDef object for the table
+     * in the schema with the given name.
+     *
+     * Throws an exception if the table is not found.
+     *
+     * @param string $name Name of the table to get
+     *
+     * @return TableDef tabledef for that table.
+     */
+
+    public function getTableDef($name)
+    {
+        $res = $this->conn->query('DESCRIBE ' . $name);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        $td = new TableDef();
+
+        $td->name    = $name;
+        $td->columns = array();
+
+        $row = array();
+
+        while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
+
+            $cd = new ColumnDef();
+
+            $cd->name = $row['Field'];
+
+            $packed = $row['Type'];
+
+            if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) {
+                $cd->type = $match[1];
+                $cd->size = $match[2];
+            } else {
+                $cd->type = $packed;
+            }
+
+            $cd->nullable = ($row['Null'] == 'YES') ? true : false;
+            $cd->key      = $row['Key'];
+            $cd->default  = $row['Default'];
+            $cd->extra    = $row['Extra'];
+
+            $td->columns[] = $cd;
+        }
+
+        return $td;
+    }
+
+    /**
+     * Gets a ColumnDef object for a single column.
+     *
+     * Throws an exception if the table is not found.
+     *
+     * @param string $table  name of the table
+     * @param string $column name of the column
+     *
+     * @return ColumnDef definition of the column or null
+     *                   if not found.
+     */
+
+    public function getColumnDef($table, $column)
+    {
+        $td = $this->getTableDef($table);
+
+        foreach ($td->columns as $cd) {
+            if ($cd->name == $column) {
+                return $cd;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Creates a table with the given names and columns.
+     *
+     * @param string $name    Name of the table
+     * @param array  $columns Array of ColumnDef objects
+     *                        for new table.
+     *
+     * @return boolean success flag
+     */
+
+    public function createTable($name, $columns)
+    {
+        $uniques = array();
+        $primary = array();
+        $indices = array();
+
+        $sql = "CREATE TABLE $name (\n";
+
+        for ($i = 0; $i < count($columns); $i++) {
+
+            $cd =& $columns[$i];
+
+            if ($i > 0) {
+                $sql .= ",\n";
+            }
+
+            $sql .= $this->_columnSql($cd);
+
+            switch ($cd->key) {
+            case 'UNI':
+                $uniques[] = $cd->name;
+                break;
+            case 'PRI':
+                $primary[] = $cd->name;
+                break;
+            case 'MUL':
+                $indices[] = $cd->name;
+                break;
+            }
+        }
+
+        if (count($primary) > 0) { // it really should be...
+            $sql .= ",\nconstraint primary key (" . implode(',', $primary) . ")";
+        }
+
+        foreach ($uniques as $u) {
+            $sql .= ",\nunique index {$name}_{$u}_idx ($u)";
+        }
+
+        foreach ($indices as $i) {
+            $sql .= ",\nindex {$name}_{$i}_idx ($i)";
+        }
+
+        $sql .= "); ";
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Drops a table from the schema
+     *
+     * Throws an exception if the table is not found.
+     *
+     * @param string $name Name of the table to drop
+     *
+     * @return boolean success flag
+     */
+
+    public function dropTable($name)
+    {
+        $res = $this->conn->query("DROP TABLE $name");
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Adds an index to a table.
+     *
+     * If no name is provided, a name will be made up based
+     * on the table name and column names.
+     *
+     * Throws an exception on database error, esp. if the table
+     * does not exist.
+     *
+     * @param string $table       Name of the table
+     * @param array  $columnNames Name of columns to index
+     * @param string $name        (Optional) name of the index
+     *
+     * @return boolean success flag
+     */
+
+    public function createIndex($table, $columnNames, $name=null)
+    {
+        if (!is_array($columnNames)) {
+            $columnNames = array($columnNames);
+        }
+
+        if (empty($name)) {
+            $name = "$table_".implode("_", $columnNames)."_idx";
+        }
+
+        $res = $this->conn->query("ALTER TABLE $table ".
+                                   "ADD INDEX $name (".
+                                   implode(",", $columnNames).")");
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Drops a named index from a table.
+     *
+     * @param string $table name of the table the index is on.
+     * @param string $name  name of the index
+     *
+     * @return boolean success flag
+     */
+
+    public function dropIndex($table, $name)
+    {
+        $res = $this->conn->query("ALTER TABLE $table DROP INDEX $name");
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Adds a column to a table
+     *
+     * @param string    $table     name of the table
+     * @param ColumnDef $columndef Definition of the new
+     *                             column.
+     *
+     * @return boolean success flag
+     */
+
+    public function addColumn($table, $columndef)
+    {
+        $sql = "ALTER TABLE $table ADD COLUMN " . $this->_columnSql($columndef);
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Modifies a column in the schema.
+     *
+     * The name must match an existing column and table.
+     *
+     * @param string    $table     name of the table
+     * @param ColumnDef $columndef new definition of the column.
+     *
+     * @return boolean success flag
+     */
+
+    public function modifyColumn($table, $columndef)
+    {
+        $sql = "ALTER TABLE $table MODIFY COLUMN " .
+          $this->_columnSql($columndef);
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Drops a column from a table
+     *
+     * The name must match an existing column.
+     *
+     * @param string $table      name of the table
+     * @param string $columnName name of the column to drop
+     *
+     * @return boolean success flag
+     */
+
+    public function dropColumn($table, $columnName)
+    {
+        $sql = "ALTER TABLE $table DROP COLUMN $columnName";
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Ensures that a table exists with the given
+     * name and the given column definitions.
+     *
+     * If the table does not yet exist, it will
+     * create the table. If it does exist, it will
+     * alter the table to match the column definitions.
+     *
+     * @param string $tableName name of the table
+     * @param array  $columns   array of ColumnDef
+     *                          objects for the table
+     *
+     * @return boolean success flag
+     */
+
+    public function ensureTable($tableName, $columns)
+    {
+        // XXX: DB engine portability -> toilet
+
+        try {
+            $td = $this->getTableDef($tableName);
+        } catch (Exception $e) {
+            if (preg_match('/no such table/', $e->getMessage())) {
+                return $this->createTable($tableName, $columns);
+            } else {
+                throw $e;
+            }
+        }
+
+        $cur = $this->_names($td->columns);
+        $new = $this->_names($columns);
+
+        $toadd  = array_diff($new, $cur);
+        $todrop = array_diff($cur, $new);
+        $same   = array_intersect($new, $cur);
+        $tomod  = array();
+
+        foreach ($same as $m) {
+            $curCol = $this->_byName($td->columns, $m);
+            $newCol = $this->_byName($columns, $m);
+
+            if (!$newCol->equals($curCol)) {
+                $tomod[] = $newCol->name;
+            }
+        }
+
+        if (count($toadd) + count($todrop) + count($tomod) == 0) {
+            // nothing to do
+            return true;
+        }
+
+        // For efficiency, we want this all in one
+        // query, instead of using our methods.
+
+        $phrase = array();
+
+        foreach ($toadd as $columnName) {
+            $cd = $this->_byName($columns, $columnName);
+
+            $phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd);
+        }
+
+        foreach ($todrop as $columnName) {
+            $phrase[] = 'DROP COLUMN ' . $columnName;
+        }
+
+        foreach ($tomod as $columnName) {
+            $cd = $this->_byName($columns, $columnName);
+
+            $phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd);
+        }
+
+        $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase);
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the array of names from an array of
+     * ColumnDef objects.
+     *
+     * @param array $cds array of ColumnDef objects
+     *
+     * @return array strings for name values
+     */
+
+    private function _names($cds)
+    {
+        $names = array();
+
+        foreach ($cds as $cd) {
+            $names[] = $cd->name;
+        }
+
+        return $names;
+    }
+
+    /**
+     * Get a ColumnDef from an array matching
+     * name.
+     *
+     * @param array  $cds  Array of ColumnDef objects
+     * @param string $name Name of the column
+     *
+     * @return ColumnDef matching item or null if no match.
+     */
+
+    private function _byName($cds, $name)
+    {
+        foreach ($cds as $cd) {
+            if ($cd->name == $name) {
+                return $cd;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Return the proper SQL for creating or
+     * altering a column.
+     *
+     * Appropriate for use in CREATE TABLE or
+     * ALTER TABLE statements.
+     *
+     * @param ColumnDef $cd column to create
+     *
+     * @return string correct SQL for that column
+     */
+
+    private function _columnSql($cd)
+    {
+        $sql = "{$cd->name} ";
+
+        if (!empty($cd->size)) {
+            $sql .= "{$cd->type}({$cd->size}) ";
+        } else {
+            $sql .= "{$cd->type} ";
+        }
+
+        if (!empty($cd->default)) {
+            $sql .= "default {$cd->default} ";
+        } else {
+            $sql .= ($cd->nullable) ? "null " : "not null ";
+        }
+        
+        if (!empty($cd->auto_increment)) {
+            $sql .= " auto_increment ";
+        }
+
+        if (!empty($cd->extra)) {
+            $sql .= "{$cd->extra} ";
+        }
+
+        return $sql;
+    }
+}
index 24896c784c78ed1105bd006afd418fd9e9e99718..1921c2bacdee0d0e06afec9898fde8aaf3cceb93 100644 (file)
@@ -39,7 +39,7 @@ class OmbQueueHandler extends QueueHandler
     function handle($notice)
     {
         if ($this->is_remote($notice)) {
-            $this->log(LOG_DEBUG, 'Ignoring remote notice ' . $notice->id);
+            common_log(LOG_DEBUG, 'Ignoring remote notice ' . $notice->id);
             return true;
         } else {
             require_once(INSTALLDIR.'/lib/omb.php');
diff --git a/lib/pgsqlschema.php b/lib/pgsqlschema.php
new file mode 100644 (file)
index 0000000..91bc096
--- /dev/null
@@ -0,0 +1,503 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Database schema utilities
+ *
+ * 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  Database
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@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 representing the database schema
+ *
+ * A class representing the database schema. Can be used to
+ * manipulate the schema -- especially for plugins and upgrade
+ * utilities.
+ *
+ * @category Database
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@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 PgsqlSchema extends Schema
+{
+
+    /**
+     * Returns a TableDef object for the table
+     * in the schema with the given name.
+     *
+     * Throws an exception if the table is not found.
+     *
+     * @param string $name Name of the table to get
+     *
+     * @return TableDef tabledef for that table.
+     */
+
+    public function getTableDef($name)
+    {
+        $res = $this->conn->query("select *, column_default as default, is_nullable as Null, udt_name as Type, column_name AS Field from INFORMATION_SCHEMA.COLUMNS where table_name = '$name'");
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        $td = new TableDef();
+
+        $td->name    = $name;
+        $td->columns = array();
+
+        $row = array();
+
+        while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
+//             var_dump($row);
+            $cd = new ColumnDef();
+
+            $cd->name = $row['field'];
+
+            $packed = $row['type'];
+
+            if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) {
+                $cd->type = $match[1];
+                $cd->size = $match[2];
+            } else {
+                $cd->type = $packed;
+            }
+
+            $cd->nullable = ($row['null'] == 'YES') ? true : false;
+            $cd->key      = $row['Key'];
+            $cd->default  = $row['default'];
+            $cd->extra    = $row['Extra'];
+
+            $td->columns[] = $cd;
+        }
+        return $td;
+    }
+
+    /**
+     * Gets a ColumnDef object for a single column.
+     *
+     * Throws an exception if the table is not found.
+     *
+     * @param string $table  name of the table
+     * @param string $column name of the column
+     *
+     * @return ColumnDef definition of the column or null
+     *                   if not found.
+     */
+
+    public function getColumnDef($table, $column)
+    {
+        $td = $this->getTableDef($table);
+
+        foreach ($td->columns as $cd) {
+            if ($cd->name == $column) {
+                return $cd;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Creates a table with the given names and columns.
+     *
+     * @param string $name    Name of the table
+     * @param array  $columns Array of ColumnDef objects
+     *                        for new table.
+     *
+     * @return boolean success flag
+     */
+
+    public function createTable($name, $columns)
+    {
+        $uniques = array();
+        $primary = array();
+        $indices = array();
+
+        $sql = "CREATE TABLE $name (\n";
+
+        for ($i = 0; $i < count($columns); $i++) {
+
+            $cd =& $columns[$i];
+
+            if ($i > 0) {
+                $sql .= ",\n";
+            }
+
+            $sql .= $this->_columnSql($cd);
+
+            switch ($cd->key) {
+            case 'UNI':
+                $uniques[] = $cd->name;
+                break;
+            case 'PRI':
+                $primary[] = $cd->name;
+                break;
+            case 'MUL':
+                $indices[] = $cd->name;
+                break;
+            }
+        }
+
+        if (count($primary) > 0) { // it really should be...
+            $sql .= ",\nconstraint primary key (" . implode(',', $primary) . ")";
+        }
+
+        foreach ($uniques as $u) {
+            $sql .= ",\nunique index {$name}_{$u}_idx ($u)";
+        }
+
+        foreach ($indices as $i) {
+            $sql .= ",\nindex {$name}_{$i}_idx ($i)";
+        }
+
+        $sql .= "); ";
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Drops a table from the schema
+     *
+     * Throws an exception if the table is not found.
+     *
+     * @param string $name Name of the table to drop
+     *
+     * @return boolean success flag
+     */
+
+    public function dropTable($name)
+    {
+        $res = $this->conn->query("DROP TABLE $name");
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Adds an index to a table.
+     *
+     * If no name is provided, a name will be made up based
+     * on the table name and column names.
+     *
+     * Throws an exception on database error, esp. if the table
+     * does not exist.
+     *
+     * @param string $table       Name of the table
+     * @param array  $columnNames Name of columns to index
+     * @param string $name        (Optional) name of the index
+     *
+     * @return boolean success flag
+     */
+
+    public function createIndex($table, $columnNames, $name=null)
+    {
+        if (!is_array($columnNames)) {
+            $columnNames = array($columnNames);
+        }
+
+        if (empty($name)) {
+            $name = "$table_".implode("_", $columnNames)."_idx";
+        }
+
+        $res = $this->conn->query("ALTER TABLE $table ".
+                                   "ADD INDEX $name (".
+                                   implode(",", $columnNames).")");
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Drops a named index from a table.
+     *
+     * @param string $table name of the table the index is on.
+     * @param string $name  name of the index
+     *
+     * @return boolean success flag
+     */
+
+    public function dropIndex($table, $name)
+    {
+        $res = $this->conn->query("ALTER TABLE $table DROP INDEX $name");
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Adds a column to a table
+     *
+     * @param string    $table     name of the table
+     * @param ColumnDef $columndef Definition of the new
+     *                             column.
+     *
+     * @return boolean success flag
+     */
+
+    public function addColumn($table, $columndef)
+    {
+        $sql = "ALTER TABLE $table ADD COLUMN " . $this->_columnSql($columndef);
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Modifies a column in the schema.
+     *
+     * The name must match an existing column and table.
+     *
+     * @param string    $table     name of the table
+     * @param ColumnDef $columndef new definition of the column.
+     *
+     * @return boolean success flag
+     */
+
+    public function modifyColumn($table, $columndef)
+    {
+        $sql = "ALTER TABLE $table MODIFY COLUMN " .
+          $this->_columnSql($columndef);
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Drops a column from a table
+     *
+     * The name must match an existing column.
+     *
+     * @param string $table      name of the table
+     * @param string $columnName name of the column to drop
+     *
+     * @return boolean success flag
+     */
+
+    public function dropColumn($table, $columnName)
+    {
+        $sql = "ALTER TABLE $table DROP COLUMN $columnName";
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Ensures that a table exists with the given
+     * name and the given column definitions.
+     *
+     * If the table does not yet exist, it will
+     * create the table. If it does exist, it will
+     * alter the table to match the column definitions.
+     *
+     * @param string $tableName name of the table
+     * @param array  $columns   array of ColumnDef
+     *                          objects for the table
+     *
+     * @return boolean success flag
+     */
+
+    public function ensureTable($tableName, $columns)
+    {
+        // XXX: DB engine portability -> toilet
+
+        try {
+            $td = $this->getTableDef($tableName);
+        } catch (Exception $e) {
+            if (preg_match('/no such table/', $e->getMessage())) {
+                return $this->createTable($tableName, $columns);
+            } else {
+                throw $e;
+            }
+        }
+
+        $cur = $this->_names($td->columns);
+        $new = $this->_names($columns);
+
+        $toadd  = array_diff($new, $cur);
+        $todrop = array_diff($cur, $new);
+        $same   = array_intersect($new, $cur);
+        $tomod  = array();
+
+        foreach ($same as $m) {
+            $curCol = $this->_byName($td->columns, $m);
+            $newCol = $this->_byName($columns, $m);
+
+            if (!$newCol->equals($curCol)) {
+                $tomod[] = $newCol->name;
+            }
+        }
+
+        if (count($toadd) + count($todrop) + count($tomod) == 0) {
+            // nothing to do
+            return true;
+        }
+
+        // For efficiency, we want this all in one
+        // query, instead of using our methods.
+
+        $phrase = array();
+
+        foreach ($toadd as $columnName) {
+            $cd = $this->_byName($columns, $columnName);
+
+            $phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd);
+        }
+
+        foreach ($todrop as $columnName) {
+            $phrase[] = 'DROP COLUMN ' . $columnName;
+        }
+
+        foreach ($tomod as $columnName) {
+            $cd = $this->_byName($columns, $columnName);
+
+            $phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd);
+        }
+
+        $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase);
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the array of names from an array of
+     * ColumnDef objects.
+     *
+     * @param array $cds array of ColumnDef objects
+     *
+     * @return array strings for name values
+     */
+
+    private function _names($cds)
+    {
+        $names = array();
+
+        foreach ($cds as $cd) {
+            $names[] = $cd->name;
+        }
+
+        return $names;
+    }
+
+    /**
+     * Get a ColumnDef from an array matching
+     * name.
+     *
+     * @param array  $cds  Array of ColumnDef objects
+     * @param string $name Name of the column
+     *
+     * @return ColumnDef matching item or null if no match.
+     */
+
+    private function _byName($cds, $name)
+    {
+        foreach ($cds as $cd) {
+            if ($cd->name == $name) {
+                return $cd;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Return the proper SQL for creating or
+     * altering a column.
+     *
+     * Appropriate for use in CREATE TABLE or
+     * ALTER TABLE statements.
+     *
+     * @param ColumnDef $cd column to create
+     *
+     * @return string correct SQL for that column
+     */
+
+    private function _columnSql($cd)
+    {
+        $sql = "{$cd->name} ";
+
+        if (!empty($cd->size)) {
+            $sql .= "{$cd->type}({$cd->size}) ";
+        } else {
+            $sql .= "{$cd->type} ";
+        }
+
+        if (!empty($cd->default)) {
+            $sql .= "default {$cd->default} ";
+        } else {
+            $sql .= ($cd->nullable) ? "null " : "not null ";
+        }
+        
+        if (!empty($cd->auto_increment)) {
+            $sql .= " auto_increment ";
+        }
+
+        if (!empty($cd->extra)) {
+            $sql .= "{$cd->extra} ";
+        }
+
+        return $sql;
+    }
+}
index c9edb8d5d79b184bbcf1d2fa045f8c82219e4f5c..a497d13850337d88bf81c6ccb5db6a0bebcf6f4e 100644 (file)
@@ -38,7 +38,7 @@ class PublicQueueHandler extends QueueHandler
         try {
             return jabber_public_notice($notice);
         } catch (XMPPHP_Exception $e) {
-            $this->log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage());
+            common_log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage());
             return false;
         }
     }
index be9cfac0c29a38268f90ccd88a8ea67933487212..b046b240c951147de3556df514d554add0bd9fcb 100644 (file)
@@ -73,6 +73,8 @@ class Router
 
         if (Event::handle('StartInitializeRouter', array(&$m))) {
 
+            $m->connect('robots.txt', array('action' => 'robotstxt'));
+
             $m->connect('opensearch/people', array('action' => 'opensearch',
                                                    'type' => 'people'));
             $m->connect('opensearch/notice', array('action' => 'opensearch',
@@ -649,7 +651,16 @@ class Router
 
             if (common_config('singleuser', 'enabled')) {
 
-                $nickname = common_config('singleuser', 'nickname');
+                $user = User::siteOwner();
+
+                if (!empty($user)) {
+                    $nickname = $user->nickname;
+                } else {
+                    $nickname = common_config('singleuser', 'nickname');
+                    if (empty($nickname)) {
+                        throw new ServerException(_("No single user defined for single-user mode."));
+                    }
+                }
 
                 foreach (array('subscriptions', 'subscribers',
                                'all', 'foaf', 'xrds',
@@ -697,6 +708,10 @@ class Router
                                   'nickname' => $nickname),
                             array('tag' => '[a-zA-Z0-9]+'));
 
+                $m->connect('rsd.xml',
+                            array('action' => 'rsd',
+                                  'nickname' => $nickname));
+
                 $m->connect('',
                             array('action' => 'showstream',
                                   'nickname' => $nickname));
@@ -711,6 +726,7 @@ class Router
                 $m->connect('featured', array('action' => 'featured'));
                 $m->connect('favorited/', array('action' => 'favorited'));
                 $m->connect('favorited', array('action' => 'favorited'));
+                $m->connect('rsd.xml', array('action' => 'rsd'));
 
                 foreach (array('subscriptions', 'subscribers',
                                'nudge', 'all', 'foaf', 'xrds',
@@ -758,6 +774,10 @@ class Router
                             array('nickname' => '[a-zA-Z0-9]{1,64}'),
                             array('tag' => '[a-zA-Z0-9]+'));
 
+                $m->connect(':nickname/rsd.xml',
+                            array('action' => 'rsd'),
+                            array('nickname' => '[a-zA-Z0-9]{1,64}'));
+
                 $m->connect(':nickname',
                             array('action' => 'showstream'),
                             array('nickname' => '[a-zA-Z0-9]{1,64}'));
index a7f64ebed10cfce99fb780756a6fffa78831df83..137b814e0269ed727978e11e55ce25cca763a199 100644 (file)
@@ -75,64 +75,14 @@ class Schema
 
     static function get()
     {
+        $type = common_config('db', 'type');
         if (empty(self::$_single)) {
-            self::$_single = new Schema();
+            $schemaClass = ucfirst($type).'Schema';
+            self::$_single = new $schemaClass();
         }
         return self::$_single;
     }
 
-    /**
-     * Returns a TableDef object for the table
-     * in the schema with the given name.
-     *
-     * Throws an exception if the table is not found.
-     *
-     * @param string $name Name of the table to get
-     *
-     * @return TableDef tabledef for that table.
-     */
-
-    public function getTableDef($name)
-    {
-        $res = $this->conn->query('DESCRIBE ' . $name);
-
-        if (PEAR::isError($res)) {
-            throw new Exception($res->getMessage());
-        }
-
-        $td = new TableDef();
-
-        $td->name    = $name;
-        $td->columns = array();
-
-        $row = array();
-
-        while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
-
-            $cd = new ColumnDef();
-
-            $cd->name = $row['Field'];
-
-            $packed = $row['Type'];
-
-            if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) {
-                $cd->type = $match[1];
-                $cd->size = $match[2];
-            } else {
-                $cd->type = $packed;
-            }
-
-            $cd->nullable = ($row['Null'] == 'YES') ? true : false;
-            $cd->key      = $row['Key'];
-            $cd->default  = $row['Default'];
-            $cd->extra    = $row['Extra'];
-
-            $td->columns[] = $cd;
-        }
-
-        return $td;
-    }
-
     /**
      * Gets a ColumnDef object for a single column.
      *
@@ -523,7 +473,7 @@ class Schema
         } else {
             $sql .= ($cd->nullable) ? "null " : "not null ";
         }
-        
+
         if (!empty($cd->auto_increment)) {
             $sql .= " auto_increment ";
         }
index 19e8c49b5ce9c9e7a996ef3f8d948a9360b8f952..6730cd213d789dcd1e4de8ed273a27f8653a4ebf 100644 (file)
  */
 
 require_once 'Stomp.php';
-
+require_once 'Stomp/Exception.php';
 
 class StompQueueManager extends QueueManager
 {
-    var $server = null;
-    var $username = null;
-    var $password = null;
-    var $base = null;
-    var $con = null;
+    protected $servers;
+    protected $username;
+    protected $password;
+    protected $base;
     protected $control;
-    
+
+    protected $useTransactions = true;
+
     protected $sites = array();
     protected $subscriptions = array();
 
-    protected $useTransactions = true;
-    protected $transaction = null;
-    protected $transactionCount = 0;
+    protected $cons = array(); // all open connections
+    protected $disconnect = array();
+    protected $transaction = array();
+    protected $transactionCount = array();
+    protected $defaultIdx = 0;
 
     function __construct()
     {
         parent::__construct();
-        $this->server   = common_config('queue', 'stomp_server');
+        $server = common_config('queue', 'stomp_server');
+        if (is_array($server)) {
+            $this->servers = $server;
+        } else {
+            $this->servers = array($server);
+        }
         $this->username = common_config('queue', 'stomp_username');
         $this->password = common_config('queue', 'stomp_password');
         $this->base     = common_config('queue', 'queue_basename');
@@ -99,9 +107,9 @@ class StompQueueManager extends QueueManager
             $message .= ':' . $param;
         }
         $this->_connect();
-        $result = $this->con->send($this->control,
-                                   $message,
-                                   array ('created' => common_sql_now()));
+        $result = $this->_send($this->control,
+                               $message,
+                               array ('created' => common_sql_now()));
         if ($result) {
             $this->_log(LOG_INFO, "Sent control ping to queue daemons: $message");
             return true;
@@ -166,28 +174,59 @@ class StompQueueManager extends QueueManager
     /**
      * Saves a notice object reference into the queue item table.
      * @return boolean true on success
+     * @throws StompException on connection or send error
      */
     public function enqueue($object, $queue)
+    {
+        $this->_connect();
+        return $this->_doEnqueue($object, $queue, $this->defaultIdx);
+    }
+
+    /**
+     * Saves a notice object reference into the queue item table
+     * on the given connection.
+     *
+     * @return boolean true on success
+     * @throws StompException on connection or send error
+     */
+    protected function _doEnqueue($object, $queue, $idx)
     {
         $msg = $this->encode($object);
         $rep = $this->logrep($object);
 
-        $this->_connect();
-
-        // XXX: serialize and send entire notice
+        $props = array('created' => common_sql_now());
+        if ($this->isPersistent($queue)) {
+            $props['persistent'] = 'true';
+        }
 
-        $result = $this->con->send($this->queueName($queue),
-                                   $msg,               // BODY of the message
-                                   array ('created' => common_sql_now(),
-                                          'persistent' => 'true'));
+        $con = $this->cons[$idx];
+        $host = $con->getServer();
+        $result = $con->send($this->queueName($queue), $msg, $props);
 
         if (!$result) {
-            common_log(LOG_ERR, "Error sending $rep to $queue queue");
+            common_log(LOG_ERR, "Error sending $rep to $queue queue on $host");
             return false;
         }
 
-        common_log(LOG_DEBUG, "complete remote queueing $rep for $queue");
+        common_log(LOG_DEBUG, "complete remote queueing $rep for $queue on $host");
         $this->stats('enqueued', $queue);
+        return true;
+    }
+
+    /**
+     * Determine whether messages to this queue should be marked as persistent.
+     * Actual persistent storage depends on the queue server's configuration.
+     * @param string $queue
+     * @return bool
+     */
+    protected function isPersistent($queue)
+    {
+        $mode = common_config('queue', 'stomp_persistent');
+        if (is_array($mode)) {
+            return in_array($queue, $mode);
+        } else {
+            return (bool)$mode;
+        }
     }
 
     /**
@@ -198,7 +237,29 @@ class StompQueueManager extends QueueManager
      */
     public function getSockets()
     {
-        return array($this->con->getSocket());
+        $sockets = array();
+        foreach ($this->cons as $con) {
+            if ($con) {
+                $sockets[] = $con->getSocket();
+            }
+        }
+        return $sockets;
+    }
+
+    /**
+     * Get the Stomp connection object associated with the given socket.
+     * @param resource $socket
+     * @return int index into connections list
+     * @throws Exception
+     */
+    protected function connectionFromSocket($socket)
+    {
+        foreach ($this->cons as $i => $con) {
+            if ($con && $con->getSocket() === $socket) {
+                return $i;
+            }
+        }
+        throw new Exception(__CLASS__ . " asked to read from unrecognized socket");
     }
 
     /**
@@ -210,27 +271,56 @@ class StompQueueManager extends QueueManager
      */
     public function handleInput($socket)
     {
-        assert($socket === $this->con->getSocket());
+        $idx = $this->connectionFromSocket($socket);
+        $con = $this->cons[$idx];
+        $host = $con->getServer();
+
         $ok = true;
-        $frames = $this->con->readFrames();
+        try {
+            $frames = $con->readFrames();
+        } catch (StompException $e) {
+            common_log(LOG_ERR, "Lost connection to $host: " . $e->getMessage());
+            $this->cons[$idx] = null;
+            $this->transaction[$idx] = null;
+            $this->disconnect[$idx] = time();
+            return false;
+        }
         foreach ($frames as $frame) {
             $dest = $frame->headers['destination'];
             if ($dest == $this->control) {
-                if (!$this->handleControlSignal($frame)) {
+                if (!$this->handleControlSignal($idx, $frame)) {
                     // We got a control event that requests a shutdown;
                     // close out and stop handling anything else!
                     break;
                 }
             } else {
-                $ok = $ok && $this->handleItem($frame);
+                $ok = $ok && $this->handleItem($idx, $frame);
             }
         }
         return $ok;
     }
 
+    /**
+     * Attempt to reconnect in background if we lost a connection.
+     */
+    function idle()
+    {
+        $now = time();
+        foreach ($this->cons as $idx => $con) {
+            if (empty($con)) {
+                $age = $now - $this->disconnect[$idx];
+                if ($age >= 60) {
+                    $this->_reconnect($idx);
+                }
+            }
+        }
+        return true;
+    }
+
     /**
      * Initialize our connection and subscribe to all the queues
-     * we're going to need to handle...
+     * we're going to need to handle... If multiple queue servers
+     * are configured for failover, we'll listen to all of them.
      *
      * Side effects: in multi-site mode, may reset site configuration.
      *
@@ -240,9 +330,14 @@ class StompQueueManager extends QueueManager
     public function start($master)
     {
         parent::start($master);
-        $this->_connect();
+        $this->_connectAll();
 
-        $this->con->subscribe($this->control);
+        common_log(LOG_INFO, "Subscribing to $this->control");
+        foreach ($this->cons as $con) {
+            if ($con) {
+                $con->subscribe($this->control);
+            }
+        }
         if ($this->sites) {
             foreach ($this->sites as $server) {
                 StatusNet::init($server);
@@ -251,10 +346,14 @@ class StompQueueManager extends QueueManager
         } else {
             $this->doSubscribe();
         }
-        $this->begin();
+        foreach ($this->cons as $i => $con) {
+            if ($con) {
+                $this->begin($i);
+            }
+        }
         return true;
     }
-    
+
     /**
      * Subscribe to all the queues we're going to need to handle...
      *
@@ -266,8 +365,12 @@ class StompQueueManager extends QueueManager
     {
         // If there are any outstanding delivered messages we haven't processed,
         // free them for another thread to take.
-        $this->rollback();
-        $this->con->unsubscribe($this->control);
+        foreach ($this->cons as $i => $con) {
+            if ($con) {
+                $this->rollback($i);
+                $con->unsubscribe($this->control);
+            }
+        }
         if ($this->sites) {
             foreach ($this->sites as $server) {
                 StatusNet::init($server);
@@ -289,23 +392,106 @@ class StompQueueManager extends QueueManager
     }
 
     /**
-     * Lazy open connection to Stomp queue server.
+     * Lazy open a single connection to Stomp queue server.
+     * If multiple servers are configured, we let the Stomp client library
+     * worry about finding a working connection among them.
      */
     protected function _connect()
     {
-        if (empty($this->con)) {
-            $this->_log(LOG_INFO, "Connecting to '$this->server' as '$this->username'...");
-            $this->con = new LiberalStomp($this->server);
-
-            if ($this->con->connect($this->username, $this->password)) {
-                $this->_log(LOG_INFO, "Connected.");
+        if (empty($this->cons)) {
+            $list = $this->servers;
+            if (count($list) > 1) {
+                shuffle($list); // Randomize to spread load
+                $url = 'failover://(' . implode(',', $list) . ')';
             } else {
-                $this->_log(LOG_ERR, 'Failed to connect to queue server');
-                throw new ServerException('Failed to connect to queue server');
+                $url = $list[0];
+            }
+            $con = $this->_doConnect($url);
+            $this->cons = array($con);
+            $this->transactionCount = array(0);
+            $this->transaction = array(null);
+            $this->disconnect = array(null);
+        }
+    }
+
+    /**
+     * Lazy open connections to all Stomp servers, if in manual failover
+     * mode. This means the queue servers don't speak to each other, so
+     * we have to listen to all of them to make sure we get all events.
+     */
+    protected function _connectAll()
+    {
+        if (!common_config('queue', 'stomp_manual_failover')) {
+            return $this->_connect();
+        }
+        if (empty($this->cons)) {
+            $this->cons = array();
+            $this->transactionCount = array();
+            $this->transaction = array();
+            foreach ($this->servers as $idx => $server) {
+                try {
+                    $this->cons[] = $this->_doConnect($server);
+                    $this->disconnect[] = null;
+                } catch (Exception $e) {
+                    // s'okay, we'll live
+                    $this->cons[] = null;
+                    $this->disconnect[] = time();
+                }
+                $this->transactionCount[] = 0;
+                $this->transaction[] = null;
+            }
+            if (empty($this->cons)) {
+                throw new ServerException("No queue servers reachable...");
+                return false;
             }
         }
     }
 
+    protected function _reconnect($idx)
+    {
+        try {
+            $con = $this->_doConnect($this->servers[$idx]);
+        } catch (Exception $e) {
+            $this->_log(LOG_ERR, $e->getMessage());
+            $con = null;
+        }
+        if ($con) {
+            $this->cons[$idx] = $con;
+            $this->disconnect[$idx] = null;
+
+            // now we have to listen to everything...
+            // @fixme refactor this nicer. :P
+            $host = $con->getServer();
+            $this->_log(LOG_INFO, "Resubscribing to $this->control on $host");
+            $con->subscribe($this->control);
+            foreach ($this->subscriptions as $site => $queues) {
+                foreach ($queues as $queue) {
+                    $this->_log(LOG_INFO, "Resubscribing to $queue on $host");
+                    $con->subscribe($queue);
+                }
+            }
+            $this->begin($idx);
+        } else {
+            // Try again later...
+            $this->disconnect[$idx] = time();
+        }
+    }
+
+    protected function _doConnect($server)
+    {
+        $this->_log(LOG_INFO, "Connecting to '$server' as '$this->username'...");
+        $con = new LiberalStomp($server);
+
+        if ($con->connect($this->username, $this->password)) {
+            $this->_log(LOG_INFO, "Connected.");
+        } else {
+            $this->_log(LOG_ERR, 'Failed to connect to queue server');
+            throw new ServerException('Failed to connect to queue server');
+        }
+
+        return $con;
+    }
+
     /**
      * Subscribe to all enabled notice queues for the current site.
      */
@@ -317,7 +503,11 @@ class StompQueueManager extends QueueManager
             $rawqueue = $this->queueName($queue);
             $this->subscriptions[$site][$queue] = $rawqueue;
             $this->_log(LOG_INFO, "Subscribing to $rawqueue");
-            $this->con->subscribe($rawqueue);
+            foreach ($this->cons as $con) {
+                if ($con) {
+                    $con->subscribe($rawqueue);
+                }
+            }
         }
     }
 
@@ -331,7 +521,11 @@ class StompQueueManager extends QueueManager
         if (!empty($this->subscriptions[$site])) {
             foreach ($this->subscriptions[$site] as $queue => $rawqueue) {
                 $this->_log(LOG_INFO, "Unsubscribing from $rawqueue");
-                $this->con->unsubscribe($rawqueue);
+                foreach ($this->cons as $con) {
+                    if ($con) {
+                        $con->unsubscribe($rawqueue);
+                    }
+                }
                 unset($this->subscriptions[$site][$queue]);
             }
         }
@@ -346,27 +540,31 @@ class StompQueueManager extends QueueManager
      * Side effects: in multi-site mode, may reset site configuration to
      * match the site that queued the event.
      *
+     * @param int $idx connection index
      * @param StompFrame $frame
      * @return bool
      */
-    protected function handleItem($frame)
+    protected function handleItem($idx, $frame)
     {
+        $this->defaultIdx = $idx;
+
         list($site, $queue) = $this->parseDestination($frame->headers['destination']);
         if ($site != $this->currentSite()) {
             $this->stats('switch');
             StatusNet::init($site);
         }
 
+        $host = $this->cons[$idx]->getServer();
         if (is_numeric($frame->body)) {
             $id = intval($frame->body);
-            $info = "notice $id posted at {$frame->headers['created']} in queue $queue";
+            $info = "notice $id posted at {$frame->headers['created']} in queue $queue from $host";
 
             $notice = Notice::staticGet('id', $id);
             if (empty($notice)) {
                 $this->_log(LOG_WARNING, "Skipping missing $info");
-                $this->ack($frame);
-                $this->commit();
-                $this->begin();
+                $this->ack($idx, $frame);
+                $this->commit($idx);
+                $this->begin($idx);
                 $this->stats('badnotice', $queue);
                 return false;
             }
@@ -374,39 +572,47 @@ class StompQueueManager extends QueueManager
             $item = $notice;
         } else {
             // @fixme should we serialize, or json, or what here?
-            $info = "string posted at {$frame->headers['created']} in queue $queue";
+            $info = "string posted at {$frame->headers['created']} in queue $queue from $host";
             $item = $frame->body;
         }
 
         $handler = $this->getHandler($queue);
         if (!$handler) {
             $this->_log(LOG_ERR, "Missing handler class; skipping $info");
-            $this->ack($frame);
-            $this->commit();
-            $this->begin();
+            $this->ack($idx, $frame);
+            $this->commit($idx);
+            $this->begin($idx);
             $this->stats('badhandler', $queue);
             return false;
         }
 
-        $ok = $handler->handle($item);
+        // If there's an exception when handling,
+        // log the error and let it get requeued.
+
+        try {
+            $ok = $handler->handle($item);
+        } catch (Exception $e) {
+            $this->_log(LOG_ERR, "Exception on queue $queue: " . $e->getMessage());
+            $ok = false;
+        }
 
         if (!$ok) {
             $this->_log(LOG_WARNING, "Failed handling $info");
             // FIXME we probably shouldn't have to do
             // this kind of queue management ourselves;
             // if we don't ack, it should resend...
-            $this->ack($frame);
+            $this->ack($idx, $frame);
             $this->enqueue($item, $queue);
-            $this->commit();
-            $this->begin();
+            $this->commit($idx);
+            $this->begin($idx);
             $this->stats('requeued', $queue);
             return false;
         }
 
         $this->_log(LOG_INFO, "Successfully handled $info");
-        $this->ack($frame);
-        $this->commit();
-        $this->begin();
+        $this->ack($idx, $frame);
+        $this->commit($idx);
+        $this->begin($idx);
         $this->stats('handled', $queue);
         return true;
     }
@@ -414,10 +620,11 @@ class StompQueueManager extends QueueManager
     /**
      * Process a control signal broadcast.
      *
+     * @param int $idx connection index
      * @param array $frame Stomp frame
      * @return bool true to continue; false to stop further processing.
      */
-    protected function handleControlSignal($frame)
+    protected function handleControlSignal($idx, $frame)
     {
         $message = trim($frame->body);
         if (strpos($message, ':') !== false) {
@@ -441,12 +648,12 @@ class StompQueueManager extends QueueManager
             $this->_log(LOG_ERR, "Ignoring unrecognized control message: $message");
         }
 
-        $this->ack($frame);
-        $this->commit();
-        $this->begin();
+        $this->ack($idx, $frame);
+        $this->commit($idx);
+        $this->begin($idx);
         return $shutdown;
     }
-    
+
     /**
      * Set us up with queue subscriptions for a new site added at runtime,
      * triggered by a broadcast to the 'statusnet-control' topic.
@@ -520,47 +727,49 @@ class StompQueueManager extends QueueManager
         common_log($level, 'StompQueueManager: '.$msg);
     }
 
-    protected function begin()
+    protected function begin($idx)
     {
         if ($this->useTransactions) {
-            if ($this->transaction) {
+            if (!empty($this->transaction[$idx])) {
                 throw new Exception("Tried to start transaction in the middle of a transaction");
             }
-            $this->transactionCount++;
-            $this->transaction = $this->master->id . '-' . $this->transactionCount . '-' . time();
-            $this->con->begin($this->transaction);
+            $this->transactionCount[$idx]++;
+            $this->transaction[$idx] = $this->master->id . '-' . $this->transactionCount[$idx] . '-' . time();
+            $this->cons[$idx]->begin($this->transaction[$idx]);
         }
     }
 
-    protected function ack($frame)
+    protected function ack($idx, $frame)
     {
         if ($this->useTransactions) {
-            if (!$this->transaction) {
+            if (empty($this->transaction[$idx])) {
                 throw new Exception("Tried to ack but not in a transaction");
             }
+            $this->cons[$idx]->ack($frame, $this->transaction[$idx]);
+        } else {
+            $this->cons[$idx]->ack($frame);
         }
-        $this->con->ack($frame, $this->transaction);
     }
 
-    protected function commit()
+    protected function commit($idx)
     {
         if ($this->useTransactions) {
-            if (!$this->transaction) {
+            if (empty($this->transaction[$idx])) {
                 throw new Exception("Tried to commit but not in a transaction");
             }
-            $this->con->commit($this->transaction);
-            $this->transaction = null;
+            $this->cons[$idx]->commit($this->transaction[$idx]);
+            $this->transaction[$idx] = null;
         }
     }
 
-    protected function rollback()
+    protected function rollback($idx)
     {
         if ($this->useTransactions) {
-            if (!$this->transaction) {
+            if (empty($this->transaction[$idx])) {
                 throw new Exception("Tried to rollback but not in a transaction");
             }
-            $this->con->commit($this->transaction);
-            $this->transaction = null;
+            $this->cons[$idx]->commit($this->transaction[$idx]);
+            $this->transaction[$idx] = null;
         }
     }
 }
index 6c9f6316ada31e05d3614be35ce5a3c677f995a0..ed1a62385b2836febb3ea1624b965f598027758d 100644 (file)
@@ -178,7 +178,6 @@ function common_ensure_session()
        }
        if (isset($id)) {
            session_id($id);
-           setcookie(session_name(), $id);
        }
         @session_start();
         if (!isset($_SESSION['started'])) {
diff --git a/plugins/Adsense/AdsensePlugin.php b/plugins/Adsense/AdsensePlugin.php
new file mode 100644 (file)
index 0000000..ab2b9a6
--- /dev/null
@@ -0,0 +1,160 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin for Google Adsense
+ *
+ * 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  Ads
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 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 to add Google Adsense to StatusNet sites
+ *
+ * This plugin lets you add Adsense ad units to your StatusNet site.
+ *
+ * We support the 4 ad sizes for the Universal Ad Platform (UAP):
+ *
+ *     Medium Rectangle
+ *     (Small) Rectangle
+ *     Leaderboard
+ *     Wide Skyscraper
+ *
+ * They fit in different places on the default theme. Some themes
+ * might interact quite poorly with this plugin.
+ *
+ * To enable advertising, you must sign up with Google Adsense and
+ * get a client ID.
+ *
+ *     https://www.google.com/adsense/
+ *
+ * You'll also need to create an Adsense for Content unit in one
+ * of the four sizes described above. At the end of the process,
+ * note the "google_ad_client" and "google_ad_slot" values in the
+ * resultant Javascript.
+ *
+ * Add the plugin to config.php like so:
+ *
+ *     addPlugin('Adsense', array('client' => 'Your client ID',
+ *                                'rectangle' => 'slot'));
+ *
+ * Here, your client ID is the value of google_ad_client and the
+ * slot is the value of google_ad_slot. Note that if you create
+ * a different size, you'll need to provide different arguments:
+ * 'mediumRectangle', 'leaderboard', or 'wideSkyscraper'.
+ *
+ * If for some reason your ad server is different from the default,
+ * use the 'adScript' parameter to set the full path to the ad script.
+ *
+ * @category Plugin
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@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/
+ *
+ * @seeAlso  UAPPlugin
+ */
+
+class AdsensePlugin extends UAPPlugin
+{
+    public $adScript = 'http://pagead2.googlesyndication.com/pagead/show_ads.js';
+    public $client   = null;
+
+    /**
+     * Show a medium rectangle 'ad'
+     *
+     * @param Action $action Action being shown
+     *
+     * @return void
+     */
+
+    protected function showMediumRectangle($action)
+    {
+        $this->showAdsenseCode($action, 300, 250, $this->mediumRectangle);
+    }
+
+    /**
+     * Show a rectangle 'ad'
+     *
+     * @param Action $action Action being shown
+     *
+     * @return void
+     */
+
+    protected function showRectangle($action)
+    {
+        $this->showAdsenseCode($action, 180, 150, $this->rectangle);
+    }
+
+    /**
+     * Show a wide skyscraper ad
+     *
+     * @param Action $action Action being shown
+     *
+     * @return void
+     */
+
+    protected function showWideSkyscraper($action)
+    {
+        $this->showAdsenseCode($action, 160, 600, $this->wideSkyscraper);
+    }
+
+    /**
+     * Show a leaderboard ad
+     *
+     * @param Action $action Action being shown
+     *
+     * @return void
+     */
+
+    protected function showLeaderboard($action)
+    {
+        $this->showAdsenseCode($action, 728, 90, $this->leaderboard);
+    }
+
+    /**
+     * Output the bits of JavaScript code to show Adsense
+     *
+     * @param Action  $action Action being shown
+     * @param integer $width  Width of the block
+     * @param integer $height Height of the block
+     * @param string  $slot   Slot identifier
+     *
+     * @return void
+     */
+
+    protected function showAdsenseCode($action, $width, $height, $slot)
+    {
+        $code  = 'google_ad_client = "'.$this->client.'"; ';
+        $code .= 'google_ad_slot = "'.$slot.'"; ';
+        $code .= 'google_ad_width = '.$width.'; ';
+        $code .= 'google_ad_height = '.$height.'; ';
+
+        $action->inlineScript($code);
+
+        $action->script($this->adScript);
+    }
+}
\ No newline at end of file
index 52cc9c97f94b48a52c7dcdaae09a3e0eb97b371c..589462ed9936399492436536a8c5ca248e96ca4b 100644 (file)
@@ -71,7 +71,7 @@ class GeonamesPlugin extends Plugin
         $loc = $this->getCache(array('name' => $name,
                                      'language' => $language));
 
-        if (!empty($loc)) {
+        if ($loc !== false) {
             $location = $loc;
             return false;
         }
@@ -87,12 +87,20 @@ class GeonamesPlugin extends Plugin
             return true;
         }
 
+        if (count($geonames) == 0) {
+            // no results
+            $this->setCache(array('name' => $name,
+                                  'language' => $language),
+                            null);
+            return true;
+        }
+
         $n = $geonames[0];
 
         $location = new Location();
 
-        $location->lat              = (string)$n->lat;
-        $location->lon              = (string)$n->lng;
+        $location->lat              = $this->canonical($n->lat);
+        $location->lon              = $this->canonical($n->lng);
         $location->names[$language] = (string)$n->name;
         $location->location_id      = (string)$n->geonameId;
         $location->location_ns      = self::LOCATION_NS;
@@ -125,7 +133,7 @@ class GeonamesPlugin extends Plugin
 
         $loc = $this->getCache(array('id' => $id));
 
-        if (!empty($loc)) {
+        if ($loc !== false) {
             $location = $loc;
             return false;
         }
@@ -157,8 +165,9 @@ class GeonamesPlugin extends Plugin
 
         $location->location_id      = (string)$last->geonameId;
         $location->location_ns      = self::LOCATION_NS;
-        $location->lat              = (string)$last->lat;
-        $location->lon              = (string)$last->lng;
+        $location->lat              = $this->canonical($last->lat);
+        $location->lon              = $this->canonical($last->lng);
+
         $location->names[$language] = implode(', ', array_reverse($parts));
 
         $this->setCache(array('id' => (string)$last->geonameId),
@@ -186,13 +195,15 @@ class GeonamesPlugin extends Plugin
 
     function onLocationFromLatLon($lat, $lon, $language, &$location)
     {
-        $lat = rtrim($lat, "0");
-        $lon = rtrim($lon, "0");
+        // Make sure they're canonical
+
+        $lat = $this->canonical($lat);
+        $lon = $this->canonical($lon);
 
         $loc = $this->getCache(array('lat' => $lat,
                                      'lon' => $lon));
 
-        if (!empty($loc)) {
+        if ($loc !== false) {
             $location = $loc;
             return false;
         }
@@ -207,6 +218,14 @@ class GeonamesPlugin extends Plugin
             return true;
         }
 
+        if (count($geonames) == 0) {
+            // no results
+            $this->setCache(array('lat' => $lat,
+                                  'lon' => $lon),
+                            null);
+            return true;
+        }
+
         $n = $geonames[0];
 
         $parts = array();
@@ -225,8 +244,8 @@ class GeonamesPlugin extends Plugin
 
         $location->location_id = (string)$n->geonameId;
         $location->location_ns = self::LOCATION_NS;
-        $location->lat         = (string)$lat;
-        $location->lon         = (string)$lon;
+        $location->lat         = $this->canonical($n->lat);
+        $location->lon         = $this->canonical($n->lng);
 
         $location->names[$language] = implode(', ', $parts);
 
@@ -264,7 +283,7 @@ class GeonamesPlugin extends Plugin
         $n = $this->getCache(array('id' => $id,
                                    'language' => $language));
 
-        if (!empty($n)) {
+        if ($n !== false) {
             $name = $n;
             return false;
         }
@@ -278,6 +297,13 @@ class GeonamesPlugin extends Plugin
             return false;
         }
 
+        if (count($geonames) == 0) {
+            $this->setCache(array('id' => $id,
+                                  'language' => $language),
+                            null);
+            return false;
+        }
+
         $parts = array();
 
         foreach ($geonames as $level) {
@@ -412,17 +438,29 @@ class GeonamesPlugin extends Plugin
             throw new Exception("HTTP error code " . $result->code);
         }
 
-        $document = new SimpleXMLElement($result->getBody());
+        $body = $result->getBody();
+
+        if (empty($body)) {
+            throw new Exception("Empty HTTP body in response");
+        }
+
+        // This will throw an exception if the XML is mal-formed
+
+        $document = new SimpleXMLElement($body);
 
-        if (empty($document)) {
-            throw new Exception("No results in response");
+        // No children, usually no results
+
+        $children = $document->children();
+
+        if (count($children) == 0) {
+            return array();
         }
 
         if (isset($document->status)) {
             throw new Exception("Error #".$document->status['value']." ('".$document->status['message']."')");
         }
 
-        // Array of elements
+        // Array of elements, >0 elements
 
         return $document->geoname;
     }
@@ -438,4 +476,12 @@ class GeonamesPlugin extends Plugin
                                'names for locations based on user-provided lat/long pairs.'));
         return true;
     }
+
+    function canonical($coord)
+    {
+        $coord = rtrim($coord, "0");
+        $coord = rtrim($coord, ".");
+
+        return $coord;
+    }
 }
index 8c8b8da6dc183cdd45dfef86598dee23fea58148..2bc4b892bd6a287c9750c73060f7522f68db5bf5 100644 (file)
@@ -59,6 +59,8 @@ class MemcachePlugin extends Plugin
 
     public $persistent = null;
 
+    public $defaultExpiry = 86400; // 24h
+
     /**
      * Initialize the plugin
      *
@@ -110,6 +112,9 @@ class MemcachePlugin extends Plugin
     function onStartCacheSet(&$key, &$value, &$flag, &$expiry, &$success)
     {
         $this->_ensureConn();
+        if ($expiry === null) {
+            $expiry = $this->defaultExpiry;
+        }
         $success = $this->_conn->set($key, $value, $flag, $expiry);
         Event::handle('EndCacheSet', array($key, $value, $flag,
                                            $expiry));
diff --git a/plugins/OpenX/OpenXPlugin.php b/plugins/OpenX/OpenXPlugin.php
new file mode 100644 (file)
index 0000000..59485f2
--- /dev/null
@@ -0,0 +1,165 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin for OpenX ad server
+ *
+ * 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  Ads
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 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 for OpenX Ad Server
+ *
+ * This plugin supports the OpenX ad server, http://www.openx.org/
+ *
+ * We support the 4 ad sizes for the Universal Ad Platform (UAP):
+ *
+ *     Medium Rectangle
+ *     (Small) Rectangle
+ *     Leaderboard
+ *     Wide Skyscraper
+ *
+ * They fit in different places on the default theme. Some themes
+ * might interact quite poorly with this plugin.
+ *
+ * To enable advertising, you will need an OpenX server. You'll need
+ * to set up a "zone" for your StatusNet site that identifies a
+ * kind of ad you want to place (of the above 4 sizes).
+ *
+ * Add the plugin to config.php like so:
+ *
+ *     addPlugin('OpenX', array('adScript' => 'full path to script',
+ *                              'rectangle' => 1));
+ *
+ * Here, the 'adScript' parameter is the full path to the OpenX
+ * ad script, like 'http://example.com/www/delivery/ajs.php'. Note
+ * that we don't do any magic to swap between HTTP and HTTPS, so
+ * if you want HTTPS, say so.
+ *
+ * The 'rectangle' parameter is the zone ID for that ad space on
+ * your site. If you've configured another size, try 'mediumRectangle',
+ * 'leaderboard', or 'wideSkyscraper'.
+ *
+ * If for some reason your ad server is different from the default,
+ * use the 'adScript' parameter to set the full path to the ad script.
+ *
+ * @category Ads
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@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/
+ *
+ * @seeAlso  UAPPlugin
+ */
+
+class OpenXPlugin extends UAPPlugin
+{
+    public $adScript = null;
+
+    /**
+     * Show a medium rectangle 'ad'
+     *
+     * @param Action $action Action being shown
+     *
+     * @return void
+     */
+
+    protected function showMediumRectangle($action)
+    {
+        $this->showAd($action, $this->mediumRectangle);
+    }
+
+    /**
+     * Show a rectangle 'ad'
+     *
+     * @param Action $action Action being shown
+     *
+     * @return void
+     */
+
+    protected function showRectangle($action)
+    {
+        $this->showAd($action, $this->rectangle);
+    }
+
+    /**
+     * Show a wide skyscraper ad
+     *
+     * @param Action $action Action being shown
+     *
+     * @return void
+     */
+
+    protected function showWideSkyscraper($action)
+    {
+        $this->showAd($action, $this->wideSkyscraper);
+    }
+
+    /**
+     * Show a leaderboard ad
+     *
+     * @param Action $action Action being shown
+     *
+     * @return void
+     */
+
+    protected function showLeaderboard($action)
+    {
+        $this->showAd($action, $this->leaderboard);
+    }
+
+    /**
+     * Show an ad using OpenX
+     *
+     * @param Action  $action Action being shown
+     * @param integer $zone   Zone to show
+     *
+     * @return void
+     */
+
+    protected function showAd($action, $zone)
+    {
+$scr = <<<ENDOFSCRIPT
+var m3_u = '%s';
+var m3_r = Math.floor(Math.random()*99999999999);
+if (!document.MAX_used) document.MAX_used = ',';
+document.write ("<scr"+"ipt type='text/javascript' src='"+m3_u);
+document.write ("?zoneid=%d");
+document.write ('&amp;cb=' + m3_r);
+if (document.MAX_used != ',') document.write ("&amp;exclude=" + document.MAX_used);
+document.write (document.charset ? '&amp;charset='+document.charset : (document.characterSet ? '&amp;charset='+document.characterSet : ''));
+document.write ("&amp;loc=" + escape(window.location));
+if (document.referrer) document.write ("&amp;referer=" + escape(document.referrer));
+if (document.context) document.write ("&context=" + escape(document.context));
+if (document.mmm_fo) document.write ("&amp;mmm_fo=1");
+document.write ("'><\/scr"+"ipt>");
+ENDOFSCRIPT;
+
+        $action->inlineScript(sprintf($scr, $this->adScript, $zone));
+        return true;
+    }
+}
\ No newline at end of file
index 16e28e94d3eed57718c63e9efdd6de916ec0845f..6c212453e4455d12499690e2d83b7b984d70c724 100644 (file)
@@ -87,7 +87,7 @@ class RealtimePlugin extends Plugin
         $scripts = $this->_getScripts();
 
         foreach ($scripts as $script) {
-            $action->script(common_path($script));
+            $action->script($script);
         }
 
         $user = common_current_user();
@@ -307,7 +307,7 @@ class RealtimePlugin extends Plugin
 
     function _getScripts()
     {
-        return array('plugins/Realtime/realtimeupdate.js');
+        return array(common_path('plugins/Realtime/realtimeupdate.js'));
     }
 
     function _updateInitialize($timeline, $user_id)
index 52151f9de8dad1dd42df0e95034b020ff98e9e32..fb9dcdbfb793bf2422a01161dfb0f69c10b9e92d 100644 (file)
@@ -132,11 +132,11 @@ RealtimeUpdate = {
           user = data['user'];
           html = data['html'].replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"').replace(/&amp;/g,'&');
           source = data['source'].replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"').replace(/&amp;/g,'&');
-
+console.log(data);
           ni = "<li class=\"hentry notice\" id=\"notice-"+unique+"\">"+
                "<div class=\"entry-title\">"+
                "<span class=\"vcard author\">"+
-               "<a href=\""+user['profile_url']+"\" class=\"url\">"+
+               "<a href=\""+user['profile_url']+"\" class=\"url\" title=\""+user['name']+"\">"+
                "<img src=\""+user['profile_image_url']+"\" class=\"avatar photo\" width=\"48\" height=\"48\" alt=\""+user['screen_name']+"\"/>"+
                "<span class=\"nickname fn\">"+user['screen_name']+"</span>"+
                "</a>"+
diff --git a/plugins/UserLimitPlugin.php b/plugins/UserLimitPlugin.php
new file mode 100644 (file)
index 0000000..ab31872
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin to limit number of users that can register (best for cloud providers)
+ *
+ * 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  Action
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@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 to limit number of users that can register (best for cloud providers)
+ *
+ * For cloud providers whose freemium model is based on how many
+ * users can register. We use it on the StatusNet Cloud.
+ *
+ * @category Plugin
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@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/
+ *
+ * @seeAlso  Location
+ */
+
+class UserLimitPlugin extends Plugin
+{
+    public $maxUsers = null;
+
+    function onStartUserRegister(&$user, &$profile)
+    {
+        $this->_checkMaxUsers();
+        return true;
+    }
+
+    function onStartRegistrationTry($action)
+    {
+        $this->_checkMaxUsers();
+        return true;
+    }
+
+    function _checkMaxUsers()
+    {
+        if (!is_null($this->maxUsers)) {
+
+            $cls = new User();
+
+            $cnt = $cls->count();
+
+            if ($cnt >= $this->maxUsers) {
+                $msg = sprintf(_('Cannot register; maximum number of users (%d) reached.'),
+                               $this->maxUsers);
+
+                throw new ClientException($msg);
+            }
+        }
+    }
+
+    function onPluginVersion(&$versions)
+    {
+        $versions[] = array('name' => 'UserLimit',
+                            'version' => STATUSNET_VERSION,
+                            'author' => 'Evan Prodromou',
+                            'homepage' => 'http://status.net/wiki/Plugin:UserLimit',
+                            'description' =>
+                            _m('Limit the number of users who can register.'));
+        return true;
+    }
+}
index f55f1486bbbf7d902db8465e4ab2cdc2116f2cc3..3a8ebdcfd42d04b4ab832bb9009a3eaf5114f1cf 100755 (executable)
@@ -1,5 +1,9 @@
 #!/bin/bash
 
+# live fast! die young!
+
+set -e
+
 source /etc/statusnet/setup.cfg
 
 export nickname=$1
index c2e2351c3910e307331303c0509e30692f89b012..30a8a9602630e439ca16736d829f25bbb10d6ce8 100755 (executable)
@@ -109,7 +109,13 @@ class QueueDaemon extends SpawningDaemon
 
         $master = new QueueMaster($this->get_id());
         $master->init($this->all);
-        $master->service();
+        try {
+            $master->service();
+        } catch (Exception $e) {
+            common_log(LOG_ERR, "Unhandled exception: " . $e->getMessage() . ' ' .
+                str_replace("\n", " ", $e->getTraceAsString()));
+            return self::EXIT_ERR;
+        }
 
         $this->log(LOG_INFO, 'finished servicing the queue');
 
diff --git a/scripts/sendemail.php b/scripts/sendemail.php
new file mode 100755 (executable)
index 0000000..436e085
--- /dev/null
@@ -0,0 +1,82 @@
+#!/usr/bin/env php
+<?php
+/*
+ * StatusNet - a 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:n:';
+$longoptions = array('id=', 'nickname=', 'subject=');
+
+$helptext = <<<END_OF_USEREMAIL_HELP
+sendemail.php [options] < <message body>
+Sends given email text to user.
+
+  -i --id       id of the user to query
+  -n --nickname nickname of the user to query
+     --subject  mail subject line (required)
+
+END_OF_USEREMAIL_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+if (have_option('i', 'id')) {
+    $id = get_option_value('i', 'id');
+    $user = User::staticGet('id', $id);
+    if (empty($user)) {
+        print "Can't find user with ID $id\n";
+        exit(1);
+    }
+} else if (have_option('n', 'nickname')) {
+    $nickname = get_option_value('n', 'nickname');
+    $user = User::staticGet('nickname', $nickname);
+    if (empty($user)) {
+        print "Can't find user with nickname '$nickname'\n";
+        exit(1);
+    }
+} else {
+    print "You must provide a user by --id or --nickname\n";
+    exit(1);
+}
+
+if (empty($user->email)) {
+    // @fixme unconfirmed address?
+    print "No email registered for user '$user->nickname'\n";
+    exit(1);
+}
+
+if (!have_option('subject')) {
+    echo "You must provide a subject line for the mail in --subject='...' param.\n";
+    exit(1);
+}
+$subject = get_option_value('subject');
+
+if (posix_isatty(STDIN)) {
+    print "You must provide message input on stdin!\n";
+    exit(1);
+}
+$body = file_get_contents('php://stdin');
+
+print "Sending to $user->email...";
+if (mail_to_user($user, $subject, $body)) {
+    print " done\n";
+} else {
+    print " failed.\n";
+    exit(1);
+}
+
diff --git a/scripts/settag.php b/scripts/settag.php
new file mode 100644 (file)
index 0000000..e91d5eb
--- /dev/null
@@ -0,0 +1,84 @@
+#!/usr/bin/env php
+<?php
+/*
+ * StatusNet - a 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 = 'd';
+$longoptions = array('delete');
+
+$helptext = <<<END_OF_SETTAG_HELP
+settag.php [options] <site> <tag>
+Set the tag <tag> for site <site>.
+
+With -d, delete the tag.
+
+END_OF_SETTAG_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+if (count($args) != 2) {
+    show_help();
+    exit(1);
+}
+
+$nickname = $args[0];
+$tag = strtolower($args[1]);
+
+$sn = Status_network::memGet('nickname', $nickname);
+
+if (empty($sn)) {
+    print "No such site.\n";
+    exit(-1);
+}
+
+$tags = $sn->getTags();
+
+$i = array_search($tag, $tags);
+
+if ($i !== false) {
+    if (have_option('d', 'delete')) { // Delete
+        unset($tags[$i]);
+
+        $orig = clone($sn);
+        $sn->tags = implode('|', $tags);
+        $result = $sn->update($orig);
+        if (!$result) {
+            print "Couldn't update.\n";
+            exit(-1);
+        }
+    } else {
+        print "Already set.\n";
+        exit(-1);
+    }
+} else {
+    if (have_option('d', 'delete')) { // Delete
+        print "No such tag.\n";
+        exit(-1);
+    } else {
+        $tags[] = $tag;
+        $orig = clone($sn);
+        $sn->tags = implode('|', $tags);
+        $result = $sn->update($orig);
+        if (!$result) {
+            print "Couldn't update.\n";
+            exit(-1);
+        }
+    }
+}
index 8d03b06f5e8db8c13e00f8ee888195cc32fe3fe6..f247a3bcae0e6910eb709bfddee23fd61682014a 100644 (file)
@@ -11,4 +11,8 @@ export AVATARBASE=/var/www/avatar.example.net
 export BACKGROUNDBASE=/var/www/background.example.net
 export FILEBASE=/var/www/file.example.net
 export PWDGEN="pwgen 20"
-
+export PHPBASE=/var/www/statusnet
+export WILDCARD=example.net
+export MAILTEMPLATE=/etc/statusnet/newsite-mail.txt
+export MAILSUBJECT="Your new StatusNet site"
+export POSTINSTALL=/etc/statusnet/morestuff.sh
index 777711fb55a29d24f836e1b3bca8e2a602b3415c..4ad808011c8c5f8a89ebb97bd3142aba0cccfbb3 100755 (executable)
@@ -1,10 +1,28 @@
 #!/bin/bash
 
+# live fast! die young!
+
+set -e
+
 source /etc/statusnet/setup.cfg
 
-export nickname=$1
-export sitename=$2
+# setup_status_net.sh mysite 'My Site' '1user' 'owner@example.com' 'Firsty McLastname'
+
+export nickname="$1"
+export sitename="$2"
+export tags="$3"
+export email="$4"
+export fullname="$5"
+
+# Fixme: if this is changed later we need to update profile URLs
+# for the created user.
+export server="$nickname.$WILDCARD"
+
+# End-user info
+export userpass=`$PWDGEN`
+export roles="administrator moderator owner"
 
+# DB info
 export password=`$PWDGEN`
 export database=$nickname$DBBASE
 export username=$nickname$USERBASE
@@ -21,8 +39,8 @@ mysql -h $DBHOST -u $ADMIN --password=$ADMINPASS $SITEDB << ENDOFCOMMANDS
 
 GRANT ALL ON $database.* TO '$username'@'localhost' IDENTIFIED BY '$password';
 GRANT ALL ON $database.* TO '$username'@'%' IDENTIFIED BY '$password';
-INSERT INTO status_network (nickname, dbhost, dbuser, dbpass, dbname, sitename, created)
-VALUES ('$nickname', '$DBHOSTNAME', '$username', '$password', '$database', '$sitename', now());
+INSERT INTO status_network (nickname, dbhost, dbuser, dbpass, dbname, sitename, created, tags)
+VALUES ('$nickname', '$DBHOSTNAME', '$username', '$password', '$database', '$sitename', now(), '$tags');
 
 ENDOFCOMMANDS
 
@@ -30,3 +48,39 @@ for top in $AVATARBASE $FILEBASE $BACKGROUNDBASE; do
     mkdir $top/$nickname
     chmod a+w $top/$nickname
 done
+
+php $PHPBASE/scripts/registeruser.php \
+  -s"$server" \
+  -n"$nickname" \
+  -f"$fullname" \
+  -w"$userpass" \
+  -e"$email"
+
+for role in $roles
+do
+  php $PHPBASE/scripts/userrole.php \
+    -s"$server" \
+    -n"$nickname" \
+    -r"$role"
+done
+
+if [ -f "$MAILTEMPLATE" ]
+then
+    # fixme how safe is this? are sitenames sanitized?
+    cat $MAILTEMPLATE | \
+      sed "s/\$nickname/$nickname/" | \
+      sed "s/\$sitename/$sitename/" | \
+      sed "s/\$userpass/$userpass/" | \
+      php $PHPBASE/scripts/sendemail.php \
+        -s"$server" \
+        -n"$nickname" \
+        --subject="$MAILSUBJECT"
+else
+    echo "No mail template, not sending email."
+fi
+
+if [ -f "$POSTINSTALL" ]
+then
+    echo "Running $POSTINSTALL ..."
+    source "$POSTINSTALL"
+fi
index b109706a13f39aae33262982952c1966c79628c4..0d6395d057a8843449343a075f525d856997649e 100644 (file)
@@ -1130,8 +1130,17 @@ top:3px;
 }
 
 .dialogbox .submit_dialogbox {
-text-indent:0;
 font-weight:bold;
+text-indent:0;
+min-width:46px;
+}
+
+#wrap form.processing input.submit,
+.entity_actions a.processing,
+.dialogbox.processing .submit_dialogbox {
+cursor:wait;
+outline:none;
+text-indent:-9999px;
 }
 
 .notice-options {
index 3aebb239d30c77d7204bee99a54bfe184a59cc7a..06711850fc3856c3452290b977f031ac9a8a9c76 100644 (file)
@@ -196,11 +196,12 @@ background-color:transparent;
 }
 
 #wrap form.processing input.submit,
-.entity_actions a.processing {
+.entity_actions a.processing,
+.dialogbox.processing .submit_dialogbox {
 background:#FFFFFF url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%;
-cursor:wait;
-text-indent:-9999px;
-outline:none;
+}
+.notice-options .form_repeat.processing {
+background-image:none;
 }
 
 #content {
index 2818196c2091020a4bdf1175cb8cddad3c62a35c..1ac96ab5be4b3ad8a2210d5cb0145c0a27b71f99 100644 (file)
@@ -196,11 +196,12 @@ background-color:transparent;
 }
 
 #wrap form.processing input.submit,
-.entity_actions a.processing {
+.entity_actions a.processing,
+.dialogbox.processing .submit_dialogbox {
 background:#FFFFFF url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%;
-cursor:wait;
-text-indent:-9999px;
-outline:none;
+}
+.notice-options .form_repeat.processing {
+background-image:none;
 }
 
 #content {