code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * Base class for daemon that can launch one or more processing threads,
 * respawning them if they exit.
 *
 * This is mainly intended for indefinite workloads such as monitoring
 * a queue or maintaining an IM channel.
 *
 * Child classes should implement the
 *
 * We can then pass individual items through the QueueHandler subclasses
 * they belong to. We additionally can handle queues for multiple sites.
 *
 * @package QueueHandler
 * @author Brion Vibber <brion@status.net>
 */
abstract class SpawningDaemon extends Daemon
{
    protected $threads=1;

    const EXIT_OK = 0;
    const EXIT_ERR = 1;
    const EXIT_SHUTDOWN = 100;
    const EXIT_RESTART = 101;

    function __construct($id=null, $daemonize=true, $threads=1)
    {
        parent::__construct($daemonize);

        if ($id) {
            $this->set_id($id);
        }
        $this->threads = $threads;
    }

    /**
     * Perform some actual work!
     *
     * @return int exit code; use self::EXIT_SHUTDOWN to request not to respawn.
     */
    public abstract function runThread();

    /**
     * Spawn one or more background processes and let them start running.
     * Each individual process will execute whatever's in the runThread()
     * method, which should be overridden.
     *
     * Child processes will be automatically respawned when they exit.
     *
     * @todo possibly allow for not respawning on "normal" exits...
     *       though ParallelizingDaemon is probably better for workloads
     *       that have forseeable endpoints.
     */
    function run()
    {
        $this->initPipes();

        $children = array();
        for ($i = 1; $i <= $this->threads; $i++) {
            $pid = pcntl_fork();
            if ($pid < 0) {
                $this->log(LOG_ERR, "Couldn't fork for thread $i; aborting\n");
                exit(1);
            } else if ($pid == 0) {
                $this->initAndRunChild($i);
            } else {
                $this->log(LOG_INFO, "Spawned thread $i as pid $pid");
                $children[$i] = $pid;
            }
            sleep(common_config('queue', 'spawndelay'));
        }

        $this->log(LOG_INFO, "Waiting for children to complete.");
        while (count($children) > 0) {
            $status = null;
            $pid = pcntl_wait($status);
            if ($pid > 0) {
                $i = array_search($pid, $children);
                if ($i === false) {
                    $this->log(LOG_ERR, "Ignoring exit of unrecognized child pid $pid");
                    continue;
                }
                if (pcntl_wifexited($status)) {
                    $exitCode = pcntl_wexitstatus($status);
                    $info = "status $exitCode";
                } else if (pcntl_wifsignaled($status)) {
                    $exitCode = self::EXIT_ERR;
                    $signal = pcntl_wtermsig($status);
                    $info = "signal $signal";
                }
                unset($children[$i]);

                if ($this->shouldRespawn($exitCode)) {
                    $this->log(LOG_INFO, "Thread $i pid $pid exited with $info; respawing.");

                    $pid = pcntl_fork();
                    if ($pid < 0) {
                        $this->log(LOG_ERR, "Couldn't fork to respawn thread $i; aborting thread.\n");
                    } else if ($pid == 0) {
                        $this->initAndRunChild($i);
                    } else {
                        $this->log(LOG_INFO, "Respawned thread $i as pid $pid");
                        $children[$i] = $pid;
                    }
                    sleep(common_config('queue', 'spawndelay'));
                } else {
                    $this->log(LOG_INFO, "Thread $i pid $pid exited with status $exitCode; closing out thread.");
                }
            }
        }
        $this->log(LOG_INFO, "All child processes complete.");
        return true;
    }

    /**
     * Create an IPC socket pair which child processes can use to detect
     * if the parent process has been killed.
     */
    function initPipes()
    {
        $sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
        if ($sockets) {
            $this->parentWriter = $sockets[0];
            $this->parentReader = $sockets[1];
        } else {
            $this->log(LOG_ERR, "Couldn't create inter-process sockets");
            exit(1);
        }
    }

    /**
     * Build an IOManager that simply ensures that we have a connection
     * to the parent process open. If it breaks, the child process will
     * die.
     *
     * @return ProcessManager
     */
    public function processManager()
    {
        return new ProcessManager($this->parentReader);
    }

    /**
     * Determine whether to respawn an exited subprocess based on its exit code.
     * Otherwise we'll respawn all exits by default.
     *
     * @param int $exitCode
     * @return boolean true to respawn
     */
    protected function shouldRespawn($exitCode)
    {
        if ($exitCode == self::EXIT_SHUTDOWN) {
            // Thread requested a clean shutdown.
            return false;
        } else {
            // Otherwise we should always respawn!
            return true;
        }
    }

    /**
     * Initialize things for a fresh thread, call runThread(), and
     * exit at completion with appropriate return value.
     */
    protected function initAndRunChild($thread)
    {
        // Close the writer end of our parent<->children pipe.
        fclose($this->parentWriter);
        $this->set_id($this->get_id() . "." . $thread);
        $this->resetDb();
        $exitCode = $this->runThread();
        exit($exitCode);
    }

    /**
     * Reconnect to the database for each child process,
     * or they'll get very confused trying to use the
     * same socket.
     */
    protected function resetDb()
    {
        // @fixme do we need to explicitly open the db too
        // or is this implied?
        global $_DB_DATAOBJECT;
        unset($_DB_DATAOBJECT['CONNECTIONS']);

        // Reconnect main memcached, or threads will stomp on
        // each other and corrupt their requests.
        $cache = Cache::instance();
        if ($cache) {
            $cache->reconnect();
        }

        // Also reconnect memcached for status_network table.
        if (!empty(Status_network::$cache)) {
            Status_network::$cache->close();
            Status_network::$cache = null;
        }
    }

    function log($level, $msg)
    {
        common_log($level, get_class($this) . ' ('. $this->get_id() .'): '.$msg);
    }

    function name()
    {
        return strtolower(get_class($this).'.'.$this->get_id());
    }
}


code:
<?php

if (!defined('GNUSOCIAL')) { exit(1); }

/**
 * Backwards compatible class for plugins for GNU social <1.2
 * and thus only have the class StatusNet defined.
 */
class StatusNet
{
    public static function getActivePlugins()
    {
        return GNUsocial::getActivePlugins();
    }

    public static function isHTTPS()
    {
        return GNUsocial::isHTTPS();
    }
}

repo_name: andremichelle/gnu

license: mit


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Abstract class for queue managers
 *
 * 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  QueueManager
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @author    Sarven Capadisli <csarven@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/
 */

require_once 'Stomp.php';
require_once 'Stomp/Exception.php';

class StompQueueManager extends QueueManager
{
    protected $servers;
    protected $username;
    protected $password;
    protected $base;
    protected $control;

    protected $useTransactions;
    protected $useAcks;

    protected $sites = array();
    protected $subscriptions = array();

    protected $cons = array(); // all open connections
    protected $disconnect = array();
    protected $transaction = array();
    protected $transactionCount = array();
    protected $defaultIdx = 0;

    function __construct()
    {
        parent::__construct();
        $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');
        $this->control         = common_config('queue', 'control_channel');
        $this->breakout        = common_config('queue', 'breakout');
        $this->useTransactions = common_config('queue', 'stomp_transactions');
        $this->useAcks         = common_config('queue', 'stomp_acks');
    }

    /**
     * Tell the i/o master we only need a single instance to cover
     * all sites running in this process.
     */
    public static function multiSite()
    {
        return IoManager::INSTANCE_PER_PROCESS;
    }

    /**
     * Optional; ping any running queue handler daemons with a notification
     * such as announcing a new site to handle or requesting clean shutdown.
     * This avoids having to restart all the daemons manually to update configs
     * and such.
     *
     * Currently only relevant for multi-site queue managers such as Stomp.
     *
     * @param string $event event key
     * @param string $param optional parameter to append to key
     * @return boolean success
     */
    public function sendControlSignal($event, $param='')
    {
        $message = $event;
        if ($param != '') {
            $message .= ':' . $param;
        }
        $this->_connect();
        $con = $this->cons[$this->defaultIdx];
        $result = $con->send($this->control,
                             $message,
                             array ('created' => common_sql_now()));
        if ($result) {
            $this->_log(LOG_INFO, "Sent control ping to queue daemons: $message");
            return true;
        } else {
            $this->_log(LOG_ERR, "Failed sending control ping to queue daemons: $message");
            return false;
        }
    }

    /**
     * Saves an object into the queue item table.
     *
     * @param mixed $object
     * @param string $queue
     * @param string $siteNickname optional override to drop into another site's queue
     *
     * @return boolean true on success
     * @throws StompException on connection or send error
     */
    public function enqueue($object, $queue, $siteNickname=null)
    {
        $this->_connect();
        if (common_config('queue', 'stomp_enqueue_on')) {
            // We're trying to force all writes to a single server.
            // WARNING: this might do odd things if that server connection dies.
            $idx = array_search(common_config('queue', 'stomp_enqueue_on'),
                                $this->servers);
            if ($idx === false) {
                common_log(LOG_ERR, 'queue stomp_enqueue_on setting does not match our server list.');
                $idx = $this->defaultIdx;
            }
        } else {
            $idx = $this->defaultIdx;
        }
        return $this->_doEnqueue($object, $queue, $idx, $siteNickname);
    }

    /**
     * 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, $siteNickname=null)
    {
        $rep = $this->logrep($object);
        $envelope = array('site' => $siteNickname ? $siteNickname : common_config('site', 'nickname'),
                          'handler' => $queue,
                          'payload' => $this->encode($object));
        $msg = base64_encode(serialize($envelope));

        $props = array('created' => common_sql_now());
        if ($this->isPersistent($queue)) {
            $props['persistent'] = 'true';
        }

        $con = $this->cons[$idx];
        $host = $con->getServer();
        $target = $this->queueName($queue);
        $result = $con->send($target, $msg, $props);

        if (!$result) {
            $this->_log(LOG_ERR, "Error sending $rep to $queue queue on $host $target");
            return false;
        }

        $this->_log(LOG_DEBUG, "complete remote queueing $rep for $queue on $host $target");
        $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;
        }
    }

    /**
     * Send any sockets we're listening on to the IO manager
     * to wait for input.
     *
     * @return array of resources
     */
    public function getSockets()
    {
        $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");
    }

    /**
     * We've got input to handle on our socket!
     * Read any waiting Stomp frame(s) and process them.
     *
     * @param resource $socket
     * @return boolean ok on success
     */
    public function handleInput($socket)
    {
        $idx = $this->connectionFromSocket($socket);
        $con = $this->cons[$idx];
        $host = $con->getServer();
        $this->defaultIdx = $idx;

        $ok = true;
        try {
            $frames = $con->readFrames();
        } catch (StompException $e) {
            $this->_log(LOG_ERR, "Lost connection to $host: " . $e->getMessage());
            fclose($socket); // ???
            $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)) {
                    // We got a control event that requests a shutdown;
                    // close out and stop handling anything else!
                    break;
                }
            } else {
                $ok = $this->handleItem($frame) && $ok;
            }
            $this->ack($idx, $frame);
            $this->commit($idx);
            $this->begin($idx);
        }
        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... 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.
     *
     * @param IoMaster $master process/event controller
     * @return bool return false on failure
     */
    public function start($master)
    {
        parent::start($master);
        $this->_connectAll();

        foreach ($this->cons as $i => $con) {
            if ($con) {
                $this->doSubscribe($con);
                $this->begin($i);
            }
        }
        return true;
    }

    /**
     * Close out any active connections.
     *
     * @return bool return false on failure
     */
    public function finish()
    {
        // If there are any outstanding delivered messages we haven't processed,
        // free them for another thread to take.
        foreach ($this->cons as $i => $con) {
            if ($con) {
                $this->rollback($i);
                $con->disconnect();
                $this->cons[$i] = null;
            }
        }
        return true;
    }

    /**
     * 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->cons)) {
            $list = $this->servers;
            if (count($list) > 1) {
                shuffle($list); // Randomize to spread load
                $url = 'failover://(' . implode(',', $list) . ')';
            } else {
                $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;
            }
        }
    }

    /**
     * Attempt to manually reconnect to the Stomp server for the given
     * slot. If successful, set up our subscriptions on it.
     */
    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;

            $this->doSubscribe($con);
            $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;
    }

    /**
     * Set up all our raw queue subscriptions on the given connection
     * @param LiberalStomp $con
     */
    protected function doSubscribe(LiberalStomp $con)
    {
        $host = $con->getServer();
        foreach ($this->subscriptions() as $sub) {
            $this->_log(LOG_INFO, "Subscribing to $sub on $host");
            $con->subscribe($sub);
        }
    }

    /**
     * Grab a full list of stomp-side queue subscriptions.
     * Will include:
     *  - control broadcast channel
     *  - shared group queues for active groups
     *  - per-handler and per-site breakouts from $config['queue']['breakout']
     *    that are rooted in the active groups.
     *
     * @return array of strings
     */
    protected function subscriptions()
    {
        $subs = array();
        $subs[] = $this->control;

        foreach ($this->activeGroups as $group) {
            $subs[] = $this->base . $group;
        }

        foreach ($this->breakout as $spec) {
            $parts = explode('/', $spec);
            if (count($parts) < 2 || count($parts) > 3) {
                common_log(LOG_ERR, "Bad queue breakout specifier $spec");
            }
            if (in_array($parts[0], $this->activeGroups)) {
                $subs[] = $this->base . $spec;
            }
        }
        return array_unique($subs);
    }

    /**
     * Handle and acknowledge an event that's come in through a queue.
     *
     * If the queue handler reports failure, the message is requeued for later.
     * Missing notices or handler classes will drop the message.
     *
     * Side effects: in multi-site mode, may reset site configuration to
     * match the site that queued the event.
     *
     * @param StompFrame $frame
     * @return bool success
     */
    protected function handleItem($frame)
    {
        $host = $this->cons[$this->defaultIdx]->getServer();
        $message = unserialize(base64_decode($frame->body));

        if ($message === false) {
            $this->_log(LOG_ERR, "Can't unserialize frame: {$frame->body}");
            $this->_log(LOG_ERR, "Unserializable frame length: " . strlen($frame->body));
            return false;
        }

        $site = $message['site'];
        $queue = $message['handler'];

        if ($this->isDeadLetter($frame, $message)) {
            $this->stats('deadletter', $queue);
	        return false;
        }

        // @fixme detect failing site switches
        $this->switchSite($site);

        try {
            $item = $this->decode($message['payload']);
        } catch (Exception $e) {
            $this->_log(LOG_ERR, "Skipping empty or deleted item in queue $queue from $host");
            $this->stats('baditem', $queue);
            return false;
        }
        $info = $this->logrep($item) . " posted at " .
                $frame->headers['created'] . " in queue $queue from $host";
        $this->_log(LOG_DEBUG, "Dequeued $info");

        try {
            $handler = $this->getHandler($queue);
            $ok = $handler->handle($item);
        } catch (NoQueueHandlerException $e) {
            $this->_log(LOG_ERR, "Missing handler class; skipping $info");
            $this->stats('badhandler', $queue);
            return false;
        } catch (Exception $e) {
            $this->_log(LOG_ERR, "Exception on queue $queue: " . $e->getMessage());
            $ok = false;
        }

        if ($ok) {
            $this->_log(LOG_INFO, "Successfully handled $info");
            $this->stats('handled', $queue);
        } else {
            $this->_log(LOG_WARNING, "Failed handling $info");
            // Requeing moves the item to the end of the line for its next try.
            // @fixme add a manual retry count
            $this->enqueue($item, $queue);
            $this->stats('requeued', $queue);
        }

        return $ok;
    }

    /**
     * Check if a redelivered message has been run through enough
     * that we're going to give up on it.
     *
     * @param StompFrame $frame
     * @param array $message unserialized message body
     * @return boolean true if we should discard
     */
    protected function isDeadLetter($frame, $message)
    {
        if (isset($frame->headers['redelivered']) && $frame->headers['redelivered'] == 'true') {
	        // Message was redelivered, possibly indicating a previous failure.
            $msgId = $frame->headers['message-id'];
            $site = $message['site'];
            $queue = $message['handler'];
	        $msgInfo = "message $msgId for $site in queue $queue";

	        $deliveries = $this->incDeliveryCount($msgId);
	        if ($deliveries > common_config('queue', 'max_retries')) {
		        $info = "DEAD-LETTER FILE: Gave up after retry $deliveries on $msgInfo";

		        $outdir = common_config('queue', 'dead_letter_dir');
		        if ($outdir) {
    		        $filename = $outdir . "/$site-$queue-" . rawurlencode($msgId);
    		        $info .= ": dumping to $filename";
    		        file_put_contents($filename, $message['payload']);
		        }

		        common_log(LOG_ERR, $info);
		        return true;
	        } else {
	            common_log(LOG_INFO, "retry $deliveries on $msgInfo");
	        }
        }
        return false;
    }

    /**
     * Update count of times we've re-encountered this message recently,
     * triggered when we get a message marked as 'redelivered'.
     *
     * Requires a CLI-friendly cache configuration.
     *
     * @param string $msgId message-id header from message
     * @return int number of retries recorded
     */
    function incDeliveryCount($msgId)
    {
	    $count = 0;
	    $cache = Cache::instance();
	    if ($cache) {
		    $key = 'statusnet:stomp:message-retries:' . $msgId;
		    $count = $cache->increment($key);
		    if (!$count) {
			    $count = 1;
			    $cache->set($key, $count, null, 3600);
			    $got = $cache->get($key);
		    }
	    }
	    return $count;
    }

    /**
     * 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($idx, $frame)
    {
        $message = trim($frame->body);
        if (strpos($message, ':') !== false) {
            list($event, $param) = explode(':', $message, 2);
        } else {
            $event = $message;
            $param = '';
        }

        $shutdown = false;

        if ($event == 'shutdown') {
            $this->master->requestShutdown();
            $shutdown = true;
        } else if ($event == 'restart') {
            $this->master->requestRestart();
            $shutdown = true;
        } else if ($event == 'update') {
            $this->updateSiteConfig($param);
        } else {
            $this->_log(LOG_ERR, "Ignoring unrecognized control message: $message");
        }
        return $shutdown;
    }

    /**
     * Switch site, if necessary, and reset current handler assignments
     * @param string $site
     */
    function switchSite($site)
    {
        if ($site != GNUsocial::currentSite()) {
            $this->stats('switch');
            GNUsocial::switchSite($site);
            $this->initialize();
        }
    }

    /**
     * (Re)load runtime configuration for a given site by nickname,
     * triggered by a broadcast to the 'statusnet-control' topic.
     *
     * Configuration changes in database should update, but config
     * files might not.
     *
     * @param array $frame Stomp frame
     * @return bool true to continue; false to stop further processing.
     */
    protected function updateSiteConfig($nickname)
    {
        $sn = Status_network::getKV('nickname', $nickname);
        if ($sn) {
            $this->switchSite($nickname);
            if (!in_array($nickname, $this->sites)) {
                $this->addSite();
            }
            $this->stats('siteupdate');
        } else {
            $this->_log(LOG_ERR, "Ignoring ping for unrecognized new site $nickname");
        }
    }

    /**
     * Combines the queue_basename from configuration with the
     * group name for this queue to give eg:
     *
     * /queue/statusnet/main
     * /queue/statusnet/main/distrib
     * /queue/statusnet/xmpp/xmppout/site01
     *
     * @param string $queue
     * @return string
     */
    protected function queueName($queue)
    {
        $group = $this->queueGroup($queue);
        $site = GNUsocial::currentSite();

        $specs = array("$group/$queue/$site",
                       "$group/$queue");
        foreach ($specs as $spec) {
            if (in_array($spec, $this->breakout)) {
                return $this->base . $spec;
            }
        }
        return $this->base . $group;
    }

    /**
     * Get the breakout mode for the given queue on the current site.
     *
     * @param string $queue
     * @return string one of 'shared', 'handler', 'site'
     */
    protected function breakoutMode($queue)
    {
        $breakout = common_config('queue', 'breakout');
        if (isset($breakout[$queue])) {
            return $breakout[$queue];
        } else if (isset($breakout['*'])) {
            return $breakout['*'];
        } else {
            return 'shared';
        }
    }

    protected function begin($idx)
    {
        if ($this->useTransactions) {
            if (!empty($this->transaction[$idx])) {
                throw new Exception("Tried to start transaction in the middle of a transaction");
            }
            $this->transactionCount[$idx]++;
            $this->transaction[$idx] = $this->master->id . '-' . $this->transactionCount[$idx] . '-' . time();
            $this->cons[$idx]->begin($this->transaction[$idx]);
        }
    }

    protected function ack($idx, $frame)
    {
        if ($this->useAcks) {
            if ($this->useTransactions) {
                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);
            }
        }
    }

    protected function commit($idx)
    {
        if ($this->useTransactions) {
            if (empty($this->transaction[$idx])) {
                throw new Exception("Tried to commit but not in a transaction");
            }
            $this->cons[$idx]->commit($this->transaction[$idx]);
            $this->transaction[$idx] = null;
        }
    }

    protected function rollback($idx)
    {
        if ($this->useTransactions) {
            if (empty($this->transaction[$idx])) {
                throw new Exception("Tried to rollback but not in a transaction");
            }
            $this->cons[$idx]->commit($this->transaction[$idx]);
            $this->transaction[$idx] = null;
        }
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Local navigation for subscriptions group of pages
 *
 * 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  Subs
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2008-2011 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') && !defined('LACONICA')) {
    exit(1);
}

require_once INSTALLDIR.'/lib/widget.php';

/**
 * Local nav menu for subscriptions, subscribers
 *
 * @category Subs
 * @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 SubGroupNav extends Menu
{
    var $user = null;

    /**
     * Construction
     *
     * @param Action $action current action, used for output
     * @param User   $user   Current user or NULL if "guest"
     */
    function __construct(Action $action=null, User $user=null)
    {
        parent::__construct($action);
        $this->user = $user;
    }

    /**
     * Show the menu
     *
     * @return void
     */
    function show()
    {
        $cur = common_current_user();
        $action = $this->action->trimmed('action');

        $this->out->elementStart('ul', array('class' => 'nav'));

        if (Event::handle('StartSubGroupNav', array($this))) {

            $this->out->menuItem(common_local_url('showstream', array('nickname' =>
                                                                      $this->user->nickname)),
                                 // TRANS: Menu item in local navigation menu.
                                 _m('MENU','Profile'),
                                 (empty($profile)) ? $this->user->nickname : $profile->getBestName(),
                                 $action == 'showstream',
                                 'nav_profile');
            $this->out->menuItem(common_local_url('subscriptions',
                                                  array('nickname' =>
                                                        $this->user->nickname)),
                                 // TRANS: Menu item in local navigation menu.
                                 _m('MENU','Subscriptions'),
                                 // TRANS: Menu item title in local navigation menu.
                                 // TRANS: %s is a user nickname.
                                 sprintf(_('People %s subscribes to.'),
                                         $this->user->nickname),
                                 $action == 'subscriptions',
                                 'nav_subscriptions');
            $this->out->menuItem(common_local_url('subscribers',
                                                  array('nickname' =>
                                                        $this->user->nickname)),
                                 // TRANS: Menu item in local navigation menu.
                                 _m('MENU','Subscribers'),
                                 // TRANS: Menu item title in local navigation menu.
                                 // TRANS: %s is a user nickname.
                                 sprintf(_('People subscribed to %s.'),
                                         $this->user->nickname),
                                 $action == 'subscribers',
                                 'nav_subscribers');
            if ($cur && $cur->id == $this->user->id) {
                // Possibly site admins should be able to get in here too
                $pending = $this->countPendingSubs();
                if ($pending || $cur->subscribe_policy == User::SUBSCRIBE_POLICY_MODERATE) {
                    $this->out->menuItem(common_local_url('subqueue',
                                                          array('nickname' =>
                                                                $this->user->nickname)),
                                         // TRANS: Menu item in local navigation menu.
                                         // TRANS: %d is the number of pending subscription requests.
                                         sprintf(_m('MENU','Pending (%d)'), $pending),
                                         // TRANS: Menu item title in local navigation menu.
                                         sprintf(_('Approve pending subscription requests.'),
                                                 $this->user->nickname),
                                         $action == 'subqueueaction',
                                         'nav_subscribers');
                }
            }
            $this->out->menuItem(common_local_url('usergroups',
                                                  array('nickname' =>
                                                        $this->user->nickname)),
                                 // TRANS: Menu item in local navigation menu.
                                 _m('MENU','Groups'),
                                 // TRANS: Menu item title in local navigation menu.
                                 // TRANS: %s is a user nickname.
                                 sprintf(_('Groups %s is a member of.'),
                                         $this->user->nickname),
                                 $action == 'usergroups',
                                 'nav_usergroups');
            $this->out->menuItem(common_local_url('peopletagsubscriptions',
                                                  array('nickname' =>
                                                        $this->user->nickname)),
                                 // TRANS: Menu item title in local navigation menu.
                                 _m('MENU','Lists'),
                                 // TRANS: Menu item title in local navigation menu.
                                 // TRANS: %s is a user nickname.
                                 sprintf(_('List subscriptions by %s.'),
                                         $this->user->nickname),
                                 in_array($action, array('peopletagsbyuser', 'peopletagsubscriptions', 'peopletagsforuser')),
                                 'nav_timeline_peopletags');

            if (common_config('invite', 'enabled') && !is_null($cur) && $this->user->id === $cur->id) {
                $this->out->menuItem(common_local_url('invite'),
                                     // TRANS: Menu item in local navigation menu.
                                     _m('MENU','Invite'),
                                     // TRANS: Menu item title in local navigation menu.
                                     // TRANS: %s is the StatusNet sitename.
                                     sprintf(_('Invite friends and colleagues to join you on %s.'),
                                             common_config('site', 'name')),
                                     $action == 'invite',
                                     'nav_invite');
            }

            Event::handle('EndSubGroupNav', array($this));
        }

        $this->out->elementEnd('ul');
    }

    function countPendingSubs()
    {
        $req = new Subscription_queue();
        $req->subscribed = $this->user->id;
        return $req->count();
    }
}


code:
<?php

if (!defined('GNUSOCIAL')) { exit(1); }

class SubQueueList extends ProfileList
{
    public function newListItem(Profile $target)
    {
        return new SubQueueListItem($target, $this->action);
    }
}


code:
<?php

if (!defined('GNUSOCIAL')) { exit(1); }

class SubQueueListItem extends ProfileListItem
{
    public function showActions()
    {
        $this->startActions();
        if (Event::handle('StartProfileListItemActionElements', array($this))) {
            $this->showApproveButtons();
            Event::handle('EndProfileListItemActionElements', array($this));
        }
        $this->endActions();
    }

    public function showApproveButtons()
    {
        $this->out->elementStart('li', 'entity_approval');
        $form = new ApproveSubForm($this->out, $this->profile);
        $form->show();
        $this->out->elementEnd('li');
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Form for subscribing to a user
 *
 * 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  Form
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @author    Sarven Capadisli <csarven@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') && !defined('LACONICA')) {
    exit(1);
}

require_once INSTALLDIR.'/lib/form.php';

/**
 * Form for subscribing to a user
 *
 * @category Form
 * @package  StatusNet
 * @author   Evan Prodromou <evan@status.net>
 * @author   Sarven Capadisli <csarven@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/
 *
 * @see      UnsubscribeForm
 */
class SubscribeForm extends Form
{
    /**
     * Profile of user to subscribe to
     */
    var $profile = null;

    /**
     * Constructor
     *
     * @param HTMLOutputter $out     output channel
     * @param Profile       $profile profile of user to subscribe to
     */
    function __construct($out=null, $profile=null)
    {
        parent::__construct($out);

        $this->profile = $profile;
    }

    /**
     * ID of the form
     *
     * @return int ID of the form
     */
    function id()
    {
        return 'subscribe-' . $this->profile->id;
    }

    /**
     * class of the form
     *
     * @return string of the form class
     */
    function formClass()
    {
        return 'form_user_subscribe ajax';
    }

    /**
     * Action of the form
     *
     * @return string URL of the action
     */
    function action()
    {
        return common_local_url('subscribe');
    }

    /**
     * Legend of the Form
     *
     * @return void
     */
    function formLegend()
    {
        // TRANS: Form of form to subscribe to a user.
        $this->out->element('legend', null, _('Subscribe to this user'));
    }

    /**
     * Data elements of the form
     *
     * @return void
     */
    function formData()
    {
        $this->out->hidden('subscribeto-' . $this->profile->id,
                           $this->profile->id,
                           'subscribeto');
    }

    /**
     * Action elements
     *
     * @return void
     */
    function formActions()
    {
        // TRANS: Button text to subscribe to a user.
        $this->out->submit('submit', _m('BUTTON','Subscribe'), 'submit', null,
                           // TRANS: Button title to subscribe to a user.
                           _('Subscribe to this user.'));
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Form for subscribing to a peopletag
 *
 * 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  Form
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @author    Shashi Gowda <connect2shashi@gmail.com>
 * @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') && !defined('LACONICA')) {
    exit(1);
}

require_once INSTALLDIR.'/lib/form.php';

/**
 * Form for subscribing to a peopletag
 *
 * @category Form
 * @package  StatusNet
 * @author   Shashi Gowda <connect2shashi@gmail.com>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 *
 * @see      UnsubscribeForm
 */
class SubscribePeopletagForm extends Form
{
    /**
     * peopletag for the user to join
     */
    var $peopletag = null;

    /**
     * Constructor
     *
     * @param HTMLOutputter $out   output channel
     * @param peopletag     $peopletag peopletag to subscribe to
     */
    function __construct($out=null, $peopletag=null)
    {
        parent::__construct($out);

        $this->peopletag = $peopletag;
    }

    /**
     * ID of the form
     *
     * @return string ID of the form
     */
    function id()
    {
        return 'peopletag-subscribe-' . $this->peopletag->id;
    }

    /**
     * class of the form
     *
     * @return string of the form class
     */
    function formClass()
    {
        return 'form_peopletag_subscribe';
    }

    /**
     * Action of the form
     *
     * @return string URL of the action
     */
    function action()
    {
        return common_local_url('subscribepeopletag',
                                array('id' => $this->peopletag->id));
    }

    /**
     * Action elements
     *
     * @return void
     */
    function formActions()
    {
        // TRANS: Button text for subscribing to a list.
        $this->out->submit('submit', _m('BUTTON', 'Subscribe'));

    }
}


code:
<?php

if (!defined('GNUSOCIAL')) { exit(1); }

class SubscribersList extends SubscriptionList
{
    function newListItem(Profile $profile)
    {
        return new SubscribersListItem($profile, $this->owner, $this->action);
    }
}


code:
<?php

if (!defined('GNUSOCIAL')) { exit(1); }

class SubscribersListItem extends SubscriptionListItem
{
    function showActions()
    {
        $this->startActions();
        if (Event::handle('StartProfileListItemActionElements', array($this))) {
            $this->showSubscribeButton();
            // Relevant code!
            $this->showBlockForm();
            Event::handle('EndProfileListItemActionElements', array($this));
        }
        $this->endActions();
    }

    function showBlockForm()
    {
        $user = common_current_user();

        if (!empty($user) && $this->owner->id == $user->id) {
            $returnto = array('action' => 'subscribers',
                              'nickname' => $this->owner->getNickname());
            $page = $this->out->arg('page');
            if ($page) {
                $returnto['param-page'] = $page;
            }
            $bf = new BlockForm($this->out, $this->profile, $returnto);
            $bf->show();
        }
    }

    function linkAttributes()
    {
        $aAttrs = parent::linkAttributes();

        if (common_config('nofollow', 'subscribers')) {
            $aAttrs['rel'] .= ' nofollow';
        }

        return $aAttrs;
    }

    function homepageAttributes()
    {
        $aAttrs = parent::linkAttributes();

        if (common_config('nofollow', 'subscribers')) {
            $aAttrs['rel'] = 'nofollow';
        }

        return $aAttrs;
    }
}


code:
<?php

if (!defined('GNUSOCIAL')) { exit(1); }

class SubscribersMiniList extends ProfileMiniList
{
    public function newListItem(Profile $profile)
    {
        return new SubscribersMiniListItem($profile, $this->action);
    }
}


code:
<?php

if (!defined('GNUSOCIAL')) { exit(1); }

class SubscribersMiniListItem extends ProfileMiniListItem
{
    function linkAttributes()
    {
        $aAttrs = parent::linkAttributes();
        if (common_config('nofollow', 'subscribers')) {
            $aAttrs['rel'] .= ' nofollow';
        }
        return $aAttrs;
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Widget to show a list of profiles
 *
 * 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  Public
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2008-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('GNUSOCIAL')) { exit(1); }

/**
 * Widget to show a list of subscriptions
 *
 * @category Public
 * @package  StatusNet
 * @author   Zach Copley <zach@status.net>
 * @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 SubscriptionList extends ProfileList
{
    /** Owner of this list */
    var $owner = null;

    public function __construct($profile, $owner = null, $action = null)
    {
        parent::__construct($profile, $action);

        $this->owner = $owner;
    }

    public function newListItem(Profile $profile)
    {
        return new SubscriptionListItem($profile, $this->owner, $this->action);
    }
}


code:
<?php

if (!defined('GNUSOCIAL')) { exit(1); }

class SubscriptionListItem extends ProfileListItem
{
    /** Owner of this list */
    var $owner = null;

    // FIXME: TagSubs plugin sends a TagSub here, but should send a Profile and handle TagSub specifics itself?
    function __construct($target, $owner, HTMLOutputter $action)
    {
        if ($owner instanceof Profile) {
            parent::__construct($target, $action, $owner);
        } else {
            parent::__construct($target, $action);
        }

        $this->owner = $owner;
    }

    function showProfile()
    {
        $this->startProfile();
        $this->showAvatar($this->profile);
        $this->showNickname();
        $this->showFullName();
        $this->showLocation();
        $this->showHomepage();
        $this->showBio();
        // Relevant portion!
        $this->showTags();
        if ($this->isOwn()) {
            $this->showOwnerControls();
        }
        $this->endProfile();
    }

    function showOwnerControls()
    {
        // pass
    }

    function isOwn()
    {
        $user = common_current_user();
        return (!empty($user) && ($this->owner->id == $user->id));
    }
}


code:
<?php

if (!defined('GNUSOCIAL')) { exit(1); }

// XXX SubscriptionsList and SubscriptionList are dangerously close

class SubscriptionsList extends SubscriptionList
{
    function newListItem(Profile $profile)
    {
        return new SubscriptionsListItem($profile, $this->owner, $this->action);
    }
}


code:
<?php

if (!defined('GNUSOCIAL')) { exit(1); }

class SubscriptionsListItem extends SubscriptionListItem
{
    function showOwnerControls()
    {
        $sub = Subscription::pkeyGet(array('subscriber' => $this->owner->id,
                                           'subscribed' => $this->profile->id));
        if (!$sub) {
            return;
        }

        $transports = array();
        Event::handle('GetImTransports', array(&$transports));
        if (!$transports && !common_config('sms', 'enabled')) {
            return;
        }

        $this->out->elementStart('form', array('id' => 'subedit-' . $this->profile->id,
                                          'method' => 'post',
                                          'class' => 'form_subscription_edit',
                                          'action' => common_local_url('subedit')));
        $this->out->hidden('token', common_session_token());
        $this->out->hidden('profile', $this->profile->id);
        if ($transports) {
            $attrs = array('name' => 'jabber',
                           'type' => 'checkbox',
                           'class' => 'checkbox',
                           'id' => 'jabber-'.$this->profile->id);
            if ($sub->jabber) {
                $attrs['checked'] = 'checked';
            }

            $this->out->element('input', $attrs);
            // TRANS: Checkbox label for enabling IM messages for a profile in a subscriptions list.
            $this->out->element('label', array('for' => 'jabber-'.$this->profile->id), _m('LABEL','IM'));
        } else {
            $this->out->hidden('jabber', $sub->jabber);
        }
        if (common_config('sms', 'enabled')) {
            $attrs = array('name' => 'sms',
                           'type' => 'checkbox',
                           'class' => 'checkbox',
                           'id' => 'sms-'.$this->profile->id);
            if ($sub->sms) {
                $attrs['checked'] = 'checked';
            }

            $this->out->element('input', $attrs);
            // TRANS: Checkbox label for enabling SMS messages for a profile in a subscriptions list.
            $this->out->element('label', array('for' => 'sms-'.$this->profile->id), _('SMS'));
        } else {
            $this->out->hidden('sms', $sub->sms);
        }
        // TRANS: Save button for settings for a profile in a subscriptions list.
        $this->out->submit('save', _m('BUTTON','Save'));
        $this->out->elementEnd('form');
    }
}


code:
<?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);
}

/**
 * A class encapsulating the structure of a table.
 *
 * @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 TableDef
{
    /** name of the table */
    public $name;
    /** array of ColumnDef objects for the columns. */
    public $columns;

    /**
     * Constructor.
     *
     * @param string  $name     name of the table
     * @param array   $columns  columns in the table
     */

    function __construct($name=null,$columns=null)
    {
        $this->name = $name;
        $this->columns = $columns;
    }
}


code:
<?php
/**
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2011, StatusNet, Inc.
 *
 * Stream of notices by a profile with a given tag
 *
 * 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  Stream
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2011 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */

if (!defined('GNUSOCIAL')) { exit(1); }

/**
 * Stream of notices with a given profile and tag
 *
 * @category  Stream
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2011 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */

class TaggedProfileNoticeStream extends ScopingNoticeStream
{
    function __construct($profile, $tag, Profile $scoped=null)
    {
        parent::__construct(new CachingNoticeStream(new RawTaggedProfileNoticeStream($profile, $tag),
                                                    'profile:notice_ids_tagged:'.$profile->id.':'.Cache::keyize($tag)),
                            $scoped);
    }
}

/**
 * Raw stream of notices with a given profile and tag
 *
 * @category  Stream
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2011 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */

class RawTaggedProfileNoticeStream extends NoticeStream
{
    protected $profile;
    protected $tag;

    function __construct($profile, $tag)
    {
        $this->profile = $profile;
        $this->tag     = $tag;
    }

    function getNoticeIds($offset, $limit, $since_id, $max_id)
    {
        // XXX It would be nice to do this without a join
        // (necessary to do it efficiently on accounts with long history)

        $notice = new Notice();

        $query =
          "select id from notice join notice_tag on id=notice_id where tag='".
          $notice->escape($this->tag) .
          "' and profile_id=" . intval($this->profile->id);

        $since = Notice::whereSinceId($since_id, 'id', 'notice.created');
        if ($since) {
            $query .= " and ($since)";
        }

        $max = Notice::whereMaxId($max_id, 'id', 'notice.created');
        if ($max) {
            $query .= " and ($max)";
        }

        $query .= ' order by notice.created DESC, id DESC';

        if (!is_null($offset)) {
            $query .= " LIMIT " . intval($limit) . " OFFSET " . intval($offset);
        }

        $notice->query($query);

        $ids = array();

        while ($notice->fetch()) {
            $ids[] = $notice->id;
        }

        return $ids;
    }
}


code:
<?php
/**
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2011, StatusNet, Inc.
 *
 * Stream of notices with a given tag
 *
 * 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  Stream
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2011 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */

if (!defined('GNUSOCIAL')) { exit(1); }

/**
 * Stream of notices with a given tag
 *
 * @category  Stream
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2011 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */

class TagNoticeStream extends ScopingNoticeStream
{
    function __construct($tag, Profile $scoped=null)
    {
        parent::__construct(new CachingNoticeStream(new RawTagNoticeStream($tag),
                                                    'notice_tag:notice_ids:' . Cache::keyize($tag)),
                            $scoped);
    }
}

/**
 * Raw stream of notices with a given tag
 *
 * @category  Stream
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2011 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */

class RawTagNoticeStream extends NoticeStream
{
    protected $tag;

    function __construct($tag)
    {
        $this->tag = $tag;
    }

    function getNoticeIds($offset, $limit, $since_id, $max_id)
    {
        $nt = new Notice_tag();

        $nt->tag = $this->tag;

        $nt->selectAdd();
        $nt->selectAdd('notice_id');

        Notice::addWhereSinceId($nt, $since_id, 'notice_id');
        Notice::addWhereMaxId($nt, $max_id, 'notice_id');

        if (!empty($this->selectVerbs)) {
            $nt->joinAdd(array('notice_id', 'notice:id'));

            $filter = array_keys(array_filter($this->selectVerbs));
            if (!empty($filter)) {
                // include verbs in selectVerbs with values that equate to true
                $nt->whereAddIn('notice.verb', $filter, 'string');
            }

            $filter = array_keys(array_filter($this->selectVerbs, function ($v) { return !$v; }));
            if (!empty($filter)) {
                // exclude verbs in selectVerbs with values that equate to false
                $nt->whereAddIn('!notice.verb', $filter, 'string');
            }
        }

        $nt->orderBy('notice.created DESC, notice_id DESC');

        if (!is_null($offset)) {
            $nt->limit($offset, $limit);
        }

        $ids = array();

        if ($nt->find()) {
            while ($nt->fetch()) {
                $ids[] = $nt->notice_id;
            }
        }

        return $ids;
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Form for tagging a profile with a peopletag
 *
 * 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  Form
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @author    Sarven Capadisli <csarven@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('GNUSOCIAL')) { exit(1); }

/**
 * Form for tagging a profile with a peopletag
 *
 * @category Form
 * @package  StatusNet
 * @author   Evan Prodromou <evan@status.net>
 * @author   Sarven Capadisli <csarven@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/
 *
 * @see      HTMLOutputter
 */
class TagProfileForm extends Form
{
    protected $target = null;

    /**
     * Constructor
     *
     * @param Action $action  Action we're being embedded into
     * @param array  $options Array of optional parameters
     *                        'user' a user instead of current
     *                        'content' notice content
     *                        'inreplyto' ID of notice to reply to
     *                        'lat' Latitude
     *                        'lon' Longitude
     *                        'location_id' ID of location
     *                        'location_ns' Namespace of location
     */
    function __construct(Action $action, Profile $target)
    {
        parent::__construct($action);

        $this->target = $target;
    }

    /**
     * ID of the form
     *
     * @return string ID of the form
     */

    function id()
    {
        return 'form_tagprofile_'.$target->id;
    }

   /**
     * Class of the form
     *
     * @return string class of the form
     */

    function formClass()
    {
        return 'form_tagprofile form_settings';
    }

    /**
     * Action of the form
     *
     * @return string URL of the action
     */

    function action()
    {
        return common_local_url('tagprofile', array('id' => $this->target->id));
    }

    /**
     * Legend of the Form
     *
     * @return void
     */
    function formLegend()
    {
        // TRANS: Form legend for notice form.
        $this->out->element('legend', null, sprintf(_m('ADDTOLIST', 'List %s'), $this->target->getNickname()));

    }

    /**
     * Data elements
     *
     * @return void
     */
    function formData()
    {
        if (Event::handle('StartShowTagProfileFormData', array($this))) {
            $this->hidden('token', common_session_token());
            $this->hidden('id', $this->target->id);

            $this->elementStart('ul', 'form_data');
            $this->elementStart('li');

            $tags = Profile_tag::getTagsArray($this->out->getScoped()->id, $this->target->id, $this->out->getScoped()->id);
            // TRANS: Field label on list form.
            $this->input('tags', _m('LABEL','Lists'),
                         ($this->arg('tags')) ? $this->arg('tags') : implode(' ', $tags),
                         // TRANS: Field title on list form.
                         _('Lists for this user (letters, numbers, -, ., and _), comma- or space- separated.'));
            $this->elementEnd('li');
            $this->elementEnd('ul');
            Event::handle('EndShowTagProfileFormData', array($this));
        }
    }

    function formActions()
    {
        // TRANS: Button text to save lists.
        $this->out->submit('save', _m('BUTTON', 'Save'));
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Utility for creating new tag: URIs
 *
 * 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  URI
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2010 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
 * @link      http://status.net/
 */

if (!defined('STATUSNET')) {
    exit(1);
}

/**
 * Mint tag: URIs
 *
 * tag: URIs are unique identifiers according to http://tools.ietf.org/html/rfc4151.
 *
 * We use them for creating URIs for things that can't be HTTP retrieved.
 *
 * @category URI
 * @package  StatusNet
 * @author   Evan Prodromou <evan@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
 * @link     http://status.net/
 */

class TagURI
{
    /**
     * Return the base part of a tag URI
     *
     * Note: use mint() instead.
     *
     * @return string Tag URI base to use
     */

    static function base()
    {
        $base = common_config('integration', 'taguri');

        if (empty($base)) {

            $base = common_config('site', 'server').','.date('Y-m-d');

            $pathPart = trim(common_config('site', 'path'), '/');

            if (!empty($pathPart)) {
                $base .= ':'.str_replace('/', ':', $pathPart);
            }
        }

        return $base;
    }

    /**
     * Make a new tag URI
     *
     * Builds the proper base and creates all the parts
     *
     * @return string minted URI
     */

    static function mint()
    {
        $base = self::base();

        $args = func_get_args();

        $format = array_shift($args);

        $extra = vsprintf($format, $args);

        return 'tag:'.$base.':'.$extra;
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Base class for RSS 1.0 feed actions
 *
 * 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  Mail
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @author    Earle Martin <earle@downlode.org>
 * @copyright 2008-9 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('GNUSOCIAL')) { exit(1); }

class TargetedRss10Action extends Rss10Action
{
    protected $target = null;

    protected function doStreamPreparation()
    {
        $this->target = User::getByNickname($this->trimmed('nickname'))->getProfile();
    }

    public function getTarget()
    {
        return $this->target;
    }

    function getImage()
    {
        return $this->target->avatarUrl(AVATAR_PROFILE_SIZE);
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Utilities for theme files and paths
 *
 * 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  Paths
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @author    Sarven Capadisli <csarven@status.net>
 * @copyright 2008-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') && !defined('LACONICA')) {
    exit(1);
}

/**
 * Class for querying and manipulating a theme
 *
 * Themes are directories with some expected sub-directories and files
 * in them. They're found in either local/theme (for locally-installed themes)
 * or theme/ subdir of installation dir.
 *
 * Note that the 'local' directory can be overridden as $config['local']['path']
 * and $config['local']['dir'] etc.
 *
 * This used to be a couple of functions, but for various reasons it's nice
 * to have a class instead.
 *
 * @category Output
 * @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 Theme
{
    const FALLBACK = 'neo';

    var $name = null;
    var $dir  = null;
    var $path = null;
    protected $metadata = null; // access via getMetadata() lazy-loader
    protected $externals = null;
    protected $deps = null;

    /**
     * Constructor
     *
     * Determines the proper directory and path for this theme.
     *
     * @param string $name Name of the theme; defaults to config value
     * @throws ServerException
     */
    function __construct($name = null)
    {
        if (empty($name)) {
            $name = common_config('site', 'theme');
        }
        if (!self::validName($name)) {
            // TRANS: Server exception displayed if a theme name was invalid.
            throw new ServerException(_('Invalid theme name.'));
        }
        $this->name = $name;

        // Check to see if it's in the local dir

        $localroot = self::localRoot();

        $fulldir = $localroot.'/'.$name;

        if (file_exists($fulldir) && is_dir($fulldir)) {
            $this->dir  = $fulldir;
            $this->path = $this->relativeThemePath('local', 'local', 'theme/' . $name);
            return;
        }

        // Check to see if it's in the distribution dir

        $instroot = self::installRoot();

        $fulldir = $instroot.'/'.$name;

        if (file_exists($fulldir) && is_dir($fulldir)) {
            $this->dir = $fulldir;
            $this->path = $this->relativeThemePath('theme', 'theme', $name);
            return;
        }

        // Ruh roh. Fall back to default, then.

        common_log(LOG_WARNING, sprintf("Unable to find theme '%s', falling back to default theme '%s'",
                                        $name,
                                        Theme::FALLBACK));

        $this->name = Theme::FALLBACK;
        $this->dir  = $instroot.'/'.Theme::FALLBACK;
        $this->path = $this->relativeThemePath('theme', 'theme', Theme::FALLBACK);

    }

    /**
     * Build a full URL to the given theme's base directory, possibly
     * using an offsite theme server path.
     *
     * @param string $group configuration section name to pull paths from
     * @param string $fallbackSubdir default subdirectory under PUBLICDIR
     * @param string $name theme name
     *
     * @return string URL
     *
     * @todo consolidate code with that for other customizable paths
     */
    protected function relativeThemePath($group, $fallbackSubdir, $name)
    {
        if (GNUsocial::isHTTPS()) {
            $sslserver = common_config($group, 'sslserver');

            if (empty($sslserver)) {
                if (is_string(common_config('site', 'sslserver')) &&
                    mb_strlen(common_config('site', 'sslserver')) > 0) {
                    $server = common_config('site', 'sslserver');
                } else if (common_config('site', 'server')) {
                    $server = common_config('site', 'server');
                }
                $path   = common_config('site', 'path') . '/';
                if ($fallbackSubdir) {
                    $path .= $fallbackSubdir . '/';
                }
            } else {
                $server = $sslserver;
                $path   = common_config($group, 'sslpath');
                if (empty($path)) {
                    $path = common_config($group, 'path');
                }
            }

            $protocol = 'https';
        } else {
            $path = common_config($group, 'path');

            if (empty($path)) {
                $path = common_config('site', 'path') . '/';
                if ($fallbackSubdir) {
                    $path .= $fallbackSubdir . '/';
                }
            }

            $server = common_config($group, 'server');

            if (empty($server)) {
                $server = common_config('site', 'server');
            }

            $protocol = 'http';
        }

        if ($path[strlen($path)-1] != '/') {
            $path .= '/';
        }

        if ($path[0] != '/') {
            $path = '/'.$path;
        }

        return $protocol.'://'.$server.$path.$name;
    }

    /**
     * Gets the full local filename of a file in this theme.
     *
     * @param string $relative relative name, like 'logo.png'
     *
     * @return string full pathname, like /var/www/mublog/theme/default/logo.png
     */
    function getFile($relative)
    {
        return $this->dir.'/'.$relative;
    }

    /**
     * Gets the full HTTP url of a file in this theme
     *
     * @param string $relative relative name, like 'logo.png'
     *
     * @return string full URL, like 'http://example.com/theme/default/logo.png'
     */
    function getPath($relative)
    {
        return $this->path.'/'.$relative;
    }

    /**
     * Fetch a list of other themes whose CSS needs to be pulled in before
     * this theme's, based on following the theme.ini 'include' settings.
     * (May be empty if this theme has no include dependencies.)
     *
     * @return array of strings with theme names
     */
    function getDeps()
    {
        if ($this->deps === null) {
            $chain = $this->doGetDeps(array($this->name));
            array_pop($chain); // Drop us back off
            $this->deps = $chain;
        }
        return $this->deps;
    }

    protected function doGetDeps($chain)
    {
        $data = $this->getMetadata();
        if (!empty($data['include'])) {
            $include = $data['include'];

            // Protect against cycles!
            if (!in_array($include, $chain)) {
                try {
                    $theme = new Theme($include);
                    array_unshift($chain, $include);
                    return $theme->doGetDeps($chain);
                } catch (Exception $e) {
                    common_log(LOG_ERR,
                            "Exception while fetching theme dependencies " .
                            "for $this->name: " . $e->getMessage());
                }
            }
        }
        return $chain;
    }

    /**
     * Pull data from the theme's theme.ini file.
     * @fixme calling getFile will fall back to default theme, this may be unsafe.
     *
     * @return array associative of strings
     */
    function getMetadata()
    {
        if ($this->metadata == null) {
            $this->metadata = $this->doGetMetadata();
        }
        return $this->metadata;
    }

    /**
     * Pull data from the theme's theme.ini file.
     * @fixme calling getFile will fall back to default theme, this may be unsafe.
     *
     * @return array associative of strings
     */
    private function doGetMetadata()
    {
        $iniFile = $this->getFile('theme.ini');
        if (file_exists($iniFile)) {
            return parse_ini_file($iniFile);
        } else {
            return [];
        }
    }

    /**
     * Get list of any external URLs required by this theme and any
     * dependencies. These are lazy-loaded from theme.ini.
     *
     * @return array of URL strings
     * @throws ServerException
     */
    function getExternals()
    {
        if ($this->externals == null) {
            $data = $this->getMetadata();
            if (!empty($data['external'])) {
                $ext = (array)$data['external'];
            } else {
                $ext = array();
            }

            if (!empty($data['include'])) {
                $theme = new Theme($data['include']);
                $ext = array_merge($ext, $theme->getExternals());
            }

            $this->externals = array_unique($ext);
        }
        return $this->externals;
    }

    /**
     * Gets the full path of a file in a theme dir based on its relative name
     *
     * @param string $relative relative path within the theme directory
     * @param string $name name of the theme; defaults to current theme
     *
     * @return string File path to the theme file
     * @throws ServerException
     */
    static function file($relative, $name=null)
    {
        $theme = new Theme($name);
        return $theme->getFile($relative);
    }

    /**
     * Gets the full URL of a file in a theme dir based on its relative name
     *
     * @param string $relative relative path within the theme directory
     * @param string $name name of the theme; defaults to current theme
     *
     * @return string URL of the file
     * @throws ServerException
     */
    static function path($relative, $name=null)
    {
        $theme = new Theme($name);
        return $theme->getPath($relative);
    }

    /**
     * list available theme names
     *
     * @return array list of available theme names
     */
    static function listAvailable()
    {
        $local   = self::subdirsOf(self::localRoot());
        $install = self::subdirsOf(self::installRoot());

        $i = array_search('base', $install);

        unset($install[$i]);

        return array_merge($local, $install);
    }

    /**
     * Utility for getting subdirs of a directory
     *
     * @param string $dir full path to directory to check
     *
     * @return array relative filenames of subdirs, or empty array
     */
    protected static function subdirsOf($dir)
    {
        $subdirs = array();

        if (is_dir($dir)) {
            if ($dh = opendir($dir)) {
                while (($filename = readdir($dh)) !== false) {
                    if ($filename != '..' && $filename !== '.' &&
                        is_dir($dir.'/'.$filename)) {
                        $subdirs[] = $filename;
                    }
                }
                closedir($dh);
            }
        }

        return $subdirs;
    }

    /**
     * Local root dir for themes
     *
     * @return string
     */
    protected static function localRoot()
    {
        $basedir = common_config('local', 'dir');

        if (empty($basedir)) {
            $basedir = PUBLICDIR . '/local';
        }

        return $basedir . '/theme';
    }

    /**
     * Root dir for themes that are shipped with GNU social
     *
     * @return string
     */
    protected static function installRoot()
    {
        $instroot = common_config('theme', 'dir');

        if (empty($instroot)) {
            $instroot = PUBLICDIR.'/theme';
        }

        return $instroot;
    }

    static function validName($name)
    {
        return preg_match('/^[a-z0-9][a-z0-9_-]*$/i', $name);
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Utilities for theme files and paths
 *
 * 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  Paths
 * @package   StatusNet
 * @author    Brion Vibber <brion@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') && !defined('LACONICA')) {
    exit(1);
}

/**
 * Encapsulation of the validation-and-save process when dealing with
 * a user-uploaded StatusNet theme archive...
 *
 * @todo extract theme metadata from css/display.css
 * @todo allow saving multiple themes
 */
class ThemeUploader
{
    protected $sourceFile;
    protected $isUpload;
    private $prevErrorReporting;

    public function __construct($filename)
    {
        if (!class_exists('ZipArchive')) {
            // TRANS: Exception thrown when a compressed theme is uploaded while no support present in PHP configuration.
            throw new Exception(_('This server cannot handle theme uploads without ZIP support.'));
        }
        $this->sourceFile = $filename;
    }

    public static function fromUpload($name)
    {
        if (!isset($_FILES[$name]['error'])) {
            // TRANS: Server exception thrown when uploading a theme fails.
            throw new ServerException(_('The theme file is missing or the upload failed.'));
        }
        if ($_FILES[$name]['error'] != UPLOAD_ERR_OK) {
            // TRANS: Server exception thrown when uploading a theme fails.
            throw new ServerException(_('The theme file is missing or the upload failed.'));
        }
        return new ThemeUploader($_FILES[$name]['tmp_name']);
    }

    /**
     * @param string $destDir
     * @throws Exception on bogus files
     */
    public function extract($destDir)
    {
        $zip = $this->openArchive();

        // First pass: validate but don't save anything to disk.
        // Any errors will trip an exception.
        $this->traverseArchive($zip);

        // Second pass: now that we know we're good, actually extract!
        $tmpDir = $destDir . '.tmp' . getmypid();
        $this->traverseArchive($zip, $tmpDir);

        $zip->close();

        if (file_exists($destDir)) {
            $killDir = $tmpDir . '.old';
            $this->quiet();
            $ok = rename($destDir, $killDir);
            $this->loud();
            if (!$ok) {
                common_log(LOG_ERR, "Could not move old custom theme from $destDir to $killDir");
                // TRANS: Server exception thrown when saving an uploaded theme after decompressing it fails.
                throw new ServerException(_('Failed saving theme.'));
            }
        } else {
            $killDir = false;
        }

        $this->quiet();
        $ok = rename($tmpDir, $destDir);
        $this->loud();
        if (!$ok) {
            common_log(LOG_ERR, "Could not move saved theme from $tmpDir to $destDir");
            // TRANS: Server exception thrown when saving an uploaded theme after decompressing it fails.
            throw new ServerException(_('Failed saving theme.'));
        }

        if ($killDir) {
            $this->recursiveRmdir($killDir);
        }
    }

    /**
     *
     */
    protected function traverseArchive($zip, $outdir=false)
    {
        $sizeLimit = 2 * 1024 * 1024; // 2 megabyte space limit?
        $blockSize = 4096; // estimated; any entry probably takes this much space

        $totalSize = 0;
        $hasMain = false;
        $commonBaseDir = false;

        for ($i = 0; $i < $zip->numFiles; $i++) {
            $data = $zip->statIndex($i);
            $name = str_replace('\\', '/', $data['name']);

            if (substr($name, -1) == '/') {
                // A raw directory... skip!
                continue;
            }

            // Is this a safe or skippable file?
            $path = pathinfo($name);
            if ($this->skippable($path['filename'], $path['extension'])) {
                // Documentation and such... booooring
                continue;
            } else {
                $this->validateFile($path['filename'], $path['extension']);
            }

            // Check the directory structure...
            $dirs = explode('/', $path['dirname']);
            $baseDir = array_shift($dirs);
            if ($commonBaseDir === false) {
                $commonBaseDir = $baseDir;
            } else {
                if ($commonBaseDir != $baseDir) {
                    // TRANS: Server exception thrown when an uploaded theme has an incorrect structure.
                    throw new ClientException(_('Invalid theme: Bad directory structure.'));
                }
            }

            foreach ($dirs as $dir) {
                $this->validateFileOrFolder($dir);
            }

            $fullPath = $dirs;
            $fullPath[] = $path['basename'];
            $localFile = implode('/', $fullPath);
            if ($localFile == 'css/display.css') {
                $hasMain = true;
            }

            $size = $data['size'];
            $estSize = $blockSize * max(1, intval(ceil($size / $blockSize)));
            $totalSize += $estSize;
            if ($totalSize > $sizeLimit) {
                // TRANS: Client exception thrown when an uploaded theme is larger than the limit.
                // TRANS: %d is the number of bytes of the uncompressed theme.
                $msg = sprintf(_m('Uploaded theme is too large; must be less than %d byte uncompressed.',
                                  'Uploaded theme is too large; must be less than %d bytes uncompressed.',
                                  $sizeLimit),
                               $sizeLimit);
                throw new ClientException($msg);
            }

            if ($outdir) {
                $this->extractFile($zip, $data['name'], "$outdir/$localFile");
            }
        }

        if (!$hasMain) {
            // TRANS: Server exception thrown when an uploaded theme is incomplete.
            throw new ClientException(_('Invalid theme archive: ' .
                                        "Missing file css/display.css"));
        }
    }

    /**
     * @fixme Probably most unrecognized files should just be skipped...
     */
    protected function skippable($filename, $ext)
    {
        $skip = array('txt', 'html', 'rtf', 'doc', 'docx', 'odt', 'xcf');
        if (strtolower($filename) == 'readme') {
            return true;
        }
        if (in_array(strtolower($ext), $skip)) {
            return true;
        }
        if ($filename == '' || substr($filename, 0, 1) == '.') {
            // Skip Unix-style hidden files
            return true;
        }
        if ($filename == '__MACOSX') {
            // Skip awful metadata files Mac OS X slips in for you.
            // Thanks Apple!
            return true;
        }
        return false;
    }

    protected function validateFile($filename, $ext)
    {
        $this->validateFileOrFolder($filename);
        $this->validateExtension($filename, $ext);
        // @fixme validate content
    }

    protected function validateFileOrFolder($name)
    {
        if (!preg_match('/^[a-z0-9_\.-]+$/i', $name)) {
            common_log(LOG_ERR, "Bad theme filename: $name");
            // TRANS: Server exception thrown when an uploaded theme has an incorrect file or folder name.
            $msg = _("Theme contains invalid file or folder name. " .
                     'Stick with ASCII letters, digits, underscore, and minus sign.');
            throw new ClientException($msg);
        }
        if (preg_match('/\.(php|cgi|asp|aspx|js|vb)\w/i', $name)) {
            common_log(LOG_ERR, "Unsafe theme filename: $name");
            // TRANS: Server exception thrown when an uploaded theme contains files with unsafe file extensions.
            $msg = _('Theme contains unsafe file extension names; may be unsafe.');
            throw new ClientException($msg);
        }
        return true;
    }

    protected function validateExtension($base, $ext)
    {
        $allowed = array('css', // CSS may need validation
                         'png', 'gif', 'jpg', 'jpeg',
                         'svg', // SVG images/fonts may need validation
                         'ttf', 'eot', 'woff');
        if (!in_array(strtolower($ext), $allowed)) {
            if ($ext == 'ini' && $base == 'theme') {
                // theme.ini exception
                return true;
            }
            // TRANS: Server exception thrown when an uploaded theme contains a file type that is not allowed.
            // TRANS: %s is the file type that is not allowed.
            $msg = sprintf(_('Theme contains file of type ".%s", which is not allowed.'),
                           $ext);
            throw new ClientException($msg);
        }
        return true;
    }

    /**
     * @return ZipArchive
     */
    protected function openArchive()
    {
        $zip = new ZipArchive;
        $ok = $zip->open($this->sourceFile);
        if ($ok !== true) {
            common_log(LOG_ERR, "Error opening theme zip archive: " .
                                "{$this->sourceFile} code: {$ok}");
            // TRANS: Server exception thrown when an uploaded compressed theme cannot be opened.
            throw new Exception(_('Error opening theme archive.'));
        }
        return $zip;
    }

    /**
     * @param ZipArchive $zip
     * @param string $from original path inside ZIP archive
     * @param string $to final destination path in filesystem
     */
    protected function extractFile($zip, $from, $to)
    {
        $dir = dirname($to);
        if (!file_exists($dir)) {
            $this->quiet();
            $ok = mkdir($dir, 0755, true);
            $this->loud();
            if (!$ok) {
                common_log(LOG_ERR, "Failed to mkdir $dir while uploading theme");
                // TRANS: Server exception thrown when an uploaded theme cannot be saved during extraction.
                throw new ServerException(_('Failed saving theme.'));
            }
        } else if (!is_dir($dir)) {
            common_log(LOG_ERR, "Output directory $dir not a directory while uploading theme");
            // TRANS: Server exception thrown when an uploaded theme cannot be saved during extraction.
            throw new ServerException(_('Failed saving theme.'));
        }

        // ZipArchive::extractTo would be easier, but won't let us alter
        // the directory structure.
        $in = $zip->getStream($from);
        if (!$in) {
            common_log(LOG_ERR, "Couldn't open archived file $from while uploading theme");
            // TRANS: Server exception thrown when an uploaded theme cannot be saved during extraction.
            throw new ServerException(_('Failed saving theme.'));
        }
        $this->quiet();
        $out = fopen($to, "wb");
        $this->loud();
        if (!$out) {
            common_log(LOG_ERR, "Couldn't open output file $to while uploading theme");
            // TRANS: Server exception thrown when an uploaded theme cannot be saved during extraction.
            throw new ServerException(_('Failed saving theme.'));
        }
        while (!feof($in)) {
            $buffer = fread($in, 65536);
            fwrite($out, $buffer);
        }
        fclose($in);
        fclose($out);
    }

    private function quiet()
    {
        $this->prevErrorReporting = error_reporting();
        error_reporting($this->prevErrorReporting & ~E_WARNING);
    }

    private function loud()
    {
        error_reporting($this->prevErrorReporting);
    }

    private function recursiveRmdir($dir)
    {
        $list = dir($dir);
        while (($file = $list->read()) !== false) {
            if ($file == '.' || $file == '..') {
                continue;
            }
            $full = "$dir/$file";
            if (is_dir($full)) {
                $this->recursiveRmdir($full);
            } else {
                unlink($full);
            }
        }
        $list->close();
        rmdir($dir);
    }
}