Horde core framework package import into Git from CVS HEAD
authorMichael M Slusarz <slusarz@curecanti.org>
Thu, 9 Jul 2009 03:00:36 +0000 (21:00 -0600)
committerMichael M Slusarz <slusarz@curecanti.org>
Thu, 9 Jul 2009 05:54:34 +0000 (23:54 -0600)
Named core framework package Core - makes more sense to me then
framework since technically, Core is part of the framework.
Also, split out Release into a separate package since 99.9% of users
don't need this package.

16 files changed:
framework/Core/lib/Horde/Horde.php [new file with mode: 0644]
framework/Core/lib/Horde/Horde/Config.php [new file with mode: 0644]
framework/Core/lib/Horde/Horde/ErrorHandler.php [new file with mode: 0644]
framework/Core/lib/Horde/Horde/Exception.php [new file with mode: 0644]
framework/Core/lib/Horde/Horde/Help.php [new file with mode: 0644]
framework/Core/lib/Horde/Horde/Menu.php [new file with mode: 0644]
framework/Core/lib/Horde/Horde/Registry.php [new file with mode: 0644]
framework/Core/lib/Horde/Horde/Registry/Caller.php [new file with mode: 0644]
framework/Core/lib/Horde/Horde/Release.php [new file with mode: 0644]
framework/Core/lib/Horde/Horde/Release/Whups.php [new file with mode: 0644]
framework/Core/lib/Horde/Horde/Script/Files.php [new file with mode: 0644]
framework/Core/package.xml [new file with mode: 0644]
framework/Core/test/Horde/Framework/url.phpt [new file with mode: 0644]
framework/Release/lib/Horde/Release.php [new file with mode: 0644]
framework/Release/lib/Horde/Release/Whups.php [new file with mode: 0644]
framework/Release/package.xml [new file with mode: 0644]

diff --git a/framework/Core/lib/Horde/Horde.php b/framework/Core/lib/Horde/Horde.php
new file mode 100644 (file)
index 0000000..ff1f3ac
--- /dev/null
@@ -0,0 +1,1939 @@
+<?php
+/**
+ * The Horde:: class provides the functionality shared by all Horde
+ * applications.
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Jon Parise <jon@horde.org>
+ * @package Core
+ */
+
+/* Log (need to include because of constants). */
+include_once 'Log.php';
+
+class Horde
+{
+    /**
+     * Log instance.
+     *
+     * @var Log
+     */
+    static protected $_logger;
+
+    /**
+     * Has compression been started?
+     *
+     * @var boolean
+     */
+    static protected $_compressStart = false;
+
+    /**
+     * The access keys already used in this page.
+     *
+     * @var array
+     */
+    static protected $_used = array();
+
+    /**
+     * The labels already used in this page.
+     *
+     * @var array
+     */
+    static protected $_labels = array();
+
+    /**
+     * Are accesskeys supported on this system.
+     *
+     * @var boolean
+     */
+    static protected $_noAccessKey;
+
+    /**
+     * Whether the hook has already been loaded.
+     *
+     * @var array
+     */
+    static protected $_hooksLoaded = array();
+
+    /**
+     * Logs a message to the global Horde log backend.
+     *
+     * @param mixed $message     Either a string or a PEAR_Error object.
+     * @param string $file       What file was the log function called from
+     *                           (e.g. __FILE__)?
+     * @param integer $line      What line was the log function called from
+     *                           (e.g. __LINE__)?
+     * @param integer $priority  The priority of the message. One of:
+     * <pre>
+     * PEAR_LOG_EMERG
+     * PEAR_LOG_ALERT
+     * PEAR_LOG_CRIT
+     * PEAR_LOG_ERR
+     * PEAR_LOG_WARNING
+     * PEAR_LOG_NOTICE
+     * PEAR_LOG_INFO
+     * PEAR_LOG_DEBUG
+     * </pre>
+     */
+    static public function logMessage($message, $file, $line,
+                                      $priority = PEAR_LOG_INFO)
+    {
+        $logger = &self::getLogger();
+        if ($logger === false) {
+            return;
+        }
+
+        if ($priority > $GLOBALS['conf']['log']['priority']) {
+            return;
+        }
+
+        if (is_a($message, 'PEAR_Error')) {
+            $userinfo = $message->getUserInfo();
+            $message = $message->getMessage();
+            if (!empty($userinfo)) {
+                if (is_array($userinfo)) {
+                    $old_error = error_reporting(0);
+                    $userinfo = implode(', ', $userinfo);
+                    error_reporting($old_error);
+                }
+                $message .= ': ' . $userinfo;
+            }
+        } elseif (is_object($message) &&
+                  is_callable(array($message, 'getMessage'))) {
+            $message = $message->getMessage();
+        }
+
+        $app = isset($GLOBALS['registry']) ? $GLOBALS['registry']->getApp() : 'horde';
+        $message = '[' . $app . '] ' . $message . ' [pid ' . getmypid() . ' on line ' . $line . ' of "' . $file . '"]';
+
+        /* Make sure to log in the system's locale. */
+        $locale = setlocale(LC_TIME, 0);
+        setlocale(LC_TIME, 'C');
+
+        $logger->log($message, $priority);
+
+        /* Restore original locale. */
+        setlocale(LC_TIME, $locale);
+
+        return true;
+    }
+
+    /**
+     * Get an instantiated instance of the configured logger, if enabled.
+     * getLogger() will fatally exit if a Log object can not be
+     * instantiated.
+     *
+     * @return mixed  Log object on success, false if disabled.
+     */
+    static public function getLogger()
+    {
+        global $conf;
+
+        if (empty($conf['log']['enabled'])) {
+            $ret = false;
+            return $ret;
+        }
+
+        if (isset(self::$_logger)) {
+            return self::$_logger;
+        }
+
+        // Try to make sure that we can log messages somehow.
+        if (empty($conf['log']) ||
+            empty($conf['log']['type']) ||
+            empty($conf['log']['name']) ||
+            empty($conf['log']['ident']) ||
+            !isset($conf['log']['params'])) {
+            self::fatal(PEAR::raiseError('Horde is not correctly configured to log error messages. You must configure at least a text file log in horde/config/conf.php.'), __FILE__, __LINE__, false);
+        }
+
+        self::$_logger = Log::singleton($conf['log']['type'],
+                                        $conf['log']['name'],
+                                        $conf['log']['ident'],
+                                        $conf['log']['params']);
+        if (!is_a(self::$_logger, 'Log')) {
+            self::fatal(PEAR::raiseError('An error has occurred. Furthermore, Horde encountered an error attempting to log this error. Please check your Horde logging configuration in horde/config/conf.php.'), __FILE__, __LINE__, false);
+        }
+
+        return self::$_logger;
+    }
+
+    /**
+     * Destroys any existing session on login and make sure to use a new
+     * session ID, to avoid session fixation issues. Should be called before
+     * checking a login.
+     */
+    static public function getCleanSession()
+    {
+        // Make sure to force a completely new session ID and clear all
+        // session data.
+        session_regenerate_id(true);
+        session_unset();
+
+        /* Reset cookie timeouts, if necessary. */
+        if (!empty($GLOBALS['conf']['session']['timeout'])) {
+            $app = $GLOBALS['registry']->getApp();
+            if (Horde_Secret::clearKey($app)) {
+                Horde_Secret::setKey($app);
+            }
+            Horde_Secret::setKey('auth');
+        }
+    }
+
+    /**
+     * Aborts with a fatal error, displaying debug information to the user.
+     *
+     * @param mixed $error   A PEAR_Error object with debug information or an
+     *                       error message.
+     * @param integer $file  The file in which the error occured.
+     * @param integer $line  The line on which the error occured.
+     * @param boolean $log   Log this message via logMessage()?
+     */
+    static public function fatal($error, $file, $line, $log = true)
+    {
+        $admin = Horde_Auth::isAdmin();
+        $cli = Horde_Cli::runningFromCLI();
+
+        $errortext = '<h1>' . _("A fatal error has occurred") . '</h1>';
+        if (is_a($error, 'PEAR_Error')) {
+            $info = array_merge(array('file' => 'conf.php', 'variable' => '$conf'),
+                                array($error->getUserInfo()));
+
+            switch ($error->getCode()) {
+            case Horde_Util::HORDE_ERROR_DRIVER_CONFIG_MISSING:
+                $message = sprintf(_("No configuration information specified for %s."), $info['name']) . '<br />' .
+                    sprintf(_("The file %s should contain some %s settings."),
+                            $GLOBALS['registry']->get('fileroot') . '/config/' . $info['file'],
+                            sprintf("%s['%s']['params']", $info['variable'], $info['driver']));
+                break;
+
+            case Horde_Util::HORDE_ERROR_DRIVER_CONFIG:
+                $message = sprintf(_("Required \"%s\" not specified in %s configuration."), $info['field'], $info['name']) . '<br />' .
+                    sprintf(_("The file %s should contain a %s setting."),
+                            $GLOBALS['registry']->get('fileroot') . '/config/' . $info['file'],
+                            sprintf("%s['%s']['params']['%s']", $info['variable'], $info['driver'], $info['field']));
+                break;
+
+            default:
+                $message = $error->getMessage();
+                break;
+            }
+
+            $errortext .= '<h3>' . htmlspecialchars($message) . '</h3>';
+        } elseif (is_object($error) && method_exists($error, 'getMessage')) {
+            $errortext .= '<h3>' . htmlspecialchars($error->getMessage()) . '</h3>';
+        } elseif (is_string($error)) {
+            $errortext .= '<h3>' . htmlspecialchars($error) . '</h3>';
+        }
+
+        if ($admin) {
+            $errortext .= '<p><code>' . sprintf(_("[line %d of %s]"), $line, $file) . '</code></p>';
+            if (is_object($error)) {
+                $errortext .= '<h3>' . _("Details:") . '</h3>';
+                $errortext .= '<h4>' . _("The full error message is logged in Horde's log file, and is shown below only to administrators. Non-administrative users will not see error details.") . '</h4>';
+                if (extension_loaded('xdebug')) {
+                    $errortext .= '<br />' . print_r($error, true);
+                } else {
+                    $errortext .= '<p><pre>' . htmlspecialchars(print_r($error, true)) . '</pre></p>';
+                }
+            }
+        } elseif ($log) {
+            $errortext .= '<h3>' . _("Details have been logged for the administrator.") . '</h3>';
+        }
+
+        // Log the error via logMessage() if requested.
+        if ($log) {
+            self::logMessage($error, $file, $line, PEAR_LOG_EMERG);
+        }
+
+        if ($cli) {
+            echo strip_tags(str_replace(array('<br />', '<p>', '</p>', '<h1>', '</h1>', '<h3>', '</h3>'), "\n", $errortext));
+        } else {
+            echo <<< HTML
+<html>
+<head><title>Horde :: Fatal Error</title></head>
+<body style="background:#fff; color:#000">$errortext</body>
+</html>
+HTML;
+        }
+        exit(1);
+    }
+
+    /**
+     * Adds the javascript code to the output (if output has already started)
+     * or to the list of script files to include via includeScriptFiles().
+     *
+     * @param string $file     The full javascript file name.
+     * @param string $app      The application name. Defaults to the current
+     *                         application.
+     * @param boolean $direct  Include the file directly without passing it
+     *                         through javascript.php
+     * @param boolean $full    Output a full URL
+     */
+    static public function addScriptFile($file, $app = null, $direct = false,
+                                         $full = false)
+    {
+        $hsf = &Horde_Script_Files::singleton();
+        $hsf->add($file, $app, $direct, $full);
+    }
+
+    /**
+     * Includes javascript files that were needed before any headers were sent.
+     */
+    static public function includeScriptFiles()
+    {
+        $hsf = &Horde_Script_Files::singleton();
+        $hsf->includeFiles();
+    }
+
+    /**
+     * Provide a list of script files to be included in the current page.
+     *
+     * @var array
+     */
+    static public function listScriptFiles()
+    {
+        $hsf = &Horde_Script_Files::singleton();
+        return $hsf->listFiles();
+    }
+
+    /**
+     * Disable auto-loading of the horde.js script.
+     * Needs to auto-load by default for BC.
+     *
+     * @todo Remove for Horde 4
+     */
+    static public function disableAutoloadHordeJS()
+    {
+        $hsf = &Horde_Script_Files::singleton();
+        $hsf->disableAutoloadHordeJS();
+    }
+
+    /**
+     * Get a token for protecting a form.
+     *
+     * @param string $slug  Slug name.
+     *
+     * @return string  Token string.
+     */
+    static public function getRequestToken($slug)
+    {
+        $token = Horde_Token::generateId($slug);
+        $_SESSION['horde_form_secrets'][$token] = time();
+        return $token;
+    }
+
+    /**
+     * Check if a token for a form is valid.
+     *
+     * @param string $slug   Slug name.
+     * @param string $token  Token to check.
+     *
+     * @throws Horde_Exception
+     */
+    static public function checkRequestToken($slug, $token)
+    {
+        if (empty($_SESSION['horde_form_secrets'][$token])) {
+            throw new Horde_Exception(_("We cannot verify that this request was really sent by you. It could be a malicious request. If you intended to perform this action, you can retry it now."));
+        }
+
+        if (($_SESSION['horde_form_secrets'][$token] + $GLOBALS['conf']['urls']['token_lifetime'] * 60) < time()) {
+            throw new Horde_Exception(sprintf(_("This request cannot be completed because the link you followed or the form you submitted was only valid for %s minutes. Please try again now."), $GLOBALS['conf']['urls']['token_lifetime']));
+        }
+
+        return true;
+    }
+
+    /**
+     * Add a signature + timestamp to a query string and return the signed query
+     * string.
+     *
+     * @param string $queryString  The query string to sign.
+     * @param integer $now         The timestamp at which to sign. Leave blank
+     *                             for generating signatures; specify when
+     *                             testing.
+     *
+     * @return string  The signed query string.
+     */
+    static public function signQueryString($queryString, $now = null)
+    {
+        if (!isset($GLOBALS['conf']['secret_key'])) {
+            return $queryString;
+        }
+
+        if (is_null($now)) {
+            $now = time();
+        }
+
+        $queryString .= '&_t=' . $now . '&_h=';
+
+        return $queryString . Horde_Util::uriB64Encode(hash_hmac('sha1', $queryString, $GLOBALS['conf']['secret_key'], true));
+    }
+
+    /**
+     * Verify a signature and timestamp on a query string.
+     *
+     * @param string $data  The signed query string.
+     * @param integer $now  The current time (can override for testing).
+     *
+     * @return boolean  Whether or not the string was valid.
+     */
+    static public function verifySignedQueryString($data, $now = null)
+    {
+        if (is_null($now)) {
+            $now = time();
+        }
+
+        $pos = strrpos($data, '&_h=');
+        if ($pos === false) {
+            return false;
+        }
+        $pos += 4;
+
+        $queryString = substr($data, 0, $pos);
+        $hmac = substr($data, $pos);
+
+        if ($hmac != Horde_Util::uriB64Encode(hash_hmac('sha1', $queryString, $GLOBALS['conf']['secret_key'], true))) {
+            return false;
+        }
+
+        // String was not tampered with; now validate timestamp
+        parse_str($queryString, $values);
+
+        return !($values['_t'] + $GLOBALS['conf']['urls']['hmac_lifetime'] * 60 < $now);
+    }
+
+    /**
+     * Checks if link should be shown and return the necessary code.
+     *
+     * @param string  $type      Type of link to display
+     * @param string  $app       The name of the current Horde application.
+     * @param boolean $override  Override Horde settings?
+     * @param boolean $referrer  Include the current page as the referrer
+     *                           (url=)?
+     *
+     * @return string  The HTML to create the link.
+     */
+    static public function getServiceLink($type, $app, $override = false,
+                                          $referrer = true)
+    {
+        if (!self::showService($type, $override)) {
+            return false;
+        }
+
+        switch ($type) {
+        case 'help':
+            if ($GLOBALS['browser']->hasFeature('javascript')) {
+                self::addScriptFile('popup.js', 'horde', true);
+            }
+            $url = self::url($GLOBALS['registry']->get('webroot', 'horde') . '/services/help/', true);
+            return Horde_Util::addParameter($url, 'module', $app);
+
+        case 'problem':
+            return self::url($GLOBALS['registry']->get('webroot', 'horde') . '/services/problem.php?return_url=' . urlencode(self::selfUrl(true, true, true)));
+
+        case 'logout':
+            return self::url(Horde_Auth::addLogoutParameters($GLOBALS['registry']->get('webroot', 'horde') . '/login.php', AUTH_REASON_LOGOUT));
+
+        case 'login':
+            return Horde_Auth::getLoginScreen('', $referrer ? self::selfUrl(true) : null);
+
+        case 'options':
+            global $conf;
+            if (($conf['prefs']['driver'] != '') && ($conf['prefs']['driver'] != 'none')) {
+                return self::url($GLOBALS['registry']->get('webroot', 'horde') . '/services/prefs.php?app=' . $app);
+            }
+            break;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns a stdClass response object with added notification information.
+     *
+     * @param string $data                     The 'response' data.
+     * @param Notification_Listener $listener  If set, adds notification
+     *                                         information to object.
+     * @param boolean $auto                    If true, the ajax application
+     *                                         will automatically display the
+     *                                         notification.  If false, the
+     *                                         callback handler is responsible
+     *                                         for displaying the notification.
+     */
+    static public function prepareResponse($data = null, $listener = null,
+                                           $auto = true)
+    {
+        $response = new stdClass();
+        $response->response = $data;
+        if ($listener) {
+            $GLOBALS['notification']->notify(array('listeners' => 'status'));
+            $stack = $listener->getStack();
+            if (!empty($stack)) {
+                $response->msgs = $stack;
+                if (!(bool)$auto) {
+                    $response->msgs_noauto = true;
+                }
+            }
+        }
+
+        return $response;
+    }
+
+    /**
+     * Send response data to browser.
+     *
+     * @param mixed $data  The data to serialize and send to the browser.
+     * @param string $ct   The content-type to send the data with.  Either
+     *                     'json', 'js-json', 'html', 'plain', and 'xml'.
+     */
+    static public function sendHTTPResponse($data, $ct)
+    {
+        $charset = NLS::getCharset();
+
+        // Output headers and encoded response.
+        switch ($ct) {
+        case 'json':
+        case 'js-json':
+            /* JSON responses are a structured object which always
+             * includes the response in a member named 'response', and an
+             * additional array of messages in 'msgs' which may be updates
+             * for the server or notification messages.
+             *
+             * Make sure no null bytes sneak into the JSON output stream.
+             * Null bytes cause IE to stop reading from the input stream,
+             * causing malformed JSON data and a failed request.  These
+             * bytes don't seem to break any other browser, but might as
+             * well remove them anyway.
+             *
+             * Finally, add prototypejs security delimiters to returned
+             * JSON. */
+            $s_data = '/*-secure-' .
+                Horde_String::convertCharset(str_replace("\00", '', Horde_Serialize::serialize($data, Horde_Serialize::JSON, $charset)), 'UTF-8') .
+                '*/';
+
+            if ($ct == 'json') {
+                header('Content-Type: application/json');
+                echo $s_data;
+            } else {
+                header('Content-Type: text/html; charset=' . $charset);
+                echo htmlspecialchars($s_data);
+            }
+            break;
+
+        case 'html':
+        case 'plain':
+        case 'xml':
+            header('Content-Type: text/' . $ct . '; charset=' . $charset);
+            echo $data;
+            break;
+
+        default:
+            echo $data;
+        }
+
+        exit;
+    }
+
+    /**
+     * Is the current HTTP connection considered secure?
+     * @TODO Move this to the request classes!
+     *
+     * @return boolean
+     */
+    static public function isConnectionSecure()
+    {
+        if ($GLOBALS['browser']->usingSSLConnection()) {
+            return true;
+        }
+
+        if (!empty($GLOBALS['conf']['safe_ips'])) {
+            if (reset($GLOBALS['conf']['safe_ips']) == '*') {
+                return true;
+            }
+
+            /* $_SERVER['HTTP_X_FORWARDED_FOR'] is user data and not
+             * reliable. We don't consult it for safe IPs. We also have to
+             * assume that if it is present, the user is coming through a proxy
+             * server. If so, we don't count any non-SSL connection as safe, no
+             * matter the source IP. */
+            if (!isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+                $remote_addr = $_SERVER['REMOTE_ADDR'];
+                foreach ($GLOBALS['conf']['safe_ips'] as $safe_ip) {
+                    $safe_ip = preg_replace('/(\.0)*$/', '', $safe_ip);
+                    if (strpos($remote_addr, $safe_ip) === 0) {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Throws an exception if not using a secure connection.
+     *
+     * @throws Horde_Exception
+     */
+    static public function requireSecureConnection()
+    {
+        if (!self::isConnectionSecure()) {
+            throw new Horde_Exception(_("The encryption features require a secure web connection."));
+        }
+    }
+
+    /**
+     * TODO
+     *
+     * @param string $type       The type of link.
+     * @param boolean $override  Override Horde settings?
+     *
+     * @return boolean  True if the link is to be shown.
+     */
+    static public function showService($type, $override = false)
+    {
+        global $conf;
+
+        if (empty($conf['menu']['links'][$type])) {
+            return false;
+        }
+
+        switch ($conf['menu']['links'][$type]) {
+        case 'all':
+            return true;
+
+        case 'never':
+            return $override;
+
+        case 'authenticated':
+            return $override || (bool)Horde_Auth::getAuth();
+
+        default:
+            return $override;
+        }
+    }
+
+    /**
+     * Loads global and vhost specific configuration files.
+     *
+     * @param string $config_file      The name of the configuration file.
+     * @param string|array $var_names  The name(s) of the variable(s) that
+     *                                 is/are defined in the configuration
+     *                                 file.
+     * @param string $app              The application. Defaults to the current
+     *                                 application.
+     * @param boolean $show_output     If true, the contents of the requested
+     *                                 config file are simply output instead of
+     *                                 loaded into a variable.
+     *
+     * @return mixed  The value of $var_names, in a compact()'ed array if
+     *                $var_names is an array.
+     */
+    static public function loadConfiguration($config_file, $var_names = null,
+                                             $app = null, $show_output = false)
+    {
+        global $registry;
+
+        if (is_null($app)) {
+            $app = $registry->getApp();
+        }
+
+        // Track if we've included some version (main or vhosted) of
+        // the config file.
+        $was_included = false;
+
+        // Load global configuration file.
+        $config_dir = (($app == 'horde') && defined('HORDE_BASE'))
+            ? HORDE_BASE . '/config/'
+            : $registry->get('fileroot', $app) . '/config/';
+        $file = $config_dir . $config_file;
+
+        if (file_exists($file)) {
+            /* If we are not exporting variables located in the configuration
+             * file, or we are not capturing the output, then there is no
+             * need to load the configuration file more than once. */
+            ob_start();
+            $success = (is_null($var_names) && !$show_output)
+                ? include_once $file
+                : include $file;
+            $output = ob_get_clean();
+
+            if (!empty($output) && !$show_output) {
+                return PEAR::raiseError(sprintf('Failed to import configuration file "%s": ', $file) . strip_tags($output));
+            }
+
+            if (!$success) {
+                return PEAR::raiseError(sprintf('Failed to import configuration file "%s".', $file));
+            }
+
+            $was_included = true;
+        }
+
+        // Load vhost configuration file.
+        if (!empty($conf['vhosts']) || !empty($GLOBALS['conf']['vhosts'])) {
+            $server_name = isset($GLOBALS['conf'])
+                ? $GLOBALS['conf']['server']['name']
+                : $conf['server']['name'];
+            $file = $config_dir . substr($config_file, 0, -4) . '-' . $server_name . '.php';
+
+            if (file_exists($file)) {
+                ob_start();
+                $success = (is_null($var_names) && !$show_output)
+                    ? include_once $file
+                    : include $file;
+                $output = ob_get_clean();
+
+                if (!empty($output) && !$show_output) {
+                    return PEAR::raiseError(sprintf('Failed to import configuration file "%s": ', $file) . strip_tags($output));
+                }
+
+                if (!$success) {
+                    return PEAR::raiseError(sprintf('Failed to import configuration file "%s".', $file));
+                }
+
+                $was_included = true;
+            }
+        }
+
+        // Return an error if neither main or vhosted versions of the config
+        // file exist.
+        if (!$was_included) {
+            return PEAR::raiseError(sprintf('Failed to import configuration file "%s".', $config_dir . $config_file));
+        }
+
+        if (isset($output) && $show_output) {
+            echo $output;
+        }
+
+        if (is_null($var_names)) {
+            return;
+        } elseif (is_array($var_names)) {
+            return compact($var_names);
+        } elseif (isset($$var_names)) {
+            return $$var_names;
+        } else {
+            return array();
+        }
+    }
+
+    /**
+     * Returns the driver parameters for the specified backend.
+     *
+     * @param mixed $backend  The backend system (e.g. 'prefs', 'categories',
+     *                        'contacts') being used.
+     *                        The used configuration array will be
+     *                        $conf[$backend]. If an array gets passed, it will
+     *                        be $conf[$key1][$key2].
+     * @param string $type    The type of driver.
+     *
+     * @return array  The connection parameters.
+     */
+    static public function getDriverConfig($backend, $type = 'sql')
+    {
+        global $conf;
+
+        $c = null;
+        if (is_array($backend)) {
+            $c = Horde_Array::getElement($conf, $backend);
+        } elseif (isset($conf[$backend])) {
+            $c = $conf[$backend];
+        }
+
+        if (!is_null($c) && isset($c['params'])) {
+            $c['params']['umask'] = $conf['umask'];
+            if (isset($conf[$type])) {
+                return array_merge($conf[$type], $c['params']);
+            } else {
+                return $c['params'];
+            }
+        }
+
+        return isset($conf[$type]) ? $conf[$type] : array();
+    }
+
+
+    /**
+     * Returns the VFS driver parameters for the specified backend.
+     *
+     * @param string $name  The VFS system name (e.g. 'images', 'documents')
+     *                      being used.
+     *
+     * @return array  A hash with the VFS parameters; the VFS driver in 'type'
+     *                and the connection parameters in 'params'.
+     */
+    static public function getVFSConfig($name)
+    {
+        global $conf;
+
+        if (!isset($conf[$name]['type'])) {
+            return PEAR::raiseError(_("You must configure a VFS backend."));
+        }
+
+        $vfs = ($conf[$name]['type'] == 'horde')
+            ? $conf['vfs']
+            : $conf[$name];
+
+        if ($vfs['type'] == 'sql') {
+            $vfs['params'] = self::getDriverConfig($name, 'sql');
+        }
+
+        return $vfs;
+    }
+
+    /**
+     * Return the driver and parameters for the current mailer configuration.
+     *
+     * @return array  Array with driver name and parameter hash.
+     */
+    static public function getMailerConfig()
+    {
+        $mail_driver = $GLOBALS['conf']['mailer']['type'];
+        $mail_params = $GLOBALS['conf']['mailer']['params'];
+        if ($mail_driver == 'smtp' && $mail_params['auth'] &&
+            empty($mail_params['username'])) {
+            $mail_params['username'] = Horde_Auth::getAuth();
+            $mail_params['password'] = Horde_Auth::getCredential('password');
+        }
+
+        return array($mail_driver, $mail_params);
+    }
+
+    /**
+     * Checks if all necessary parameters for a driver configuration
+     * are set and throws a fatal error with a detailed explanation
+     * how to fix this, if something is missing.
+     *
+     * @param array $params     The configuration array with all parameters.
+     * @param string $driver    The key name (in the configuration array) of
+     *                          the driver.
+     * @param array $fields     An array with mandatory parameter names for
+     *                          this driver.
+     * @param string $name      The clear text name of the driver. If not
+     *                          specified, the application name will be used.
+     * @param string $file      The configuration file that should contain
+     *                          these settings.
+     * @param string $variable  The name of the configuration variable.
+     */
+    static public function assertDriverConfig($params, $driver, $fields,
+                                              $name = null,
+                                              $file = 'conf.php',
+                                              $variable = '$conf')
+    {
+        global $registry;
+
+        // Don't generate a fatal error if we fail during or before
+        // Registry instantiation.
+        if (is_null($name)) {
+            $name = isset($registry) ? $registry->getApp() : '[unknown]';
+        }
+        $fileroot = isset($registry) ? $registry->get('fileroot') : '';
+
+        if (!is_array($params) || !count($params)) {
+            self::fatal(PEAR::raiseError(
+                sprintf(_("No configuration information specified for %s."), $name) . "\n\n" .
+                sprintf(_("The file %s should contain some %s settings."),
+                    $fileroot . '/config/' . $file,
+                    sprintf("%s['%s']['params']", $variable, $driver))),
+                __FILE__, __LINE__);
+        }
+
+        foreach ($fields as $field) {
+            if (!isset($params[$field])) {
+                self::fatal(PEAR::raiseError(
+                    sprintf(_("Required \"%s\" not specified in %s configuration."), $field, $name) . "\n\n" .
+                    sprintf(_("The file %s should contain a %s setting."),
+                        $fileroot . '/config/' . $file,
+                        sprintf("%s['%s']['params']['%s']", $variable, $driver, $field))),
+                    __FILE__, __LINE__);
+            }
+        }
+    }
+
+    /**
+     * Returns a session-id-ified version of $uri.
+     * If a full URL is requested, all parameter separators get converted to
+     * "&", otherwise to "&amp;".
+     *
+     * @param string $uri              The URI to be modified.
+     * @param boolean $full            Generate a full (http://server/path/)
+     *                                 URL.
+     * @param integer $append_session  0 = only if needed, 1 = always, -1 =
+     *                                 never.
+     * @param boolean $force_ssl       Ignore $conf['use_ssl'] and force
+     *                                 creation of a SSL URL?
+     *
+     * @return string  The URL with the session id appended (if needed).
+     */
+    static public function url($uri, $full = false, $append_session = 0,
+                               $force_ssl = false)
+    {
+        if ($force_ssl) {
+            $full = true;
+        }
+
+        if ($full) {
+            global $conf, $registry, $browser;
+
+            /* Store connection parameters in local variables. */
+            $server_name = $conf['server']['name'];
+            $server_port = $conf['server']['port'];
+
+            $protocol = 'http';
+            if ($conf['use_ssl'] == 1) {
+                $protocol = 'https';
+            } elseif ($conf['use_ssl'] == 2 &&
+                      $browser->usingSSLConnection()) {
+                $protocol = 'https';
+            } elseif ($conf['use_ssl'] == 3) {
+                $server_port = '';
+                if ($force_ssl) {
+                    $protocol = 'https';
+                }
+            }
+
+            /* If using non-standard ports, add the port to the URL. */
+            if (!empty($server_port) &&
+                ((($protocol == 'http') && ($server_port != 80)) ||
+                 (($protocol == 'https') && ($server_port != 443)))) {
+                $server_name .= ':' . $server_port;
+            }
+
+            /* Store the webroot in a local variable. */
+            $webroot = $registry->get('webroot');
+
+            $url = $protocol . '://' . $server_name;
+            if (preg_match('|^([\w+-]{1,20})://|', $webroot)) {
+                /* Don't prepend to webroot if it's already absolute. */
+                $url = '';
+            }
+
+            if (substr($uri, 0, 1) != '/') {
+                /* Simple case for http:// absolute webroots. */
+                if (preg_match('|^([\w+-]{1,20})://|', $uri)) {
+                    $url = $uri;
+                } elseif (substr($webroot, -1) == '/') {
+                    $url .= $webroot . $uri;
+                } else {
+                    $url .= $webroot . '/' . $uri;
+                }
+            } else {
+                $url .= $uri;
+            }
+        } else {
+            $url = $uri;
+
+            if (!empty($_SERVER['HTTP_HOST'])) {
+                // Don't generate absolute URLs if we don't have to.
+                if (preg_match('|^([\w+-]{1,20}://' . preg_quote($_SERVER['HTTP_HOST'], '|') . ')/|', $url, $matches)) {
+                    $url = substr($url, strlen($matches[1]));
+                }
+            }
+        }
+
+        if (empty($GLOBALS['conf']['session']['use_only_cookies']) &&
+            (($append_session == 1) ||
+             (($append_session == 0) &&
+              !isset($_COOKIE[session_name()])))) {
+            $url = Horde_Util::addParameter($url, session_name(), session_id());
+        }
+
+        if ($full) {
+            /* We need to run the replace twice, because we only catch every
+             * second match. */
+            return preg_replace(array('/(=?.*?)&amp;(.*?=)/',
+                                      '/(=?.*?)&amp;(.*?=)/'),
+                                '$1&$2', $url);
+        } elseif (preg_match('/=.*&amp;.*=/', $url)) {
+            return $url;
+        } else {
+            return htmlentities($url);
+        }
+    }
+
+    /**
+     * Returns a session-id-ified version of $uri, using the current
+     * application's webroot setting.
+     *
+     * @param string $uri              The URI to be modified.
+     * @param boolean $full            Generate a full (http://server/path/)
+     *                                 URL.
+     * @param integer $append_session  0 = only if needed, 1 = always, -1 =
+     *                                 never.
+     *
+     * @return string  The url with the session id appended.
+     */
+    static public function applicationUrl($uri, $full = false,
+                                          $append_session = 0)
+    {
+        if ($full) {
+            return self::url($uri, $full, $append_session);
+        }
+
+        if (substr($uri, 0, 1) != '/') {
+            $webroot = $GLOBALS['registry']->get('webroot');
+            if (substr($webroot, -1) != '/') {
+                $webroot .= '/';
+            }
+            $uri = $webroot . $uri;
+        }
+
+        return self::url($uri, $full, $append_session);
+    }
+
+    /**
+     * Returns an external link passed through the dereferrer to strip session
+     * IDs from the referrer.
+     *
+     * @param string $url   The external URL to link to.
+     * @param boolean $tag  If true, a complete <a> tag is returned, only the
+     *                      url otherwise.
+     *
+     * @return string  The link to the dereferrer script.
+     */
+    static public function externalUrl($url, $tag = false)
+    {
+        if (!isset($_GET[session_name()]) ||
+            Horde_String::substr($url, 0, 1) == '#' ||
+            Horde_String::substr($url, 0, 7) == 'mailto:') {
+            $ext = $url;
+        } else {
+            $ext = self::url($GLOBALS['registry']->get('webroot', 'horde') .
+                              '/services/go.php', true, -1);
+
+            /* We must make sure there are no &amp's in the URL. */
+            $url = preg_replace(array('/(=?.*?)&amp;(.*?=)/', '/(=?.*?)&amp;(.*?=)/'), '$1&$2', $url);
+            $ext .= '?' . self::signQueryString('url=' . urlencode($url));
+        }
+
+        if ($tag) {
+            $ext = self::link($ext, $url, '', '_blank');
+        }
+
+        return $ext;
+    }
+
+    /**
+     * Returns a URL to be used for downloading, that takes into account any
+     * special browser quirks (i.e. IE's broken filename handling).
+     *
+     * @param string $filename  The filename of the download data.
+     * @param array $params     Any additional parameters needed.
+     * @param string $url       The URL to alter. If none passed in, will use
+     *                          the file 'view.php' located in the current
+     *                          module's base directory.
+     *
+     * @return string  The download URL.
+     */
+    static public function downloadUrl($filename, $params = array(), $url = null)
+    {
+        global $browser, $registry;
+
+        $horde_url = false;
+
+        if (is_null($url)) {
+            $url = Horde_Util::addParameter(self::url($registry->get('webroot', 'horde') . '/services/download/'), 'module', $registry->getApp());
+            $horde_url = true;
+        }
+
+        /* Add parameters. */
+        if (!is_null($params)) {
+            $url = Horde_Util::addParameter($url, $params);
+        }
+
+        /* If we are using the default Horde download link, add the
+         * filename to the end of the URL. Although not necessary for
+         * many browsers, this should allow every browser to download
+         * correctly. */
+        if ($horde_url) {
+            $url = Horde_Util::addParameter($url, 'fn', '/' . rawurlencode($filename));
+        } elseif ($browser->hasQuirk('break_disposition_filename')) {
+            /* Some browsers will only obtain the filename correctly
+             * if the extension is the last argument in the query
+             * string and rest of the filename appears in the
+             * PATH_INFO element. */
+            $filename = rawurlencode($filename);
+
+            /* Get the webserver ID. */
+            $server = self::webServerID();
+
+            /* Get the name and extension of the file.  Apache 2 does
+             * NOT support PATH_INFO information being passed to the
+             * PHP module by default, so disable that
+             * functionality. */
+            if (($server != 'apache2')) {
+                if (($pos = strrpos($filename, '.'))) {
+                    $name = '/' . preg_replace('/\./', '%2E', substr($filename, 0, $pos));
+                    $ext = substr($filename, $pos);
+                } else {
+                    $name = '/' . $filename;
+                    $ext = '';
+                }
+
+                /* Enter the PATH_INFO information. */
+                if (($pos = strpos($url, '?'))) {
+                    $url = substr($url, 0, $pos) . $name . substr($url, $pos);
+                } else {
+                    $url .= $name;
+                }
+            }
+
+            /* Append the extension, if it exists. */
+            if (($server == 'apache2') || !empty($ext)) {
+                $url = Horde_Util::addParameter($url, 'fn_ext', '/' . $filename);
+            }
+        }
+
+        return $url;
+    }
+
+    /**
+     * Returns an anchor tag with the relevant parameters
+     *
+     * @param string $url        The full URL to be linked to.
+     * @param string $title      The link title/description.
+     * @param string $class      The CSS class of the link.
+     * @param string $target     The window target to point to.
+     * @param string $onclick    JavaScript action for the 'onclick' event.
+     * @param string $title2     The link title (tooltip) (deprecated - just
+     *                           use $title).
+     * @param string $accesskey  The access key to use.
+     * @param array $attributes  Any other name/value pairs to add to the <a>
+     *                           tag.
+     * @param boolean $escape    Whether to escape special characters in the
+     *                           title attribute.
+     *
+     * @return string  The full <a href> tag.
+     */
+    static public function link($url = '', $title = '', $class = '',
+                                $target = '', $onclick = '', $title2 = '',
+                                $accesskey = '', $attributes = array(),
+                                $escape = true)
+    {
+        if (!empty($title2)) {
+            $title = $title2;
+        }
+
+        $ret = '<a';
+        if (!empty($url)) {
+            $ret .= " href=\"$url\"";
+        }
+        if (!empty($onclick)) {
+            $ret .= " onclick=\"$onclick\"";
+        }
+        if (!empty($class)) {
+            $ret .= " class=\"$class\"";
+        }
+        if (!empty($target)) {
+            $ret .= " target=\"$target\"";
+        }
+        if (!empty($title)) {
+            if ($escape) {
+                $charset = NLS::getCharset();
+                $old_error = error_reporting(0);
+                $title = str_replace(
+                    array("\r", "\n"), '',
+                    htmlspecialchars(
+                        nl2br(htmlspecialchars($title, ENT_QUOTES, $charset)),
+                        ENT_QUOTES, $charset));
+                error_reporting($old_error);
+            }
+            $ret .= ' title="' . $title . '"';
+        }
+        if (!empty($accesskey)) {
+            $ret .= ' accesskey="' . htmlspecialchars($accesskey) . '"';
+        }
+
+        foreach ($attributes as $name => $value) {
+            $ret .= ' ' . htmlspecialchars($name) . '="'
+                . htmlspecialchars($value) . '"';
+        }
+
+        return "$ret>";
+    }
+
+    /**
+     * Uses DOM Tooltips to display the 'title' attribute for
+     * link() calls.
+     *
+     * @param string $url        The full URL to be linked to
+     * @param string $status     The JavaScript mouse-over string
+     * @param string $class      The CSS class of the link
+     * @param string $target     The window target to point to.
+     * @param string $onclick    JavaScript action for the 'onclick' event.
+     * @param string $title      The link title (tooltip).
+     * @param string $accesskey  The access key to use.
+     * @param array  $attributes Any other name/value pairs to add to the <a>
+     *                           tag.
+     *
+     * @return string  The full <a href> tag.
+     */
+    static public function linkTooltip($url, $status = '', $class = '',
+                                       $target = '', $onclick = '',
+                                       $title = '', $accesskey = '',
+                                       $attributes = array())
+    {
+        if (!empty($title)) {
+            $charset = NLS::getCharset();
+            $old_error = error_reporting(0);
+            $title = '&lt;pre&gt;' . preg_replace(array('/\n/', '/((?<!<br)\s{1,}(?<!\/>))/em', '/<br \/><br \/>/', '/<br \/>/'), array('', 'str_repeat("&nbsp;", strlen("$1"))', '&lt;br /&gt; &lt;br /&gt;', '&lt;br /&gt;'), nl2br(htmlspecialchars(htmlspecialchars($title, ENT_QUOTES, $charset), ENT_QUOTES, $charset))) . '&lt;/pre&gt;';
+            error_reporting($old_error);
+        }
+
+        return self::link($url, $title, $class, $target, $onclick, null, $accesskey, $attributes, false);
+    }
+
+    /**
+     * Returns an anchor sequence with the relevant parameters for a widget
+     * with accesskey and text.
+     *
+     * @param string  $url      The full URL to be linked to.
+     * @param string  $title    The link title/description.
+     * @param string  $class    The CSS class of the link
+     * @param string  $target   The window target to point to.
+     * @param string  $onclick  JavaScript action for the 'onclick' event.
+     * @param string  $title2   The link title (tooltip) (deprecated - just use
+     *                          $title).
+     * @param boolean $nocheck  Don't check if the access key already has been
+     *                          used. Defaults to false (= check).
+     *
+     * @return string  The full <a href>Title</a> sequence.
+     */
+    static public function widget($url, $title = '', $class = 'widget',
+                                  $target = '', $onclick = '', $title2 = '',
+                                  $nocheck = false)
+    {
+        if (!empty($title2)) {
+            $title = $title2;
+        }
+
+        $ak = self::getAccessKey($title, $nocheck);
+
+        return self::link($url, '', $class, $target, $onclick, '', $ak) . self::highlightAccessKey($title, $ak) . '</a>';
+    }
+
+    /**
+     * Returns a session-id-ified version of $SCRIPT_NAME resp. $PHP_SELF.
+     *
+     * @param boolean $script_params Include script parameters like
+     *                               QUERY_STRING and PATH_INFO?
+     * @param boolean $nocache       Include a nocache parameter in the URL?
+     * @param boolean $full          Return a full URL?
+     * @param boolean $force_ssl     Ignore $conf['use_ssl'] and force creation
+     *                               of a SSL URL?
+     *
+     * @return string  The requested URI.
+     */
+    static public function selfUrl($script_params = false, $nocache = true,
+                                   $full = false, $force_ssl = false)
+    {
+        if (!strncmp(PHP_SAPI, 'cgi', 3)) {
+            // When using CGI PHP, SCRIPT_NAME may contain the path to
+            // the PHP binary instead of the script being run; use
+            // PHP_SELF instead.
+            $url = $_SERVER['PHP_SELF'];
+        } else {
+            $url = isset($_SERVER['SCRIPT_NAME']) ?
+                $_SERVER['SCRIPT_NAME'] :
+                $_SERVER['PHP_SELF'];
+        }
+
+        if ($script_params) {
+            if ($pathInfo = Horde_Util::getPathInfo()) {
+                $url .= $pathInfo;
+            }
+            if (!empty($_SERVER['QUERY_STRING'])) {
+                $url .= '?' . $_SERVER['QUERY_STRING'];
+            }
+        }
+
+        $url = self::url($url, $full, 0, $force_ssl);
+
+        return $nocache
+            ? Horde_Util::nocacheUrl($url, !$full)
+            : $url;
+    }
+
+    /**
+     * Constructs a correctly-pathed link to an image.
+     *
+     * @param string $src   The image file.
+     * @param string $alt   Text describing the image.
+     * @param mixed  $attr  Any additional attributes for the image tag. Can be
+     *                      a pre-built string or an array of key/value pairs
+     *                      that will be assembled and html-encoded.
+     * @param string $dir   The root graphics directory.
+     *
+     * @return string  The full image tag.
+     */
+    static public function img($src, $alt = '', $attr = '', $dir = null)
+    {
+        $charset = NLS::getCharset();
+
+        /* If browser does not support images, simply return the ALT text. */
+        if (!$GLOBALS['browser']->hasFeature('images')) {
+            $old_error = error_reporting(0);
+            $res = htmlspecialchars($alt, ENT_COMPAT, $charset);
+            error_reporting($old_error);
+            return $res;
+        }
+
+        /* If no directory has been specified, get it from the registry. */
+        if (is_null($dir)) {
+            $dir = $GLOBALS['registry']->getImageDir();
+        }
+
+        /* If a directory has been provided, prepend it to the image source. */
+        if (!empty($dir)) {
+            $src = $dir . '/' . $src;
+        }
+
+        /* Build all of the tag attributes. */
+        $attributes = array('alt' => $alt);
+        if (is_array($attr)) {
+            $attributes = array_merge($attributes, $attr);
+        }
+        if (empty($attributes['title'])) {
+            $attributes['title'] = '';
+        }
+
+        $img = '<img';
+        $old_error = error_reporting(0);
+        foreach ($attributes as $attribute => $value) {
+            $img .= ' ' . $attribute . '="' . htmlspecialchars($value, ENT_COMPAT, $charset) . '"';
+        }
+        error_reporting($old_error);
+
+        /* If the user supplied a pre-built string of attributes, add that. */
+        if (is_string($attr) && !empty($attr)) {
+            $img .= ' ' . $attr;
+        }
+
+        /* Return the closed image tag. */
+        return $img . ' src="' . self::base64ImgData($src) . '" />';
+    }
+
+    /**
+     * Same as Horde::img(), but returns a full source url for the image.
+     * Useful for when the image may be part of embedded Horde content on an
+     * external site. Basically a stop-gap measure until Horde_View etc...
+     *
+     * @see Horde::img()
+     */
+    static public function fullSrcImg($src, $options = array())
+    {
+        $charset = NLS::getCharset();
+
+        /* If browser does not support images, simply return the ALT text. */
+        if (!$GLOBALS['browser']->hasFeature('images')) {
+            $old_error = error_reporting(0);
+            $res = htmlspecialchars($alt, ENT_COMPAT, $charset);
+            error_reporting($old_error);
+            return $res;
+        }
+
+        /* If no directory has been specified, get it from the registry. */
+        $dir = empty($options['dir']) ? $GLOBALS['registry']->getImageDir() : $options['dir'];
+
+        /* If we can send as data, no need to get the full path */
+        $src = self::base64ImgData($dir . '/' . $src);
+        if (substr($src, 0, 10) != 'data:image') {
+            $src = self::url($src, true, -1);
+        }
+
+        /* Build all of the tag attributes. */
+        $attributes = !empty($options['attr']) ? $options['attr'] : array();
+
+        $img = '<img';
+        $old_error = error_reporting(0);
+        foreach ($attributes as $attribute => $value) {
+            $img .= ' ' . $attribute . '="' . htmlspecialchars($value, ENT_COMPAT, $charset) . '"';
+        }
+        error_reporting($old_error);
+
+        /* If the user supplied a pre-built string of attributes, add that. */
+        if (!empty($options['attr']) && is_string($options['attr'])) {
+            $img .= ' ' . $options['attr'];
+        }
+
+        /* Return the closed image tag. */
+        return $img . ' src="' . $src . '" />';
+    }
+
+    /**
+     * Generate RFC 2397-compliant image data strings.
+     *
+     * @param string $file  Filename containing image data.
+     *
+     * @return string  The string to use in the image 'src' attribute; either
+     *                 the image data if the browser supports, or the filepath
+     *                 if not.
+     */
+    public function base64ImgData($file)
+    {
+        $dataurl = $GLOBALS['browser']->hasFeature('dataurl');
+        if (!$dataurl) {
+            return $file;
+        }
+
+        /* Only encode image files if they are below the dataurl limit. */
+        $filename = realpath($GLOBALS['registry']->get('fileroot', 'horde')) . preg_replace('/^' . preg_quote($GLOBALS['registry']->get('webroot', 'horde'), '/') . '/', '', $file);
+
+        /* Delete approx. 50 chars from the limit to account for the various
+         * data/base64 header text.  Multiply by 0.75 to determine the
+         * base64 encoded size. */
+        return (($dataurl === true) ||
+                (filesize($filename) <= (($dataurl * 0.75) - 50)))
+            ? 'data:image/' . substr($file, strrpos($file, '.') + 1) . ';base64,' . base64_encode(file_get_contents($filename))
+            : $file;
+    }
+
+    /**
+     * Determines the location of the system temporary directory. If a specific
+     * setting cannot be found, it defaults to /tmp.
+     *
+     * @return string  A directory name that can be used for temp files.
+     *                 Returns false if one could not be found.
+     */
+    static public function getTempDir()
+    {
+        global $conf;
+
+        /* If one has been specifically set, then use that */
+        if (!empty($conf['tmpdir'])) {
+            $tmp = $conf['tmpdir'];
+        }
+
+        /* Next, try Horde_Util::getTempDir(). */
+        if (empty($tmp)) {
+            $tmp = Horde_Util::getTempDir();
+        }
+
+        /* If it is still empty, we have failed, so return false;
+         * otherwise return the directory determined. */
+        return empty($tmp)
+            ? false
+            : $tmp;
+    }
+
+    /**
+     * Creates a temporary filename for the lifetime of the script, and
+     * (optionally) registers it to be deleted at request shutdown.
+     *
+     * @param string $prefix   Prefix to make the temporary name more
+     *                         recognizable.
+     * @param boolean $delete  Delete the file at the end of the request?
+     * @param string $dir      Directory to create the temporary file in.
+     * @param boolean $secure  If deleting file, should we securely delete the
+     *                         file?
+     *
+     * @return string   Returns the full path-name to the temporary file or
+     *                  false if a temporary file could not be created.
+     */
+    static public function getTempFile($prefix = 'Horde', $delete = true,
+                                       $dir = '', $secure = false)
+    {
+        if (empty($dir) || !is_dir($dir)) {
+            $dir = self::getTempDir();
+        }
+
+        return Horde_Util::getTempFile($prefix, $delete, $dir, $secure);
+    }
+
+    /**
+     * Starts output compression, if requested.
+     */
+    static public function compressOutput()
+    {
+        if (self::$_compressStart) {
+            return;
+        }
+
+        /* Compress output if requested and possible. */
+        if ($GLOBALS['conf']['compress_pages'] &&
+            !$GLOBALS['browser']->hasQuirk('buggy_compression') &&
+            !(bool)ini_get('zlib.output_compression') &&
+            !(bool)ini_get('zend_accelerator.compress_all') &&
+            ini_get('output_handler') != 'ob_gzhandler') {
+            if (ob_get_level()) {
+                ob_end_clean();
+            }
+            ob_start('ob_gzhandler');
+        }
+
+        self::$_compressStart = true;
+    }
+
+    /**
+     * Determines if output compression can be used.
+     *
+     * @return boolean  True if output compression can be used, false if not.
+     */
+    static public function allowOutputCompression()
+    {
+        $browser = &Horde_Browser::singleton();
+        return !$browser->hasQuirk('buggy_compression') &&
+               (ini_get('zlib.output_compression') == '') &&
+               (ini_get('zend_accelerator.compress_all') == '') &&
+               (ini_get('output_handler') != 'ob_gzhandler');
+    }
+
+    /**
+     * Returns the Web server being used.
+     * PHP string list built from the PHP 'configure' script.
+     *
+     * @return string  A web server identification string.
+     * @see php_sapi_name()
+     */
+    static public function webServerID()
+    {
+        switch (PHP_SAPI) {
+        case 'apache':
+            return 'apache1';
+
+        case 'apache2filter':
+        case 'apache2handler':
+            return 'apache2';
+
+        default:
+            return PHP_SAPI;
+        }
+    }
+
+    /**
+     * Returns the <link> tags for the CSS stylesheets.
+     *
+     * @param string|array $app  The Horde application(s).
+     * @param mixed $theme       The theme to use; specify an empty value to
+     *                           retrieve the theme from user preferences, and
+     *                           false for no theme.
+     * @param boolean $inherit   Inherit Horde-wide CSS?
+     *
+     * @return string  <link> tags for CSS stylesheets.
+     */
+    static public function stylesheetLink($apps = null, $theme = '',
+                                          $inherit = true)
+    {
+        $css = self::getStylesheets($apps, $theme, $inherit);
+
+        $html = '';
+        foreach ($css as $css_link) {
+            $html .= '<link href="' . $css_link['u'] . '" rel="stylesheet" type="text/css" />' . "\n";
+        }
+
+        return $html;
+    }
+
+    /**
+     * Return the list of base stylesheets to display.
+     *
+     * @param string|array $app  The Horde application(s).
+     * @param mixed $theme       The theme to use; specify an empty value to
+     *                           retrieve the theme from user preferences, and
+     *                           false for no theme.
+     * @param boolean $inherit   Inherit Horde-wide CSS?
+     *
+     * @return array
+     */
+    static public function getStylesheets($apps = null, $theme = '',
+                                          $inherit = true)
+    {
+        if ($theme === '' && isset($GLOBALS['prefs'])) {
+            $theme = $GLOBALS['prefs']->getValue('theme');
+        }
+
+        $css = array();
+        $rtl = isset($GLOBALS['nls']['rtl'][$GLOBALS['language']]);
+
+        if (!is_array($apps)) {
+            $apps = is_null($apps) ? array() : array($apps);
+        }
+        if ($inherit) {
+            $key = array_search('horde', $apps);
+            if ($key !== false) {
+                unset($apps[$key]);
+            }
+            array_unshift($apps, 'horde');
+        }
+
+        /* Collect browser specific stylesheets if needed. */
+        $browser_css = array();
+
+        switch ($GLOBALS['browser']->getBrowser()) {
+        case 'msie':
+            $ie_major = $GLOBALS['browser']->getMajor();
+            if ($ie_major == 7) {
+                $browser_css[] = 'ie7.css';
+            } elseif ($ie_major < 7) {
+                $browser_css[] = 'ie6_or_less.css';
+                if ($GLOBALS['browser']->getPlatform() == 'mac') {
+                    $browser_css[] = 'ie5mac.css';
+                }
+            }
+            break;
+
+        case 'opera':
+            $browser_css[] = 'opera.css';
+            break;
+
+        case 'mozilla':
+            if ($GLOBALS['browser']->getMajor() >= 5 &&
+                preg_match('/rv:(.*)\)/', $GLOBALS['browser']->getAgentString(), $revision) &&
+                $revision[1] <= 1.4) {
+                $browser_css[] = 'moz14.css';
+            }
+            break;
+
+        case 'webkit':
+            $browser_css[] = 'webkit.css';
+            break;
+        }
+
+        foreach ($apps as $app) {
+            $themes_fs = $GLOBALS['registry']->get('themesfs', $app);
+            $themes_uri = self::url($GLOBALS['registry']->get('themesuri', $app), false, -1);
+            $css[] = array('u' => $themes_uri . '/screen.css', 'f' => $themes_fs . '/screen.css');
+            if (!empty($theme) &&
+                file_exists($themes_fs . '/' . $theme . '/screen.css')) {
+                $css[] = array('u' => $themes_uri . '/' . $theme . '/screen.css', 'f' => $themes_fs . '/' . $theme . '/screen.css');
+            }
+
+            if ($rtl) {
+                $css[] = array('u' => $themes_uri . '/rtl.css', 'f' => $themes_fs . '/rtl.css');
+                if (!empty($theme) &&
+                    file_exists($themes_fs . '/' . $theme . '/rtl.css')) {
+                    $css[] = array('u' => $themes_uri . '/' . $theme . '/rtl.css', 'f' => $themes_fs . '/' . $theme . '/rtl.css');
+                }
+            }
+            foreach ($browser_css as $browser) {
+                if (file_exists($themes_fs . '/' . $browser)) {
+                    $css[] = array('u' => $themes_uri . '/' . $browser, 'f' => $themes_fs . '/' . $browser);
+                }
+                if (!empty($theme) &&
+                    file_exists($themes_fs . '/' . $theme . '/' . $browser)) {
+                    $css[] = array('u' => $themes_uri . '/' . $theme . '/' . $browser, 'f' => $themes_fs . '/' . $theme . '/' . $browser);
+                }
+            }
+        }
+
+        return $css;
+    }
+
+    /**
+     * Sets a custom session handler up, if there is one.
+     * If the global variable 'session_cache_limiter' is defined, its value
+     * will override the cache limiter setting found in the configuration
+     * file.
+     *
+     * The custom session handler object will be contained in the global
+     * 'horde_sessionhandler' variable.
+     */
+    static public function setupSessionHandler()
+    {
+        global $conf;
+
+        ini_set('url_rewriter.tags', 0);
+        if (!empty($conf['session']['use_only_cookies'])) {
+            ini_set('session.use_only_cookies', 1);
+            if (!empty($conf['cookie']['domain']) &&
+                strpos($conf['server']['name'], '.') === false) {
+                self::fatal('Session cookies will not work without a FQDN and with a non-empty cookie domain. Either use a fully qualified domain name like "http://www.example.com" instead of "http://example" only, or set the cookie domain in the Horde configuration to an empty value, or enable non-cookie (url-based) sessions in the Horde configuration.', __FILE__, __LINE__);
+            }
+        }
+
+        session_set_cookie_params($conf['session']['timeout'],
+                                  $conf['cookie']['path'], $conf['cookie']['domain'], $conf['use_ssl'] == 1 ? 1 : 0);
+        session_cache_limiter(Horde_Util::nonInputVar('session_cache_limiter', $conf['session']['cache_limiter']));
+        session_name(urlencode($conf['session']['name']));
+
+        $type = !empty($conf['sessionhandler']['type']) ? $conf['sessionhandler']['type'] : 'none';
+        if ($type == 'external') {
+            $calls = $conf['sessionhandler']['params'];
+            session_set_save_handler($calls['open'],
+                                     $calls['close'],
+                                     $calls['read'],
+                                     $calls['write'],
+                                     $calls['destroy'],
+                                     $calls['gc']);
+        } elseif ($type != 'none') {
+            try {
+                $sh = &Horde_SessionHandler::singleton($conf['sessionhandler']['type'], array_merge(self::getDriverConfig('sessionhandler', $conf['sessionhandler']['type']), array('memcache' => !empty($conf['sessionhandler']['memcache']))));
+                ini_set('session.save_handler', 'user');
+                session_set_save_handler(array(&$sh, 'open'),
+                                         array(&$sh, 'close'),
+                                         array(&$sh, 'read'),
+                                         array(&$sh, 'write'),
+                                         array(&$sh, 'destroy'),
+                                         array(&$sh, 'gc'));
+                $GLOBALS['horde_sessionhandler'] = $sh;
+            } catch (Horde_Exception $e) {
+                self::fatal(PEAR::raiseError('Horde is unable to correctly start the custom session handler.'), __FILE__, __LINE__, false);
+            }
+        }
+    }
+
+    /**
+     * Returns an un-used access key from the label given.
+     *
+     * @param string $label     The label to choose an access key from.
+     * @param boolean $nocheck  Don't check if the access key already has been
+     *                          used?
+     *
+     * @return string  A single lower case character access key or empty
+     *                 string if none can be found
+     */
+    static public function getAccessKey($label, $nocheck = false,
+                                        $shutdown = false)
+    {
+        /* Shutdown call for translators? */
+        if ($shutdown) {
+            if (!count(self::$_labels)) {
+                return;
+            }
+            $script = basename($_SERVER['PHP_SELF']);
+            $labels = array_keys(self::$_labels);
+            sort($labels);
+            $used = array_keys(self::$_used);
+            sort($used);
+            $remaining = str_replace($used, array(), 'abcdefghijklmnopqrstuvwxyz');
+            self::logMessage('Access key information for ' . $script, __FILE__, __LINE__);
+            self::logMessage('Used labels: ' . implode(',', $labels), __FILE__, __LINE__);
+            self::logMessage('Used keys: ' . implode('', $used), __FILE__, __LINE__);
+            self::logMessage('Free keys: ' . $remaining, __FILE__, __LINE__);
+            return;
+        }
+
+        /* Use access keys at all? */
+        if (!isset(self::$_noAccessKey)) {
+            self::$_noAccessKey = !$GLOBALS['browser']->hasFeature('accesskey') || !$GLOBALS['prefs']->getValue('widget_accesskey');
+        }
+
+        if (self::$_noAccessKey || !preg_match('/_([A-Za-z])/', $label, $match)) {
+            return '';
+        }
+        $key = $match[1];
+
+        /* Has this key already been used? */
+        if (isset(self::$_used[strtolower($key)]) &&
+            !($nocheck && isset(self::$_labels[$label]))) {
+            return '';
+        }
+
+        /* Save key and label. */
+        self::$_used[strtolower($key)] = true;
+        self::$_labels[$label] = true;
+
+        return $key;
+    }
+
+    /**
+     * Strips an access key from a label.
+     * For multibyte charset strings the access key gets removed completely,
+     * otherwise only the underscore gets removed.
+     *
+     * @param string $label  The label containing an access key.
+     *
+     * @return string  The label with the access key being stripped.
+     */
+    static public function stripAccessKey($label)
+    {
+        if (!isset($GLOBALS['nls'])) {
+            self::loadConfiguration('nls.php', null, 'horde');
+        }
+        $multibyte = isset($GLOBALS['nls']['multibyte'][NLS::getCharset(true)]);
+
+        return preg_replace('/_([A-Za-z])/',
+                            $multibyte && preg_match('/[\x80-\xff]/', $label) ? '' : '\1',
+                            $label);
+    }
+
+    /**
+     * Highlights an access key in a label.
+     *
+     * @param string $label      The label to highlight the access key in.
+     * @param string $accessKey  The access key to highlight.
+     *
+     * @return string  The HTML version of the label with the access key
+     *                 highlighted.
+     */
+    static public function highlightAccessKey($label, $accessKey)
+    {
+        $stripped_label = self::stripAccessKey($label);
+
+        if (empty($accessKey)) {
+            return $stripped_label;
+        }
+
+        if (isset($GLOBALS['nls']['multibyte'][NLS::getCharset(true)])) {
+            /* Prefix parenthesis with the UTF-8 representation of the LRO
+             * (Left-to-Right-Override) Unicode codepoint U+202D. */
+            $prefix = NLS::getCharset() == 'UTF-8' ? "\xe2\x80\xad" : '';
+            return $stripped_label . $prefix . '(<span class="accessKey">'
+                . strtoupper($accessKey) . '</span>' . ')';
+        } else {
+            return str_replace('_' . $accessKey, '<span class="accessKey">' . $accessKey . '</span>', $label);
+        }
+    }
+
+    /**
+     * Returns the appropriate "accesskey" and "title" attributes for an HTML
+     * tag and the given label.
+     *
+     * @param string $label     The title of an HTML element
+     * @param boolean $nocheck  Don't check if the access key already has been
+     *                          used?
+     *
+     * @return string  The title, and if appropriate, the accesskey attributes
+     *                 for the element.
+     */
+    static public function getAccessKeyAndTitle($label, $nocheck = false)
+    {
+        $ak = self::getAccessKey($label, $nocheck);
+        $attributes = 'title="' . self::stripAccessKey($label);
+        if (!empty($ak)) {
+            $attributes .= sprintf(_(" (Accesskey %s)"), $ak);
+            $attributes .= '" accesskey="' . $ak;
+        }
+
+        return $attributes . '"';
+    }
+
+    /**
+     * Returns a label element including an access key for usage in conjuction
+     * with a form field. User preferences regarding access keys are respected.
+     *
+     * @param string $for    The form field's id attribute.
+     * @param string $label  The label text.
+     * @param string $ak     The access key to use. If null a new access key
+     *                       will be generated.
+     *
+     * @return string  The html code for the label element.
+     */
+    static public function label($for, $label, $ak = null)
+    {
+        if (is_null($ak)) {
+            $ak = self::getAccessKey($label, 1);
+        }
+        $label = self::highlightAccessKey($label, $ak);
+
+        return sprintf('<label for="%s"%s>%s</label>',
+                       $for,
+                       !empty($ak) ? ' accesskey="' . $ak . '"' : '',
+                       $label);
+    }
+
+    /**
+     * Redirects to the main Horde login page on authentication failure.
+     */
+    static public function authenticationFailureRedirect()
+    {
+        if (Horde_Cli::runningFromCLI()) {
+            $cli = &Horde_Cli::singleton();
+            $cli->fatal(_("You are not authenticated."));
+        }
+
+        $url = $GLOBALS['registry']->get('webroot', 'horde') . '/login.php';
+        $url = Horde_Util::addParameter($url, array('url' => self::selfUrl(true), 'nosidebar' => 1), null, false);
+        $url = Horde_Auth::addLogoutParameters($url);
+        header('Location: ' . self::url($url, true));
+        exit;
+    }
+
+    /**
+     * Provides a standardised function to call a Horde hook, checking whether
+     * a hook config file exists and whether the function itself exists. If
+     * these two conditions are not satisfied it will return the specified
+     * value (by default a PEAR error).
+     *
+     * @param string $hook  The function to call.
+     * @param array  $args  An array of any arguments to pass to the hook
+     *                      function.
+     * @param string $app   If specified look for hooks in the config directory
+     *                      of this app.
+     * @param mixed $error  What to return if $app/config/hooks.php or $hook
+     *                      does not exist. If this is the string 'PEAR_Error'
+     *                      a PEAR error object is returned instead, detailing
+     *                      the failure.
+     *
+     * @return mixed  Either the results of the hook or PEAR error on failure.
+     */
+    static public function callHook($hook, $args = array(), $app = 'horde',
+                                    $error = 'PEAR_Error')
+    {
+        if (!isset(self::$_hooksLoaded[$app])) {
+            $success = self::loadConfiguration('hooks.php', null, $app);
+            if (is_a($success, 'PEAR_Error')) {
+                self::logMessage($success, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                self::$_hooksLoaded[$app] = false;
+            } else {
+                self::$_hooksLoaded[$app] = true;
+            }
+        }
+
+        if (function_exists($hook)) {
+            $result = call_user_func_array($hook, $args);
+            if (is_a($result, 'PEAR_Error')) {
+                self::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            }
+            return $result;
+        }
+
+        if (is_string($error) && strcmp($error, 'PEAR_Error') == 0) {
+            $error = PEAR::raiseError(sprintf('Hook %s in application %s not called.', $hook, $app));
+            self::logMessage($error, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        }
+
+        return $error;
+    }
+
+    /**
+     * Returns the specified permission for the current user.
+     *
+     * @param string $permission  A permission, currently only 'max_blocks'.
+     *
+     * @return mixed  The value of the specified permission.
+     */
+    static public function hasPermission($permission)
+    {
+        global $perms;
+
+        if (!$perms->exists('horde:' . $permission)) {
+            return true;
+        }
+
+        $allowed = $perms->getPermissions('horde:' . $permission);
+        if (is_array($allowed)) {
+            switch ($permission) {
+            case 'max_blocks':
+                $allowed = max($allowed);
+                break;
+            }
+        }
+
+        return $allowed;
+    }
+
+    /**
+     * Utility function to send redirect headers to browser, handling any
+     * browser quirks.
+     *
+     * @param string $url  The URL to redirect to.
+     */
+    static public function redirect($url)
+    {
+        if ($GLOBALS['browser']->isBrowser('msie') &&
+            ($GLOBALS['conf']['use_ssl'] == 3) &&
+            (strlen($url) < 160)) {
+            header('Refresh: 0; URL=' . $url);
+        } else {
+            header('Location: ' . $url);
+        }
+        exit;
+    }
+
+}
diff --git a/framework/Core/lib/Horde/Horde/Config.php b/framework/Core/lib/Horde/Horde/Config.php
new file mode 100644 (file)
index 0000000..14b5ba2
--- /dev/null
@@ -0,0 +1,1548 @@
+<?php
+/**
+ * The Horde_Config:: package provides a framework for managing the
+ * configuration of Horde applications, writing conf.php files from
+ * conf.xml source files, generating user interfaces, etc.
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package Core
+ */
+class Horde_Config
+{
+    /**
+     * The name of the configured application.
+     *
+     * @var string
+     */
+    protected $_app;
+
+    /**
+     * The XML tree of the configuration file traversed to an
+     * associative array.
+     *
+     * @var array
+     */
+    protected $_xmlConfigTree = null;
+
+    /**
+     * The content of the generated configuration file.
+     *
+     * @var string
+     */
+    protected $_phpConfig;
+
+    /**
+     * The content of the old configuration file.
+     *
+     * @var string
+     */
+    protected $_oldConfig;
+
+    /**
+     * The manual configuration in front of the generated configuration.
+     *
+     * @var string
+     */
+    protected $_preConfig;
+
+    /**
+     * The manual configuration after the generated configuration.
+     *
+     * @var string
+     */
+    protected $_postConfig;
+
+    /**
+     * The current $conf array of the configured application.
+     *
+     * @var array
+     */
+    protected $_currentConfig = array();
+
+    /**
+     * The version tag of the conf.xml file which will be copied into the
+     * conf.php file.
+     *
+     * @var string
+     */
+    protected $_versionTag = '';
+
+    /**
+     * The line marking the begin of the generated configuration.
+     *
+     * @var string
+     */
+    protected $_configBegin = "/* CONFIG START. DO NOT CHANGE ANYTHING IN OR AFTER THIS LINE. */\n";
+
+    /**
+     * The line marking the end of the generated configuration.
+     *
+     * @var string
+     */
+    protected $_configEnd = "/* CONFIG END. DO NOT CHANGE ANYTHING IN OR BEFORE THIS LINE. */\n";
+
+    /**
+     * Constructor.
+     *
+     * @param string $app  The name of the application to be configured.
+     */
+    public function __construct($app)
+    {
+        $this->_app = $app;
+    }
+
+    /**
+     * Reads the application's conf.xml file and builds an associative array
+     * from its XML tree.
+     *
+     * @param array $custom_conf  Any settings that shall be included in the
+     *                            generated configuration.
+     *
+     * @return array  An associative array representing the configuration
+     *                tree.
+     */
+    public function readXMLConfig($custom_conf = null)
+    {
+        if (!is_null($this->_xmlConfigTree) && !$custom_conf) {
+            return $this->_xmlConfigTree;
+        }
+
+        $path = $GLOBALS['registry']->get('fileroot', $this->_app) . '/config';
+
+        if ($custom_conf) {
+            $this->_currentConfig = $custom_conf;
+        } else {
+            /* Fetch the current conf.php contents. */
+            @eval($this->getPHPConfig());
+            if (isset($conf)) {
+                $this->_currentConfig = $conf;
+            }
+        }
+
+        /* Load the DOM object. */
+        include_once 'Horde/DOM.php';
+        $doc = Horde_DOM_Document::factory(array('filename' => $path . '/conf.xml'));
+
+        /* Check if there is a CVS version tag and store it. */
+        $node = $doc->first_child();
+        while (!empty($node)) {
+            if ($node->type == XML_COMMENT_NODE) {
+                // @TODO: Old CVS tag
+                if (preg_match('/\$.*?conf\.xml,v .*? .*\$/', $node->node_value(), $match) ||
+                // New Git tag
+                    preg_match('/\$Id:\s*[0-9a-f]+\s*\$/', $node->node_value(), $match)) {
+                    $this->_versionTag = $match[0] . "\n";
+                    break;
+                }
+            }
+            $node = $node->next_sibling();
+        }
+
+        /* Parse the config file. */
+        $this->_xmlConfigTree = array();
+        $root = $doc->root();
+        if ($root->has_child_nodes()) {
+            $this->_parseLevel($this->_xmlConfigTree, $root->child_nodes(), '');
+        }
+
+        return $this->_xmlConfigTree;
+    }
+
+    /**
+     * Returns the file content of the current configuration file.
+     *
+     * @return string  The unparsed configuration file content.
+     */
+    public function getPHPConfig()
+    {
+        if (!is_null($this->_oldConfig)) {
+            return $this->_oldConfig;
+        }
+
+        $path = $GLOBALS['registry']->get('fileroot', $this->_app) . '/config';
+        if (file_exists($path . '/conf.php')) {
+            $this->_oldConfig = file_get_contents($path . '/conf.php');
+            if (!empty($this->_oldConfig)) {
+                $this->_oldConfig = preg_replace('/<\?php\n?/', '', $this->_oldConfig);
+                $pos = strpos($this->_oldConfig, $this->_configBegin);
+                if ($pos !== false) {
+                    $this->_preConfig = substr($this->_oldConfig, 0, $pos);
+                    $this->_oldConfig = substr($this->_oldConfig, $pos);
+                }
+                $pos = strpos($this->_oldConfig, $this->_configEnd);
+                if ($pos !== false) {
+                    $this->_postConfig = substr($this->_oldConfig, $pos + strlen($this->_configEnd));
+                    $this->_oldConfig = substr($this->_oldConfig, 0, $pos);
+                }
+            }
+        } else {
+            $this->_oldConfig = '';
+        }
+
+        return $this->_oldConfig;
+    }
+
+    /**
+     * Generates the content of the application's configuration file.
+     *
+     * @param Horde_Variables $formvars  The processed configuration form
+     *                                   data.
+     * @param array $custom_conf         Any settings that shall be included
+     *                                   in the generated configuration.
+     *
+     * @return string  The content of the generated configuration file.
+     */
+    public function generatePHPConfig($formvars, $custom_conf = null)
+    {
+        $this->readXMLConfig($custom_conf);
+        $this->getPHPConfig();
+
+        $this->_phpConfig = "<?php\n" . $this->_preConfig . $this->_configBegin;
+        if (!empty($this->_versionTag)) {
+            $this->_phpConfig .= '// ' . $this->_versionTag;
+        }
+        $this->_generatePHPConfig($this->_xmlConfigTree, '', $formvars);
+        $this->_phpConfig .= $this->_configEnd . $this->_postConfig;
+
+        return $this->_phpConfig;
+    }
+
+    /**
+     * Generates the configuration file items for a part of the configuration
+     * tree.
+     *
+     * @param array $section             An associative array containing the
+     *                                   part of the traversed XML
+     *                                   configuration tree that should be
+     *                                   processed.
+     * @param string $prefix             A configuration prefix determining
+     *                                   the current position inside the
+     *                                   configuration file. This prefix will
+     *                                   be translated to keys of the $conf
+     *                                   array in the generated configuration
+     *                                   file.
+     * @param Horde_Variables $formvars  The processed configuration form
+     *                                   data.
+     */
+    protected function _generatePHPConfig($section, $prefix, $formvars)
+    {
+        if (!is_array($section)) {
+            return;
+        }
+
+        foreach ($section as $name => $configitem) {
+            $prefixedname = empty($prefix)
+                ? $name
+                : $prefix . '|' . $name;
+            $configname = str_replace('|', '__', $prefixedname);
+            $quote = (!isset($configitem['quote']) || $configitem['quote'] !== false);
+
+            if ($configitem == 'placeholder') {
+                $this->_phpConfig .= '$conf[\'' . str_replace('|', '\'][\'', $prefix) . "'] = array();\n";
+            } elseif (isset($configitem['switch'])) {
+                $val = $formvars->getExists($configname, $wasset);
+                if (!$wasset) {
+                    $val = isset($configitem['default']) ? $configitem['default'] : null;
+                }
+                if (isset($configitem['switch'][$val])) {
+                    $value = $val;
+                    if ($quote && $value != 'true' && $value != 'false') {
+                        $value = "'" . $value . "'";
+                    }
+                    $this->_generatePHPConfig($configitem['switch'][$val]['fields'], $prefix, $formvars);
+                }
+            } elseif (isset($configitem['_type'])) {
+                $val = $formvars->getExists($configname, $wasset);
+                if (!$wasset) {
+                    $val = isset($configitem['default']) ? $configitem['default'] : null;
+                }
+
+                $type = $configitem['_type'];
+                switch ($type) {
+                case 'multienum':
+                    if (is_array($val)) {
+                        $encvals = array();
+                        foreach ($val as $v) {
+                            $encvals[] = $this->_quote($v);
+                        }
+                        $arrayval = "'" . implode('\', \'', $encvals) . "'";
+                        if ($arrayval == "''") {
+                            $arrayval = '';
+                        }
+                    } else {
+                        $arrayval = '';
+                    }
+                    $value = 'array(' . $arrayval . ')';
+                    break;
+
+                case 'boolean':
+                    if (is_bool($val)) {
+                        $value = $val ? 'true' : 'false';
+                    } else {
+                        $value = ($val == 'on') ? 'true' : 'false';
+                    }
+                    break;
+
+                case 'stringlist':
+                    $values = explode(',', $val);
+                    if (!is_array($values)) {
+                        $value = "array('" . $this->_quote(trim($values)) . "')";
+                    } else {
+                        $encvals = array();
+                        foreach ($values as $v) {
+                            $encvals[] = $this->_quote(trim($v));
+                        }
+                        $arrayval = "'" . implode('\', \'', $encvals) . "'";
+                        if ($arrayval == "''") {
+                            $arrayval = '';
+                        }
+                        $value = 'array(' . $arrayval . ')';
+                    }
+                    break;
+
+                case 'int':
+                    if ($val !== '') {
+                        $value = (int)$val;
+                    }
+                    break;
+
+                case 'octal':
+                    $value = sprintf('0%o', octdec($val));
+                    break;
+
+                case 'header':
+                case 'description':
+                    break;
+
+                default:
+                    if ($val != '') {
+                        $value = $val;
+                        if ($quote && $value != 'true' && $value != 'false') {
+                            $value = "'" . $this->_quote($value) . "'";
+                        }
+                    }
+                    break;
+                }
+            } else {
+                $this->_generatePHPConfig($configitem, $prefixedname, $formvars);
+            }
+
+            if (isset($value)) {
+                $this->_phpConfig .= '$conf[\'' . str_replace('__', '\'][\'', $configname) . '\'] = ' . $value . ";\n";
+            }
+            unset($value);
+        }
+    }
+
+    /**
+     * Parses one level of the configuration XML tree into the associative
+     * array containing the traversed configuration tree.
+     *
+     * @param array &$conf     The already existing array where the processed
+     *                         XML tree portion should be appended to.
+     * @param array $children  An array containing the XML nodes of the level
+     *                         that should be parsed.
+     * @param string $ctx      A string representing the current position
+     *                         (context prefix) inside the configuration XML
+     *                         file.
+     */
+    protected function _parseLevel(&$conf, $children, $ctx)
+    {
+        reset($children);
+        while (list(,$node) = each($children)) {
+            if ($node->type != XML_ELEMENT_NODE) {
+                continue;
+            }
+            $name = $node->get_attribute('name');
+            $desc = Horde_Text_Filter::filter($node->get_attribute('desc'), 'linkurls', array('callback' => 'Horde::externalUrl'));
+            $required = !($node->get_attribute('required') == 'false');
+            $quote = !($node->get_attribute('quote') == 'false');
+
+            $curctx = empty($ctx)
+                ? $name
+                : $ctx . '|' . $name;
+
+            switch ($node->tagname) {
+            case 'configdescription':
+                if (empty($name)) {
+                    $name = hash('md5', uniqid(mt_rand(), true));
+                }
+
+                $conf[$name] = array(
+                    '_type' => 'description',
+                    'desc' => Horde_Text_Filter::filter($this->_default($curctx, $this->_getNodeOnlyText($node)), 'linkurls', array('callback' => 'Horde::externalUrl'))
+                );
+                break;
+
+            case 'configheader':
+                if (empty($name)) {
+                    $name = hash('md5', uniqid(mt_rand(), true));
+                }
+
+                $conf[$name] = array(
+                    '_type' => 'header',
+                    'desc' => $this->_default($curctx, $this->_getNodeOnlyText($node))
+                );
+                break;
+
+            case 'configswitch':
+                $values = $this->_getSwitchValues($node, $ctx);
+                list($default, $isDefault) = $quote
+                    ? $this->__default($curctx, $this->_getNodeOnlyText($node))
+                    : $this->__defaultRaw($curctx, $this->_getNodeOnlyText($node));
+
+                if ($default === '') {
+                    $default = key($values);
+                }
+
+                if (is_bool($default)) {
+                    $default = $default ? 'true' : 'false';
+                }
+
+                $conf[$name] = array(
+                    'desc' => $desc,
+                    'switch' => $values,
+                    'default' => $default,
+                    'is_default' => $isDefault
+                );
+                break;
+
+            case 'configenum':
+                $values = $this->_getEnumValues($node);
+                list($default, $isDefault) = $quote
+                    ? $this->__default($curctx, $this->_getNodeOnlyText($node))
+                    : $this->__defaultRaw($curctx, $this->_getNodeOnlyText($node));
+
+                if ($default === '') {
+                    $default = key($values);
+                }
+
+                if (is_bool($default)) {
+                    $default = $default ? 'true' : 'false';
+                }
+
+                $conf[$name] = array(
+                    '_type' => 'enum',
+                    'required' => $required,
+                    'quote' => $quote,
+                    'values' => $values,
+                    'desc' => $desc,
+                    'default' => $default,
+                    'is_default' => $isDefault
+                );
+                break;
+
+            case 'configlist':
+                list($default, $isDefault) = $this->__default($curctx, null);
+
+                if (is_null($default)) {
+                    $default = $this->_getNodeOnlyText($node);
+                } elseif (is_array($default)) {
+                    $default = implode(', ', $default);
+                }
+
+                $conf[$name] = array(
+                    '_type' => 'stringlist',
+                    'required' => $required,
+                    'desc' => $desc,
+                    'default' => $default,
+                    'is_default' => $isDefault
+                );
+                break;
+
+            case 'configmultienum':
+                $values = $this->_getEnumValues($node);
+                list($default, $isDefault) = $this->__default($curctx, explode(',', $this->_getNodeOnlyText($node)));
+
+                $conf[$name] = array(
+                    '_type' => 'multienum',
+                    'required' => $required,
+                    'values' => $values,
+                    'desc' => $desc,
+                    'default' => Horde_Array::valuesToKeys($default),
+                    'is_default' => $isDefault
+                );
+                break;
+
+            case 'configpassword':
+                $conf[$name] = array(
+                    '_type' => 'password',
+                    'required' => $required,
+                    'desc' => $desc,
+                    'default' => $this->_default($curctx, $this->_getNodeOnlyText($node)),
+                    'is_default' => $this->_isDefault($curctx, $this->_getNodeOnlyText($node))
+                );
+                break;
+
+            case 'configstring':
+                $conf[$name] = array(
+                    '_type' => 'text',
+                    'required' => $required,
+                    'desc' => $desc,
+                    'default' => $this->_default($curctx, $this->_getNodeOnlyText($node)),
+                    'is_default' => $this->_isDefault($curctx, $this->_getNodeOnlyText($node))
+                );
+
+                if ($conf[$name]['default'] === false) {
+                    $conf[$name]['default'] = 'false';
+                } elseif ($conf[$name]['default'] === true) {
+                    $conf[$name]['default'] = 'true';
+                }
+                break;
+
+            case 'configboolean':
+                $default = $this->_getNodeOnlyText($node);
+                $default = !(empty($default) || $default === 'false');
+
+                $conf[$name] = array(
+                    '_type' => 'boolean',
+                    'required' => $required,
+                    'desc' => $desc,
+                    'default' => $this->_default($curctx, $default),
+                    'is_default' => $this->_isDefault($curctx, $default)
+                );
+                break;
+
+            case 'configinteger':
+                $values = $this->_getEnumValues($node);
+
+                $conf[$name] = array(
+                    '_type' => 'int',
+                    'required' => $required,
+                    'values' => $values,
+                    'desc' => $desc,
+                    'default' => $this->_default($curctx, $this->_getNodeOnlyText($node)),
+                    'is_default' => $this->_isDefault($curctx, $this->_getNodeOnlyText($node))
+                );
+
+                if ($node->get_attribute('octal') == 'true' &&
+                    $conf[$name]['default'] != '') {
+                    $conf[$name]['_type'] = 'octal';
+                    $conf[$name]['default'] = sprintf('0%o', $this->_default($curctx, octdec($this->_getNodeOnlyText($node))));
+                }
+                break;
+
+            case 'configphp':
+                $conf[$name] = array(
+                    '_type' => 'php',
+                    'required' => $required,
+                    'quote' => false,
+                    'desc' => $desc,
+                    'default' => $this->_defaultRaw($curctx, $this->_getNodeOnlyText($node)),
+                    'is_default' => $this->_isDefaultRaw($curctx, $this->_getNodeOnlyText($node))
+                );
+                break;
+
+            case 'configsecret':
+                $conf[$name] = array(
+                    '_type' => 'text',
+                    'required' => true,
+                    'desc' => $desc,
+                    'default' => $this->_default($curctx, sha1(uniqid(mt_rand(), true))),
+                    'is_default' => $this->_isDefault($curctx, $this->_getNodeOnlyText($node))
+                );
+                break;
+
+            case 'configsql':
+                $conf[$node->get_attribute('switchname')] = $this->_configSQL($ctx, $node);
+                break;
+
+            case 'configvfs':
+                $conf[$node->get_attribute('switchname')] = $this->_configVFS($ctx, $node);
+                break;
+
+            case 'configsection':
+                $conf[$name] = array();
+                $cur = &$conf[$name];
+                if ($node->has_child_nodes()) {
+                    $this->_parseLevel($cur, $node->child_nodes(), $curctx);
+                }
+                break;
+
+            case 'configtab':
+                $key = hash('md5', uniqid(mt_rand(), true));
+
+                $conf[$key] = array(
+                    'tab' => $name,
+                    'desc' => $desc
+                );
+
+                if ($node->has_child_nodes()) {
+                    $this->_parseLevel($conf, $node->child_nodes(), $ctx);
+                }
+                break;
+
+            case 'configplaceholder':
+                $conf[hash('md5', uniqid(mt_rand(), true))] = 'placeholder';
+                break;
+
+            default:
+                $conf[$name] = array();
+                $cur = &$conf[$name];
+                if ($node->has_child_nodes()) {
+                    $this->_parseLevel($cur, $node->child_nodes(), $curctx);
+                }
+                break;
+            }
+        }
+    }
+
+    /**
+     * Returns the configuration tree for an SQL backend configuration to
+     * replace a <configsql> tag.
+     * Subnodes will be parsed and added to both the Horde defaults and the
+     * Custom configuration parts.
+     *
+     * @param string $ctx         The context of the <configsql> tag.
+     * @param DomNode $node       The DomNode representation of the <configsql>
+     *                            tag.
+     * @param string $switchname  If DomNode is not set, the value of the
+     *                            tag's switchname attribute.
+     *
+     * @return array  An associative array with the SQL configuration tree.
+     */
+    protected function _configSQL($ctx, $node = null,
+                                  $switchname = 'driverconfig')
+    {
+        $persistent = array(
+            '_type' => 'boolean',
+            'required' => false,
+            'desc' => 'Request persistent connections?',
+            'default' => $this->_default($ctx . '|persistent', false)
+        );
+
+        $hostspec = array(
+            '_type' => 'text',
+            'required' => true,
+            'desc' => 'Database server/host',
+            'default' => $this->_default($ctx . '|hostspec', '')
+        );
+
+        $username = array(
+            '_type' => 'text',
+            'required' => true,
+            'desc' => 'Username to connect to the database as',
+            'default' => $this->_default($ctx . '|username', '')
+        );
+
+        $password = array(
+            '_type' => 'text',
+            'required' => false,
+            'desc' => 'Password to connect with',
+            'default' => $this->_default($ctx . '|password', '')
+        );
+
+        $database = array(
+            '_type' => 'text',
+            'required' => true,
+            'desc' => 'Database name to use',
+            'default' => $this->_default($ctx . '|database', '')
+        );
+
+        $socket = array(
+            '_type' => 'text',
+            'required' => false,
+            'desc' => 'Location of UNIX socket',
+            'default' => $this->_default($ctx . '|socket', '')
+        );
+
+        $port = array(
+            '_type' => 'int',
+            'required' => false,
+            'desc' => 'Port the DB is running on, if non-standard',
+            'default' => $this->_default($ctx . '|port', null)
+        );
+
+        $protocol = array(
+            'desc' => 'How should we connect to the database?',
+            'default' => $this->_default($ctx . '|protocol', 'unix'),
+            'switch' => array(
+                'unix' => array(
+                    'desc' => 'UNIX Sockets',
+                    'fields' => array(
+                        'socket' => $socket
+                    )
+                ),
+                'tcp' => array(
+                    'desc' => 'TCP/IP',
+                    'fields' => array(
+                        'hostspec' => $hostspec,
+                        'port' => $port
+                    )
+                )
+            )
+        );
+
+        $mysql_protocol = $protocol;
+        $mysql_protocol['switch']['tcp']['fields']['port']['default'] = $this->_default($ctx . '|port', 3306);
+
+        $charset = array(
+            '_type' => 'text',
+            'required' => true,
+            'desc' => 'Internally used charset',
+            'default' => $this->_default($ctx . '|charset', 'utf-8')
+        );
+
+        $ssl = array(
+            '_type' => 'boolean',
+            'required' => false,
+            'desc' => 'Use SSL to connect to the server?',
+            'default' => $this->_default($ctx . '|ssl', false)
+        );
+
+        $ca = array(
+            '_type' => 'text',
+            'required' => false,
+            'desc' => 'Certification Authority to use for SSL connections',
+            'default' => $this->_default($ctx . '|ca', '')
+        );
+
+        $oci8_fields = array(
+            'persistent' => $persistent,
+            'username' => $username,
+            'password' => $password
+        );
+        if (function_exists('oci_connect')) {
+            $oci8_fields['database'] = array(
+                '_type' => 'text',
+                'required' => true,
+                'desc' => 'Database name or Easy Connect parameter',
+                'default' => $this->_default($ctx . '|database', 'horde')
+            );
+        } else {
+            $oci8_fields['hostspec'] = array(
+                '_type' => 'text',
+                'required' => true,
+                'desc' => 'Database name or Easy Connect parameter',
+                'default' => $this->_default($ctx . '|hostspec', 'horde')
+            );
+        }
+        $oci8_fields['charset'] = $charset;
+
+        $read_hostspec = array(
+            '_type' => 'text',
+            'required' => true,
+            'desc' => 'Read database server/host',
+            'default' => $this->_default($ctx . '|read|hostspec', '')
+        );
+
+        $read_port = array(
+            '_type' => 'int',
+            'required' => false,
+            'desc' => 'Port the read DB is running on, if non-standard',
+            'default' => $this->_default($ctx . '|read|port', null)
+        );
+
+        $splitread = array(
+            '_type' => 'boolean',
+            'required' => false,
+            'desc' => 'Split reads to a different server?',
+            'default' => $this->_default($ctx . '|splitread', 'false'),
+            'switch' => array(
+                'false' => array(
+                    'desc' => 'Disabled',
+                    'fields' => array()
+                ),
+                'true' => array(
+                    'desc' => 'Enabled',
+                    'fields' => array(
+                        'read' => array(
+                            'persistent' => $persistent,
+                            'username' => $username,
+                            'password' => $password,
+                            'protocol' => $mysql_protocol,
+                            'database' => $database,
+                            'charset' => $charset
+                        )
+                    )
+                )
+            )
+        );
+
+        $custom_fields = array(
+            'required' => true,
+            'desc' => 'What database backend should we use?',
+            'default' => $this->_default($ctx . '|phptype', 'false'),
+            'switch' => array(
+                'false' => array(
+                    'desc' => '[None]',
+                    'fields' => array()
+                ),
+                'dbase' => array(
+                    'desc' => 'dBase',
+                    'fields' => array(
+                        'database' => array(
+                            '_type' => 'text',
+                            'required' => true,
+                            'desc' => 'Absolute path to the database file',
+                            'default' => $this->_default($ctx . '|database', '')
+                        ),
+                        'mode' => array(
+                            '_type' => 'enum',
+                            'desc' => 'The mode to open the file with',
+                            'values' => array(
+                                0 => 'Read Only',
+                                2 => 'Read Write'),
+                            'default' => $this->_default($ctx . '|mode', 2)
+                        ),
+                        'charset' => $charset
+                    )
+                ),
+                'ibase' => array(
+                    'desc' => 'Firebird/InterBase',
+                    'fields' => array(
+                        'dbsyntax' => array(
+                            '_type' => 'enum',
+                            'desc' => 'The database syntax variant to use',
+                            'required' => false,
+                            'values' => array(
+                                'ibase' => 'InterBase',
+                                'firebird' => 'Firebird'
+                            ),
+                            'default' => $this->_default($ctx . '|dbsyntax', 'firebird')
+                        ),
+                        'persistent' => $persistent,
+                        'hostspec' => $hostspec,
+                        'username' => $username,
+                        'password' => $password,
+                        'database' => $database,
+                        'buffers' => array(
+                            '_type' => 'int',
+                            'desc' => 'The number of database buffers to allocate',
+                            'required' => false,
+                            'default' => $this->_default($ctx . '|buffers', null)
+                        ),
+                        'dialect' => array(
+                            '_type' => 'int',
+                            'desc' => 'The default SQL dialect for any statement executed within a connection.',
+                            'required' => false,
+                            'default' => $this->_default($ctx . '|dialect', null)
+                        ),
+                        'role' => array(
+                            '_type' => 'text',
+                            'desc' => 'Role',
+                            'required' => false,
+                            'default' => $this->_default($ctx . '|role', null)),
+                        'charset' => $charset
+                    )
+                ),
+                'fbsql' => array(
+                    'desc' => 'Frontbase',
+                    'fields' => array(
+                        'persistent' => $persistent,
+                        'hostspec' => $hostspec,
+                        'username' => $username,
+                        'password' => $password,
+                        'database' => $database,
+                        'charset' => $charset
+                    )
+                ),
+                'ifx' => array(
+                    'desc' => 'Informix',
+                    'fields' => array(
+                        'persistent' => $persistent,
+                        'username' => $username,
+                        'password' => $password,
+                        'database' => $database,
+                        'charset' => $charset
+                    )
+                ),
+                'msql' => array(
+                    'desc' => 'mSQL',
+                    'fields' => array(
+                        'persistent' => $persistent,
+                        'hostspec' => $hostspec,
+                        'username' => $username,
+                        'password' => $password,
+                        'port' => $port,
+                        'database' => $database,
+                        'charset' => $charset
+                    )
+                ),
+                'mssql' => array(
+                    'desc' => 'MS SQL Server',
+                    'fields' => array(
+                        'persistent' => $persistent,
+                        'hostspec' => $hostspec,
+                        'username' => $username,
+                        'password' => $password,
+                        'port' => $port,
+                        'database' => $database,
+                        'charset' => $charset
+                    )
+                ),
+                'mysql' => array(
+                    'desc' => 'MySQL',
+                    'fields' => array(
+                        'persistent' => $persistent,
+                        'username' => $username,
+                        'password' => $password,
+                        'protocol' => $mysql_protocol,
+                        'database' => $database,
+                        'charset' => $charset,
+                        'ssl' => $ssl,
+                        'ca' => $ca,
+                        'splitread' => $splitread
+                    )
+                ),
+                'mysqli' => array(
+                    'desc' => 'MySQL (mysqli)',
+                    'fields' => array(
+                        'username' => $username,
+                        'password' => $password,
+                        'protocol' => $mysql_protocol,
+                        'database' => $database,
+                        'charset' => $charset,
+                        'splitread' => $splitread,
+                        'ssl' => $ssl,
+                        'ca' => $ca
+                )),
+                'oci8' => array(
+                    'desc' => 'Oracle',
+                    'fields' => $oci8_fields
+                ),
+                'odbc' => array(
+                    'desc' => 'ODBC',
+                    'fields' => array(
+                        'persistent' => $persistent,
+                        'username' => $username,
+                        'password' => $password,
+                        'hostspec' => array(
+                            '_type' => 'text',
+                            'desc' => 'DSN',
+                            'default' => $this->_default($ctx . '|hostspec', '')
+                        ),
+                        'dbsyntax' => array(
+                            '_type' => 'enum',
+                            'desc' => 'The database syntax variant to use',
+                            'required' => false,
+                            'values' => array(
+                                'sql92' => 'SQL92',
+                                'access' => 'Access',
+                                'db2' => 'DB2',
+                                'solid' => 'Solid',
+                                'navision' => 'Navision',
+                                'mssql' => 'MS SQL Server',
+                                'sybase' => 'Sybase',
+                                'mysql' => 'MySQL',
+                                'mysqli' => 'MySQL (mysqli)',
+                            ),
+                            'default' => $this->_default($ctx . '|dbsyntax', 'sql92')
+                        ),
+                        'cursor' => array(
+                            '_type' => 'enum',
+                            'desc' => 'Cursor type',
+                            'quote' => false,
+                            'required' => false,
+                            'values' => array(
+                                'null' => 'None',
+                                'SQL_CUR_DEFAULT' => 'Default',
+                                'SQL_CUR_USE_DRIVER' => 'Use Driver',
+                                'SQL_CUR_USE_ODBC' => 'Use ODBC',
+                                'SQL_CUR_USE_IF_NEEDED' => 'Use If Needed'
+                            ),
+                            'default' => $this->_default($ctx . '|cursor', null)
+                        ),
+                        'charset' => $charset
+                    )
+                ),
+                'pgsql' => array(
+                    'desc' => 'PostgreSQL',
+                    'fields' => array(
+                        'persistent' => $persistent,
+                        'username' => $username,
+                        'password' => $password,
+                        'protocol' => $protocol,
+                        'database' => $database,
+                        'charset' => $charset
+                    )
+                ),
+                'sqlite' => array(
+                    'desc' => 'SQLite',
+                    'fields' => array(
+                        'database' => array(
+                            '_type' => 'text',
+                            'required' => true,
+                            'desc' => 'Absolute path to the database file',
+                            'default' => $this->_default($ctx . '|database', '')
+                        ),
+                        'mode' => array(
+                            '_type' => 'text',
+                            'desc' => 'The mode to open the file with',
+                            'default' => $this->_default($ctx . '|mode', '0644')
+                        ),
+                        'charset' => $charset
+                    )
+                ),
+                'sybase' => array(
+                    'desc' => 'Sybase',
+                    'fields' => array(
+                        'persistent' => $persistent,
+                        'hostspec' => $hostspec,
+                        'username' => $username,
+                        'password' => $password,
+                        'database' => $database,
+                        'appname' => array(
+                            '_type' => 'text',
+                            'desc' => 'Application Name',
+                            'required' => false,
+                            'default' => $this->_default($ctx . '|appname', '')
+                        ),
+                        'charset' => $charset
+                    )
+                )
+            )
+        );
+
+        if (isset($node) && $node->get_attribute('baseconfig') == 'true') {
+            return $custom_fields;
+        }
+
+        list($default, $isDefault) = $this->__default($ctx . '|' . (isset($node) ? $node->get_attribute('switchname') : $switchname), 'horde');
+        $config = array(
+            'desc' => 'Driver configuration',
+            'default' => $default,
+            'is_default' => $isDefault,
+            'switch' => array(
+                'horde' => array(
+                    'desc' => 'Horde defaults',
+                    'fields' => array()
+                ),
+                'custom' => array(
+                    'desc' => 'Custom parameters',
+                    'fields' => array(
+                        'phptype' => $custom_fields
+                    )
+                )
+            )
+        );
+
+        if (isset($node) && $node->has_child_nodes()) {
+            $cur = array();
+            $this->_parseLevel($cur, $node->child_nodes(), $ctx);
+            $config['switch']['horde']['fields'] = array_merge($config['switch']['horde']['fields'], $cur);
+            $config['switch']['custom']['fields'] = array_merge($config['switch']['custom']['fields'], $cur);
+        }
+
+        return $config;
+    }
+
+    /**
+     * Returns the configuration tree for a VFS backend configuration to
+     * replace a <configvfs> tag.
+     * Subnodes will be parsed and added to both the Horde defaults and the
+     * Custom configuration parts.
+     *
+     * @param string $ctx    The context of the <configvfs> tag.
+     * @param DomNode $node  The DomNode representation of the <configvfs>
+     *                       tag.
+     *
+     * @return array  An associative array with the VFS configuration tree.
+     */
+    protected function _configVFS($ctx, $node)
+    {
+        $sql = $this->_configSQL($ctx . '|params');
+        $default = $node->get_attribute('default');
+        $default = empty($default) ? 'none' : $default;
+        list($default, $isDefault) = $this->__default($ctx . '|' . $node->get_attribute('switchname'), $default);
+
+        $config = array(
+            'desc' => 'What VFS driver should we use?',
+            'default' => $default,
+            'is_default' => $isDefault,
+            'switch' => array(
+                'none' => array(
+                    'desc' => 'None',
+                    'fields' => array()
+                ),
+                'file' => array(
+                    'desc' => 'Files on the local system',
+                    'fields' => array(
+                        'params' => array(
+                            'vfsroot' => array(
+                                '_type' => 'text',
+                                'desc' => 'Where on the real filesystem should Horde use as root of the virtual filesystem?',
+                                'default' => $this->_default($ctx . '|params|vfsroot', '/tmp')
+                            )
+                        )
+                    )
+                ),
+                'sql' => array(
+                    'desc' => 'SQL database',
+                    'fields' => array(
+                        'params' => array(
+                            'driverconfig' => $sql
+                        )
+                    )
+                )
+            )
+        );
+
+        if (isset($node) && $node->get_attribute('baseconfig') != 'true') {
+            $config['switch']['horde'] = array(
+                'desc' => 'Horde defaults',
+                'fields' => array()
+            );
+        }
+        $cases = $this->_getSwitchValues($node, $ctx . '|params');
+        foreach ($cases as $case => $fields) {
+            if (isset($config['switch'][$case])) {
+                $config['switch'][$case]['fields']['params'] = array_merge($config['switch'][$case]['fields']['params'], $fields['fields']);
+            }
+        }
+
+        return $config;
+    }
+
+    /**
+     * Returns a certain value from the current configuration array or
+     * a default value, if not found.
+     *
+     * @param string $ctx     A string representing the key of the
+     *                        configuration array to return.
+     * @param mixed $default  The default value to return if the key wasn't
+     *                        found.
+     *
+     * @return mixed  Either the value of the configuration array's requested
+     *                key or the default value if the key wasn't found.
+     */
+    protected function _default($ctx, $default)
+    {
+        list ($ptr,) = $this->__default($ctx, $default);
+        return $ptr;
+    }
+
+    /**
+     * Returns whether a certain value from the current configuration array
+     * exists or a default value will be used.
+     *
+     * @param string $ctx     A string representing the key of the
+     *                        configuration array to return.
+     * @param mixed $default  The default value to return if the key wasn't
+     *                        found.
+     *
+     * @return boolean  Whether the default value will be used.
+     */
+    protected function _isDefault($ctx, $default)
+    {
+        list (,$isDefault) = $this->__default($ctx, $default);
+        return $isDefault;
+    }
+
+    /**
+     * Returns a certain value from the current configuration array or a
+     * default value, if not found, and which of the values have been
+     * returned.
+     *
+     * @param string $ctx     A string representing the key of the
+     *                        configuration array to return.
+     * @param mixed $default  The default value to return if the key wasn't
+     *                        found.
+     *
+     * @return array  First element: either the value of the configuration
+     *                array's requested key or the default value if the key
+     *                wasn't found.
+     *                Second element: whether the returned value was the
+     *                default value.
+     */
+    protected function __default($ctx, $default)
+    {
+        $ctx = explode('|', $ctx);
+        $ptr = $this->_currentConfig;
+
+        for ($i = 0; $i < count($ctx); ++$i) {
+            if (!isset($ptr[$ctx[$i]])) {
+                return array($default, true);
+            }
+
+            $ptr = $ptr[$ctx[$i]];
+        }
+
+        if (is_string($ptr)) {
+            $ptr = Horde_String::convertCharset($ptr, 'iso-8859-1');
+        }
+
+        return array($ptr, false);
+    }
+
+    /**
+     * Returns a certain value from the current configuration file or
+     * a default value, if not found.
+     * It does NOT return the actual value, but the PHP expression as used
+     * in the configuration file.
+     *
+     * @param string $ctx     A string representing the key of the
+     *                        configuration array to return.
+     * @param mixed $default  The default value to return if the key wasn't
+     *                        found.
+     *
+     * @return mixed  Either the value of the configuration file's requested
+     *                key or the default value if the key wasn't found.
+     */
+    protected function _defaultRaw($ctx, $default)
+    {
+        list ($ptr,) = $this->__defaultRaw($ctx, $default);
+        return $ptr;
+    }
+
+    /**
+     * Returns whether a certain value from the current configuration array
+     * exists or a default value will be used.
+     *
+     * @param string $ctx     A string representing the key of the
+     *                        configuration array to return.
+     * @param mixed $default  The default value to return if the key wasn't
+     *                        found.
+     *
+     * @return boolean  Whether the default value will be used.
+     */
+    protected function _isDefaultRaw($ctx, $default)
+    {
+        list (,$isDefault) = $this->__defaultRaw($ctx, $default);
+        return $isDefault;
+    }
+
+    /**
+     * Returns a certain value from the current configuration file or
+     * a default value, if not found, and which of the values have been
+     * returned.
+     *
+     * It does NOT return the actual value, but the PHP expression as used
+     * in the configuration file.
+     *
+     * @param string $ctx     A string representing the key of the
+     *                        configuration array to return.
+     * @param mixed $default  The default value to return if the key wasn't
+     *                        found.
+     *
+     * @return array  First element: either the value of the configuration
+     *                array's requested key or the default value if the key
+     *                wasn't found.
+     *                Second element: whether the returned value was the
+     *                default value.
+     */
+    protected function __defaultRaw($ctx, $default)
+    {
+        $ctx = explode('|', $ctx);
+        $pattern = '/^\$conf\[\'' . implode("'\]\['", $ctx) . '\'\] = (.*);\r?$/m';
+
+        return preg_match($pattern, $this->getPHPConfig(), $matches)
+            ? array($matches[1], false)
+            : array($default, true);
+    }
+
+    /**
+     * Returns the content of all text node children of the specified node.
+     *
+     * @param DomNode $node  A DomNode object whose text node children to
+     *                       return.
+     *
+     * @return string  The concatenated values of all text nodes.
+     */
+    protected function _getNodeOnlyText($node)
+    {
+        $text = '';
+
+        if (!$node->has_child_nodes()) {
+            return $node->get_content();
+        }
+
+        foreach ($node->child_nodes() as $tnode) {
+            if ($tnode->type == XML_TEXT_NODE) {
+                $text .= $tnode->content;
+            }
+        }
+
+        return trim($text);
+    }
+
+    /**
+     * Returns an associative array containing all possible values of the
+     * specified <configenum> tag.
+     *
+     * The keys contain the actual enum values while the values contain their
+     * corresponding descriptions.
+     *
+     * @param DomNode $node  The DomNode representation of the <configenum>
+     *                       tag whose values should be returned.
+     *
+     * @return array  An associative array with all possible enum values.
+     */
+    protected function _getEnumValues($node)
+    {
+        $values = array();
+
+        if (!$node->has_child_nodes()) {
+            return $values;
+        }
+
+        foreach ($node->child_nodes() as $vnode) {
+            if ($vnode->type == XML_ELEMENT_NODE &&
+                $vnode->tagname == 'values') {
+                if (!$vnode->has_child_nodes()) {
+                    return array();
+                }
+
+                foreach ($vnode->child_nodes() as $value) {
+                    if ($value->type == XML_ELEMENT_NODE) {
+                        if ($value->tagname == 'configspecial') {
+                            return $this->_handleSpecials($value);
+                        } elseif ($value->tagname == 'value') {
+                            $text = $value->get_content();
+                            $desc = $value->get_attribute('desc');
+                            $values[$text] = empty($desc) ? $text : $desc;
+                        }
+                    }
+                }
+            }
+        }
+
+        return $values;
+    }
+
+    /**
+     * Returns a multidimensional associative array representing the specified
+     * <configswitch> tag.
+     *
+     * @param DomNode &$node  The DomNode representation of the <configswitch>
+     *                        tag to process.
+     *
+     * @return array  An associative array representing the node.
+     */
+    protected function _getSwitchValues(&$node, $curctx)
+    {
+        $values = array();
+
+        if (!$node->has_child_nodes()) {
+            return $values;
+        }
+
+        foreach ($node->child_nodes() as $case) {
+            if ($case->type == XML_ELEMENT_NODE) {
+                $name = $case->get_attribute('name');
+                $values[$name] = array(
+                    'desc' => $case->get_attribute('desc'),
+                    'fields' => array()
+                );
+                if ($case->has_child_nodes()) {
+                    $this->_parseLevel($values[$name]['fields'], $case->child_nodes(), $curctx);
+                }
+            }
+        }
+
+        return $values;
+    }
+
+    /**
+     * Returns an associative array containing the possible values of a
+     * <configspecial> tag as used inside of enum configurations.
+     *
+     * @param DomNode $node  The DomNode representation of the <configspecial>
+     *                       tag.
+     *
+     * @return array  An associative array with the possible values.
+     */
+    protected function _handleSpecials($node)
+    {
+        switch ($node->get_attribute('name')) {
+        case 'list-horde-apps':
+            $apps = Horde_Array::valuesToKeys($GLOBALS['registry']->listApps(array('hidden', 'notoolbar', 'active')));
+            asort($apps);
+            return $apps;
+
+        case 'list-horde-languages':
+            return array_map(create_function('$val', 'return preg_replace(array("/&#x([0-9a-f]{4});/ie", "/(&[^;]+;)/e"), array("Horde_String::convertCharset(pack(\"H*\", \"$1\"), \"ucs-2\", \"' . NLS::getCharset() . '\")", "Horde_String::convertCharset(html_entity_decode(\"$1\", ENT_COMPAT, \"iso-8859-1\"), \"iso-8859-1\", \"' . NLS::getCharset() . '\")"), $val);'), $GLOBALS['nls']['languages']);
+
+        case 'list-blocks':
+            $collection = Horde_Block_Collection::singleton('portal');
+            return $collection->getBlocksList();
+
+        case 'list-client-fields':
+            global $registry;
+            $f = array();
+            if ($GLOBALS['registry']->hasMethod('clients/getClientSource')) {
+                $addressbook = $GLOBALS['registry']->call('clients/getClientSource');
+                $fields = $GLOBALS['registry']->call('clients/clientFields', array($addressbook));
+                if ($fields instanceof PEAR_Error) {
+                    $fields = $GLOBALS['registry']->call('clients/fields', array($addressbook));
+                }
+                if (!$fields instanceof PEAR_Error) {
+                    foreach ($fields as $field) {
+                        $f[$field['name']] = $field['label'];
+                    }
+                }
+            }
+            return $f;
+
+        case 'list-contact-sources':
+            $res = $GLOBALS['registry']->call('contacts/sources');
+            return $res;
+        }
+
+        return array();
+    }
+
+    /**
+     * Returns the specified string with escaped single quotes
+     *
+     * @param string $string  A string to escape.
+     *
+     * @return string  The specified string with single quotes being escaped.
+     */
+    protected function _quote($string)
+    {
+        return str_replace("'", "\'", $string);
+    }
+
+}
+
+/**
+ * A Horde_Form:: form that implements a user interface for the config
+ * system.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @package Core
+ */
+class ConfigForm extends Horde_Form
+{
+    /**
+     * Don't use form tokens for the configuration form - while
+     * generating configuration info, things like the Token system
+     * might not work correctly. This saves some headaches.
+     *
+     * @var boolean
+     */
+    protected $_useFormToken = false;
+
+    /**
+     * Contains the Horde_Config object that this form represents.
+     *
+     * @var Horde_Config
+     */
+    protected $_xmlConfig;
+
+    /**
+     * Contains the Horde_Variables object of this form.
+     *
+     * @var Horde_Variables
+     */
+    protected $_vars;
+
+    /**
+     * Constructor.
+     *
+     * @param Horde_Variables &$vars  The variables object of this form.
+     * @param string $app             The name of the application that this
+     *                                configuration form is for.
+     */
+    public function __construct(&$vars, $app)
+    {
+        parent::__construct($vars);
+
+        $this->_xmlConfig = new Horde_Config($app);
+        $this->_vars = &$vars;
+        $config = $this->_xmlConfig->readXMLConfig();
+        $this->addHidden('', 'app', 'text', true);
+        $this->_buildVariables($config);
+    }
+
+    /**
+     * Builds the form based on the specified level of the configuration tree.
+     *
+     * @param array $config   The portion of the configuration tree for that
+     *                        the form fields should be created.
+     * @param string $prefix  A string representing the current position
+     *                        inside the configuration tree.
+     */
+    protected function _buildVariables($config, $prefix = '')
+    {
+        if (!is_array($config)) {
+            return;
+        }
+
+        foreach ($config as $name => $configitem) {
+            $prefixedname = empty($prefix) ? $name : $prefix . '|' . $name;
+            $varname = str_replace('|', '__', $prefixedname);
+            if ($configitem == 'placeholder') {
+                continue;
+            } elseif (isset($configitem['tab'])) {
+                $this->setSection($configitem['tab'], $configitem['desc']);
+            } elseif (isset($configitem['switch'])) {
+                $selected = $this->_vars->getExists($varname, $wasset);
+                $var_params = array();
+                $select_option = true;
+                if (is_bool($configitem['default'])) {
+                    $configitem['default'] = $configitem['default'] ? 'true' : 'false';
+                }
+                foreach ($configitem['switch'] as $option => $case) {
+                    $var_params[$option] = $case['desc'];
+                    if ($option == $configitem['default']) {
+                        $select_option = false;
+                        if (!$wasset) {
+                            $selected = $option;
+                        }
+                    }
+                }
+
+                $name = '$conf[' . implode('][', explode('|', $prefixedname)) . ']';
+                $desc = $configitem['desc'];
+
+                $v = &$this->addVariable($name, $varname, 'enum', true, false, $desc, array($var_params, $select_option));
+                if (array_key_exists('default', $configitem)) {
+                    $v->setDefault($configitem['default']);
+                }
+                if (!empty($configitem['is_default'])) {
+                    $v->_new = true;
+                }
+                $v_action = Horde_Form_Action::factory('reload');
+                $v->setAction($v_action);
+                if (isset($selected) && isset($configitem['switch'][$selected])) {
+                    $this->_buildVariables($configitem['switch'][$selected]['fields'], $prefix);
+                }
+            } elseif (isset($configitem['_type'])) {
+                $required = (isset($configitem['required'])) ? $configitem['required'] : true;
+                $type = $configitem['_type'];
+
+                // FIXME: multienum fields can well be required, meaning that
+                // you need to select at least one entry. Changing this before
+                // Horde 4.0 would break a lot of configuration files though.
+                if ($type == 'multienum' || $type == 'header' ||
+                    $type == 'description') {
+                    $required = false;
+                }
+
+                $var_params = ($type == 'multienum' || $type == 'enum')
+                    ? array($configitem['values'])
+                    : array();
+
+                if ($type == 'header' || $type == 'description') {
+                    $name = $configitem['desc'];
+                    $desc = null;
+                } else {
+                    $name = '$conf[' . implode('][', explode('|', $prefixedname)) . ']';
+                    $desc = $configitem['desc'];
+                    if ($type == 'php') {
+                        $type = 'text';
+                        $desc .= "\nEnter a valid PHP expression.";
+                    }
+                }
+
+                $v = &$this->addVariable($name, $varname, $type, $required, false, $desc, $var_params);
+                if (isset($configitem['default'])) {
+                    $v->setDefault($configitem['default']);
+                }
+                if (!empty($configitem['is_default'])) {
+                    $v->_new = true;
+                }
+            } else {
+                $this->_buildVariables($configitem, $prefixedname);
+            }
+        }
+    }
+
+}
diff --git a/framework/Core/lib/Horde/Horde/ErrorHandler.php b/framework/Core/lib/Horde/Horde/ErrorHandler.php
new file mode 100644 (file)
index 0000000..c706694
--- /dev/null
@@ -0,0 +1,202 @@
+<?php
+/**
+ * Horde_ErrorHandler: simple error_handler implementation for
+ * handling PHP errors, generating backtraces for them, etc.
+ *
+ * @TODO Split dump() off into a Horde_Log backend, and make this more
+ * general-purpose. Also make it configurable whether or not to honor
+ * suppression of errors with @.
+ *
+ * @category Horde
+ * @package  Core
+ */
+class Horde_ErrorHandler
+{
+    /**
+     * Mapping of error codes to error code names.
+     *
+     * @var array
+     */
+    public static $errorTypes = array(
+        1 => 'ERROR',
+        2 => 'WARNING',
+        4 => 'PARSE',
+        8 => 'NOTICE',
+        16 => 'CORE_ERROR',
+        32 => 'CORE_WARNING',
+        64 => 'COMPILE_ERROR',
+        128 => 'COMPILE_WARNING',
+        256 => 'USER_ERROR',
+        512 => 'USER_WARNING',
+        1024 => 'USER_NOTICE',
+        2047 => 'ALL',
+        2048 => 'STRICT',
+        4096 => 'RECOVERABLE_ERROR',
+    );
+
+    /**
+     * error_reporting mask
+     *
+     * @var integer
+     */
+    protected static $_mask = E_ALL;
+
+    /**
+     * Array of errors that have been caught.
+     *
+     * @var array
+     */
+    protected static $_errors = array();
+
+    /**
+     * Configurable function to run on shutdown.
+     *
+     * @var callable
+     */
+    protected static $_shutdownFunc;
+
+    /**
+     * Set the error handler and shutdown functions.
+     *
+     * @param TODO
+     */
+    public static function register($shutdownFunc = null)
+    {
+        set_error_handler(array(__CLASS__, 'handleError'));
+
+        if (is_null($shutdownFunc)) {
+            $shutdownFunc = array(__CLASS__, 'dump');
+        }
+
+        self::$_shutdownFunc = $shutdownFunc;
+    }
+
+    /**
+     * Call the shutdown func, passing in accumulated errors.
+     */
+    public function __destruct()
+    {
+        if (self::$_errors) {
+            call_user_func(self::$_shutdownFunc, self::$_errors);
+        }
+    }
+
+    /**
+     * Process and handle/store an error.
+     *
+     * @param integer $errno    TODO
+     * @param string $errstr    TODO
+     * @param string $errfile   TODO
+     * @param integer $errline  TODO
+     */
+    public static function handleError($errno, $errstr, $errfile, $errline)
+    {
+        // Was the error suppressed?
+        if (!error_reporting()) {
+            // @TODO
+            // ...
+        }
+
+        // Check the mask.
+        if ($errno & self::$_mask) {
+            self::$_errors[] = array(
+                'no' => $errno,
+                'str' => self::_cleanErrorString($errstr),
+                'file' => $errfile,
+                'line' => $errline,
+                'trace' => self::_errorBacktrace(),
+            );
+        }
+    }
+
+    /**
+     * Include the context of the error in the debug
+     * information. Takes more (and could be much more) memory.
+     *
+     * @param integer $errno    TODO
+     * @param string $errstr    TODO
+     * @param string $errfile   TODO
+     * @param integer $errline  TODO
+     * @param TODO $errcontext  TODO
+     */
+    public static function handleErrorWithContext($errno, $errstr, $errfile,
+                                                  $errline, $errcontext)
+    {
+        self::$_errors[] = array(
+            'no' => $errno,
+            'str' => self::_cleanErrorString($errstr),
+            'file' => $errfile,
+            'line' => $errline,
+            'context' => $errcontext,
+            'trace' => self::_errorBacktrace(),
+        );
+    }
+
+    /**
+     * Remove function documentation links from an error string.
+     *
+     * @param string $errstr  TODO
+     *
+     * @return string  TODO
+     */
+    protected static function _cleanErrorString($errstr)
+    {
+        return preg_replace("%\s\[<a href='function\.[\d\w-_]+'>function\.[\d\w-_]+</a>\]%", '', $errstr);
+    }
+
+    /**
+     * Generate an exception-like backtrace from the debug_backtrace()
+     * function for errors.
+     *
+     * @return array  TODO
+     */
+    protected static function _errorBacktrace()
+    {
+        // Skip two levels of backtrace
+        $skip = 2;
+
+        $backtrace = debug_backtrace();
+        $trace = array();
+        for ($i = $skip, $i_max = count($backtrace); $i < $i_max; $i++) {
+            $frame = $backtrace[$i];
+            $trace[$i - $skip] = array(
+                'file' => isset($frame['file']) ? $frame['file'] : null,
+                'line' => isset($frame['line']) ? $frame['line'] : null,
+                'function' => isset($frame['function']) ? $frame['function'] : null,
+                'class' => isset($frame['class']) ? $frame['class'] : null,
+                'type' => isset($frame['type']) ? $frame['type'] : null,
+                'args' => isset($frame['args']) ? $frame['args'] : null,
+            );
+        }
+
+        return $trace;
+    }
+
+    /**
+     * On text/html pages, if the user is an administrator, show all
+     * errors that occurred during the request.
+     *
+     * @param array $errors  Accumulated errors.
+     */
+    public static function dump($errors)
+    {
+        if (!Horde_Auth::isAdmin()) {
+            return;
+        }
+
+        $dump = false;
+        foreach (headers_list() as $header) {
+            if (strpos($header, 'Content-type: text/html') !== false) {
+                $dump = true;
+                break;
+            }
+        }
+
+        if ($dump) {
+            foreach ($errors as $error) {
+                echo '<p>' . htmlspecialchars($error['file']) . ':' . htmlspecialchars($error['line']) . ': ' . htmlspecialchars($error['str']) . '</p>';
+            }
+        }
+    }
+
+}
diff --git a/framework/Core/lib/Horde/Horde/Exception.php b/framework/Core/lib/Horde/Horde/Exception.php
new file mode 100644 (file)
index 0000000..375485e
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+/**
+ * Horde base exception class, which includes the ability to take the
+ * output of error_get_last() as $code and mask itself as that error.
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Horde
+ * @package  Core
+ */
+class Horde_Exception extends Exception
+{
+    /**
+     * Exception constructor
+     *
+     * If $code_or_lasterror is passed the return value of
+     * error_get_last() (or a matching format), the exception will be
+     * rewritten to have its file and line parameters match that of
+     * the array, and any message in the array will be appended to
+     * $message.
+     *
+     * @param mixed $message            The exception message, a PEAR_Error
+     *                                  object, or an Exception object.
+     * @param mixed $code_or_lasterror  Either a numeric error code, or
+     *                                  an array from error_get_last().
+     */
+    public function __construct($message = null, $code_or_lasterror = null)
+    {
+        if (is_object($message) &&
+            method_exists($message, 'getMessage')) {
+            if (is_null($code_or_lasterror) &&
+                method_exists($message, 'getCode')) {
+                $code_or_lasterror = $message->getCode();
+            }
+            $message = $message->getMessage();
+        }
+
+        if (is_null($code_or_lasterror)) {
+            $code_or_lasterror = 0;
+        }
+
+        if (is_array($code_or_lasterror)) {
+            if ($message) {
+                $message .= $code_or_lasterror['message'];
+            } else {
+                $message = $code_or_lasterror['message'];
+            }
+
+            $this->file = $code_or_lasterror['file'];
+            $this->line = $code_or_lasterror['line'];
+            $code = $code_or_lasterror['type'];
+        } else {
+            $code = $code_or_lasterror;
+        }
+
+        if (is_string($code)) {
+            $code = null;
+        }
+
+        parent::__construct($message, $code);
+    }
+
+}
diff --git a/framework/Core/lib/Horde/Horde/Help.php b/framework/Core/lib/Horde/Horde/Help.php
new file mode 100644 (file)
index 0000000..cc1a1ea
--- /dev/null
@@ -0,0 +1,530 @@
+<?php
+/**
+ * The Horde_Help:: class provides an interface to the online help subsystem.
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jon Parise <jon@horde.org>
+ * @package Core
+ */
+class Horde_Help
+{
+    /* Raw help in the string. */
+    const SOURCE_RAW = 0;
+
+    /* Help text is in a file. */
+    const SOURCE_FILE = 1;
+
+    /**
+     * Handle for the XML parser object.
+     *
+     * @var resource
+     */
+    protected $_parser;
+
+    /**
+     * String buffer to hold the XML help source.
+     *
+     * @var string
+     */
+    protected $_buffer = '';
+
+    /**
+     * String containing the ID of the requested help entry.
+     *
+     * @var string
+     */
+    protected $_reqEntry = '';
+
+    /**
+     * String containing the ID of the current help entry.
+     *
+     * @var string
+     */
+    protected $_curEntry = '';
+
+    /**
+     * String containing the formatted output.
+     *
+     * @var string
+     */
+    protected $_output = '';
+
+    /**
+     * Boolean indicating whether we're inside a <help> block.
+     *
+     * @var boolean
+     */
+    protected $_inHelp = false;
+
+    /**
+     * Boolean indicating whether we're inside the requested block.
+     *
+     * @var boolean
+     */
+    protected $_inBlock = false;
+
+    /**
+     * Boolean indicating whether we're inside a <title> block.
+     *
+     * @var boolean
+     */
+    protected $_inTitle = false;
+
+    /**
+     * Boolean indicating whether we're inside a heading block.
+     *
+     * @var boolean
+     */
+    protected $_inHeading = false;
+
+    /**
+     * Hash containing an index of all of the help entries.
+     *
+     * @var array
+     */
+    protected $_entries = array();
+
+    /**
+     * String containing the charset of the XML data source.
+     *
+     * @var string
+     */
+    protected $_charset = 'iso-8859-1';
+
+    /**
+     * Hash of user-defined function handlers for the XML elements.
+     *
+     * @var array
+     */
+    protected $_handlers = array(
+        'help'     =>  '_helpHandler',
+        'entry'    =>  '_entryHandler',
+        'title'    =>  '_titleHandler',
+        'heading'  =>  '_headingHandler',
+        'para'     =>  '_paraHandler',
+        'ref'      =>  '_refHandler',
+        'eref'     =>  '_erefHandler',
+        'href'     =>  '_hrefHandler',
+        'b'        =>  '_bHandler',
+        'i'        =>  '_iHandler',
+        'pre'      =>  '_preHandler',
+        'tip'      =>  '_tipHandler',
+        'warn'     =>  '_warnHandler'
+    );
+
+    /**
+     * Hash containing an index of all of the search results.
+     *
+     * @var array
+     */
+    protected $_search = array();
+
+    /**
+     * String containing the keyword for the search.
+     *
+     * @var string
+     */
+    protected $_keyword = '';
+
+    /**
+     * Constructor.
+     *
+     * @param integer $source  The source of the XML help data, based on the
+     *                         SOURCE_* constants.
+     * @param string $arg      Source-dependent argument for this Help
+     *                         instance.
+     */
+    public function __construct($source, $arg = null)
+    {
+        if (isset($GLOBALS['nls']['charsets'][$GLOBALS['language']])) {
+            $this->_charset = $GLOBALS['nls']['charsets'][$GLOBALS['language']];
+        }
+
+        /* Populate $this->_buffer based on $source. */
+        switch ($source) {
+        case self::SOURCE_RAW:
+            $this->_buffer = $arg;
+            break;
+
+        case self::SOURCE_FILE:
+            if (file_exists($arg[0]) && filesize($arg[0])) {
+                $this->_buffer = file_get_contents($arg[0]);
+            } elseif (file_exists($arg[1]) && filesize($arg[1])) {
+                $this->_buffer = file_get_contents($arg[1]);
+            } else {
+                $this->_buffer = '';
+            }
+            break;
+
+        default:
+            $this->_buffer = '';
+            break;
+        }
+    }
+
+    /**
+     * Generates the HTML link that will pop up a help window for the
+     * requested topic.
+     *
+     * @param string $module  The name of the current Horde module.
+     * @param string $topic   The help topic to be displayed.
+     *
+     * @return string  The HTML to create the help link.
+     */
+    static public function link($module, $topic)
+    {
+        if (!Horde::showService('help')) {
+            return '&nbsp;';
+        }
+
+        if ($GLOBALS['browser']->hasFeature('javascript')) {
+            Horde::addScriptFile('popup.js', 'horde');
+        }
+
+        $url = Horde::url($GLOBALS['registry']->get('webroot', 'horde') . '/services/help/', true);
+        $url = Horde_Util::addParameter($url, array('module' => $module,
+                                                    'topic' => $topic));
+
+        return Horde::link($url, _("Help"), 'helplink', 'hordehelpwin', 'popup(this.href); return false;') .
+            Horde::img('help.png', _("Help"), 'width="16" height="16"', $GLOBALS['registry']->getImageDir('horde')) . '</a>';
+    }
+
+    /**
+     * Looks up the requested entry in the XML help buffer.
+     *
+     * @param string $entry  String containing the entry ID.
+     */
+    public function lookup($entry)
+    {
+        $this->_output = '';
+        $this->_reqEntry = Horde_String::upper($entry);
+        $this->_init();
+        xml_parse($this->_parser, $this->_buffer, true);
+    }
+
+    /**
+     * Returns a hash of all of the topics in this help buffer
+     * containing the keyword specified.
+     *
+     * @return array  Hash of all of the search results.
+     */
+    public function search($keyword)
+    {
+        $this->_init();
+        $this->_keyword = $keyword;
+        xml_parse($this->_parser, $this->_buffer, true);
+
+        return $this->_search;
+    }
+
+    /**
+     * Returns a hash of all of the topics in this help buffer.
+     *
+     * @return array  Hash of all of the topics in this buffer.
+     */
+    public function topics()
+    {
+        $this->_init();
+        xml_parse($this->_parser, $this->_buffer, true);
+
+        return $this->_entries;
+    }
+
+    /**
+     * Display the contents of the formatted output buffer.
+     */
+    public function display()
+    {
+        echo $this->_output;
+    }
+
+    /**
+     * Initializes the XML parser.
+     *
+     * @return boolean  Returns true on success, false on failure.
+     */
+    protected function _init()
+    {
+        if (!isset($this->_parser)) {
+            if (!Horde_Util::extensionExists('xml')) {
+                Horde::fatal(PEAR::raiseError('The XML functions are not available. Rebuild PHP with --with-xml.'), __FILE__, __LINE__, false);
+            }
+
+            /* Create a new parser and set its default properties. */
+            $this->_parser = xml_parser_create();
+            xml_set_object($this->_parser, $this);
+            xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false);
+            xml_set_element_handler($this->_parser, '_startElement', '_endElement');
+            xml_set_character_data_handler($this->_parser, '_defaultHandler');
+        }
+
+        return ($this->_parser != 0);
+    }
+
+    /**
+     * User-defined function callback for start elements.
+     *
+     * @param object $parser  Handle to the parser instance.
+     * @param string $name    The name of this XML element.
+     * @param array $attrs    List of this element's attributes.
+     */
+    protected function _startElement($parser, $name, $attrs)
+    {
+        /* Call the assigned handler for this element, if one is
+         * available. */
+        if (in_array($name, array_keys($this->_handlers))) {
+            call_user_func(array(&$this, $this->_handlers[$name]), true, $attrs);
+        }
+    }
+
+    /**
+     * User-defined function callback for end elements.
+     *
+     * @param object $parser  Handle to the parser instance.
+     * @param string $name    The name of this XML element.
+     */
+    protected function _endElement($parser, $name)
+    {
+        /* Call the assigned handler for this element, if one is available. */
+        if (in_array($name, array_keys($this->_handlers))) {
+            call_user_func(array(&$this, $this->_handlers[$name]), false);
+        }
+    }
+
+    /**
+     * User-defined function callback for character data.
+     *
+     * @param object $parser  Handle to the parser instance.
+     * @param string $data    String of character data.
+     */
+    protected function _defaultHandler($parser, $data)
+    {
+        $data = Horde_String::convertCharset($data, version_compare(zend_version(), '2', '<') ? $this->_charset : 'UTF-8');
+        if ($this->_inTitle) {
+            $this->_entries[$this->_curEntry] .= $data;
+        }
+
+        if ($this->_inHelp && $this->_inBlock) {
+            $this->_output .= htmlspecialchars($data);
+        }
+
+        if ($this->_keyword) {
+            if (stristr($data, $this->_keyword) !== false) {
+                $this->_search[$this->_curEntry] = $this->_entries[$this->_curEntry];
+            }
+        }
+    }
+
+    /**
+     * XML element handler for the <help> tag.
+     *
+     * @param boolean $startTag  Boolean indicating whether this instance is a
+     *                           start tag.
+     * @param array $attrs       Additional element attributes (Not used).
+     */
+    protected function _helpHandler($startTag, $attrs = array())
+    {
+        $this->_inHelp = $startTag ?  true : false;
+    }
+
+    /**
+     * XML element handler for the <entry> tag.
+     * Attributes: id
+     *
+     * @param boolean $startTag  Boolean indicating whether this instance is a
+     *                           start tag.
+     * @param array $attrs       Additional element attributes.
+     */
+    protected function _entryHandler($startTag, $attrs = array())
+    {
+        if (!$startTag) {
+            $this->_inBlock = false;
+        } else {
+            $id = Horde_String::upper($attrs['id']);
+            $this->_curEntry = $id;
+            $this->_entries[$id] = '';
+            $this->_inBlock = ($id == $this->_reqEntry);
+        }
+    }
+
+    /**
+     * XML element handler for the <title> tag.
+     *
+     * @param boolean $startTag  Boolean indicating whether this instance is a
+     *                           start tag.
+     * @param array $attrs       Additional element attributes (Not used).
+     */
+    protected function _titleHandler($startTag, $attrs = array())
+    {
+        $this->_inTitle = $startTag;
+        if ($this->_inHelp && $this->_inBlock) {
+            $this->_output .= $startTag ? '<h1>' : '</h1>';
+        }
+    }
+
+    /**
+     * XML element handler for the <heading> tag.
+     *
+     * @param boolean $startTag  Boolean indicating whether this instance is a
+     *                           start tag.
+     * @param  array $attrs      Additional element attributes (Not used).
+     */
+    protected function _headingHandler($startTag, $attrs = array())
+    {
+        $this->_inHeading = $startTag;
+        if ($this->_inHelp && $this->_inBlock) {
+            $this->_output .= $startTag ? '<h2>' : '</h2>';
+        }
+    }
+
+    /**
+     * XML element handler for the <para> tag.
+     *
+     * @param boolean $startTag  Boolean indicating whether this instance is a
+     *                           start tag.
+     * @param array $attrs       Additional element attributes (Not used).
+     */
+    protected function _paraHandler($startTag, $attrs = array())
+    {
+        if ($this->_inHelp && $this->_inBlock) {
+            $this->_output .= $startTag ? '<p>' : '</p>';
+        }
+    }
+
+    /**
+     * XML element handler for the <ref> tag.
+     * Required attributes: ENTRY, MODULE
+     *
+     * @param boolean $startTag  Boolean indicating whether this instance is a
+     *                           start tag.
+     * @param array $attrs       Additional element attributes.
+     */
+    protected function _refHandler($startTag, $attrs = array())
+    {
+        if ($this->_inHelp && $this->_inBlock) {
+            if ($startTag && isset($attrs['module']) && isset($attrs['entry'])) {
+                $url = Horde_Util::addParameter(Horde::selfUrl(),
+                                          array('show' => 'entry',
+                                                'module' => $attrs['module'],
+                                                'topic'  => $attrs['entry']));
+                $this->_output .= Horde::link($url);
+            } else {
+                $this->_output .= '</a>';
+            }
+        }
+    }
+
+    /**
+     * XML element handler for the <eref> tag.
+     * Required elements: URL
+     *
+     * @param boolean $startTag  Boolean indicating whether this instance is a
+     *                           start tag.
+     * @param array $attrs       Additional element attributes.
+     */
+    protected function _erefHandler($startTag, $attrs = array())
+    {
+        if ($this->_inHelp && $this->_inBlock) {
+            if ($startTag) {
+                $this->_output .= Horde::link($attrs['url'], null, '', '_blank');
+            } else {
+                $this->_output .= '</a>';
+            }
+        }
+    }
+
+    /**
+     * XML element handler for the <href> tag.
+     * Required elements: url, app.
+     *
+     * @param boolean $startTag  Boolean indicating whether this instance is a
+     *                           start tag.
+     * @param array $attrs       Additional element attributes.
+     */
+    protected function _hrefHandler($startTag, $attrs = array())
+    {
+        if ($this->_inHelp && $this->_inBlock) {
+            if ($startTag) {
+                $url = Horde::url($GLOBALS['registry']->get('webroot', $attrs['app']) . '/' . $attrs['url']);
+                $this->_output .= Horde::link($url, null, '', '_blank');
+            } else {
+                $this->_output .= '</a>';
+            }
+        }
+    }
+
+    /**
+     * XML element handler for the &lt;b&gt; tag.
+     *
+     * @param boolean $startTag  Boolean indicating whether this instance is a
+     *                           start tag.
+     * @param array $attrs       Additional element attributes (Not used).
+     */
+    protected function _bHandler($startTag, $attrs = array())
+    {
+        if ($this->_inHelp && $this->_inBlock) {
+            $this->_output .= $startTag ? '<strong>' : '</strong>';
+        }
+    }
+
+    /**
+     * XML element handler for the &lt;i&gt; tag.
+     *
+     * @param boolean $startTag  Boolean indicating whether this instance is a
+     *                           start tag.
+     * @param array $attrs       Additional element attributes.
+     */
+    protected function _iHandler($startTag, $attrs = array())
+    {
+        if ($this->_inHelp && $this->_inBlock) {
+            $this->_output .= $startTag ? '<em>' : '</em>';
+        }
+    }
+
+    /**
+     * XML element handler for the &lt;pre&gt; tag.
+     *
+     * @param boolean $startTag  Boolean indicating whether this instance is a
+     *                           start tag.
+     * @param array $attrs       Additional element attributes.
+     */
+    protected function _preHandler($startTag, $attrs = array())
+    {
+        if ($this->_inHelp && $this->_inBlock) {
+            $this->_output .= $startTag ? '<pre>' : '</pre>';
+        }
+    }
+
+    /**
+     * XML element handler for the <tip> tag.
+     *
+     * @param boolean $startTag  Boolean indicating whether this instance is a
+     *                           start tag.
+     * @param array $attrs       Additional element attributes.
+     */
+    protected function _tipHandler($startTag, $attrs = array())
+    {
+        if ($this->_inHelp && $this->_inBlock) {
+            $this->_output .= $startTag ? '<em class="helpTip">' : '</em>';
+        }
+    }
+
+    /**
+     * XML element handler for the <warn> tag.
+     *
+     * @param boolean $startTag  Boolean indicating whether this instance is a
+     *                           start tag.
+     * @param array $attrs       Additional element attributes.
+     */
+    protected function _warnHandler($startTag, $attrs = array())
+    {
+        if ($this->_inHelp && $this->_inBlock) {
+            $this->_output .= $startTag ? '<em class="helpWarn">' : '</em>';
+        }
+    }
+
+}
diff --git a/framework/Core/lib/Horde/Horde/Menu.php b/framework/Core/lib/Horde/Horde/Menu.php
new file mode 100644 (file)
index 0000000..5a66ce8
--- /dev/null
@@ -0,0 +1,313 @@
+<?php
+/**
+ * The Horde_Menu:: class provides standardized methods for creating menus in
+ * Horde applications.
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Jon Parise <jon@horde.org>
+ * @package Core
+ */
+class Horde_Menu
+{
+    /* TODO */
+    const MASK_NONE = 0;
+    const MASK_HELP = 1;
+    const MASK_LOGIN = 2;
+    const MASK_PREFS = 4;
+    const MASK_PROBLEM = 8;
+    const MASK_ALL = 15;
+
+    /* TODO */
+    const POS_LAST = 999;
+
+    /**
+     * Menu array.
+     *
+     * @var array
+     */
+    protected $_menu = array();
+
+    /**
+     * Mask defining what general Horde links are shown in this Menu.
+     *
+     * @var integer
+     */
+    protected $_mask;
+
+    /**
+     * Constructor
+     */
+    public function __construct($mask = self::MASK_ALL)
+    {
+        /* Menuitem mask. */
+        $this->_mask = $mask;
+
+        /* Location of the menufile. */
+        $this->_menufile = $GLOBALS['registry']->get('fileroot') . '/config/menu.php';
+    }
+
+    /**
+     * Add an item to the menu array.
+     *
+     * @param string $url        String containing the value for the hyperlink.
+     * @param string $text       String containing the label for this menu
+     *                           item.
+     * @param string $icon       String containing the filename of the image
+     *                           icon to display for this menu item.
+     * @param string $icon_path  If the icon lives in a non-default directory,
+     *                           where is it?
+     * @param string $target     If the link needs to open in another frame or
+     *                           window, what is its name?
+     * @param string $onclick    Onclick javascript, if desired.
+     * @param string $class      CSS class for the menu item.
+     *
+     * @return integer  The id (NOT guaranteed to be an array index) of the
+     *                  item just added to the menu.
+     */
+    public function add($url, $text, $icon = '', $icon_path = null,
+                        $target = '', $onclick = null, $class = null)
+    {
+        $pos = count($this->_menu);
+        if (!$pos || ($pos - 1 != max(array_keys($this->_menu)))) {
+            $pos = count($this->_menu);
+        }
+
+        $this->_menu[$pos] =
+            array(
+                'url' => $url,
+                'text' => $text,
+                'icon' => $icon,
+                'icon_path' => $icon_path,
+                'target' => $target,
+                'onclick' => $onclick,
+                'class' => $class
+            );
+
+        return $pos;
+    }
+
+    /**
+     * Add an item to the menu array.
+     *
+     * @param string $url        String containing the value for the hyperlink.
+     * @param string $text       String containing the label for this menu
+     *                           item.
+     * @param string $icon       String containing the filename of the image
+     *                           icon to display for this menu item.
+     * @param string $icon_path  If the icon lives in a non-default directory,
+     *                           where is it?
+     * @param string $target     If the link needs to open in another frame or
+     *                           window, what is its name?
+     * @param string $onclick    Onclick javascript, if desired.
+     * @param string $class      CSS class for the menu item.
+     *
+     * @return integer  The id (NOT guaranteed to be an array index) of the item
+     *                  just added to the menu.
+     */
+    public function addArray($item)
+    {
+        $pos = count($this->_menu);
+        if (!$pos || ($pos - 1 != max(array_keys($this->_menu)))) {
+            $pos = count($this->_menu);
+        }
+
+        $this->_menu[$pos] = $item;
+
+        return $pos;
+    }
+
+    /**
+     * TODO
+     */
+    public function setPosition($id, $pos)
+    {
+        if (!isset($this->_menu[$id]) || isset($this->_menu[$pos])) {
+            return false;
+        }
+
+        $item = $this->_menu[$id];
+        unset($this->_menu[$id]);
+        $this->_menu[$pos] = $item;
+
+        return true;
+    }
+
+    /**
+     * Return the unordered list representing the list of menu items. Styling
+     * is done through CSS.
+     *
+     * @return string  An unordered list of menu elements that can be entirely
+     *                 styled with CSS.
+     */
+    public function render()
+    {
+        global $conf, $registry, $prefs;
+
+        $graphics = $registry->getImageDir('horde');
+        $app = $registry->getApp();
+
+        if ($this->_mask !== self::MASK_NONE) {
+            /* Add any custom menu items. */
+            $this->addSiteLinks();
+
+            /* Add any app menu items. */
+            $this->addAppLinks();
+        }
+
+        /* Add settings link. */
+        if ($this->_mask & self::MASK_PREFS && $url = Horde::getServiceLink('options', $app)) {
+            $this->add($url, _("_Options"), 'prefs.png', $graphics);
+        }
+
+        /* Add problem link. */
+        if ($this->_mask & self::MASK_PROBLEM && $problem_link = Horde::getServiceLink('problem', $app)) {
+            $this->add($problem_link, _("Problem"), 'problem.png', $graphics);
+        }
+
+        /* Add help link. */
+        if ($this->_mask & self::MASK_HELP && $help_link = Horde::getServiceLink('help', $app)) {
+            $this->add($help_link, _("Help"), 'help_index.png', $graphics, 'help', 'popup(this.href); return false;', 'helplink');
+        }
+
+        /* Login/Logout. */
+        if ($this->_mask & self::MASK_LOGIN) {
+            /* If the sidebar isn't always shown, but is sometimes
+             * shown, then logout links should be to the parent
+             * frame. */
+            $auth_target = null;
+            if ($conf['menu']['always'] || $prefs->getValue('show_sidebar')) {
+                $auth_target = '_parent';
+            }
+
+            if (Horde_Auth::getAuth()) {
+                if ($logout_link = Horde::getServiceLink('logout', $app, !$prefs->getValue('show_sidebar'))) {
+                    $this->add($logout_link, _("_Log out"), 'logout.png', $graphics, $auth_target, null, '__noselection');
+                }
+            } else {
+                if ($login_link = Horde::getServiceLink('login', $app)) {
+                    $this->add($login_link, _("_Log in"), 'login.png', $graphics, $auth_target, null, '__noselection');
+                }
+            }
+        }
+
+        /* No need to return an empty list if there are no menu
+         * items. */
+        if (!count($this->_menu)) {
+            return '';
+        }
+
+        /* Sort to match explicitly set positions. */
+        ksort($this->_menu);
+        if (!empty($GLOBALS['nls']['rtl'][$GLOBALS['language']]))  {
+            $this->_menu = array_reverse($this->_menu) ;
+        }
+
+        $menu_view = $prefs->getValue('menu_view');
+        $output = '<ul>';
+        foreach ($this->_menu as $m) {
+            /* Check for separators. */
+            if ($m == 'separator') {
+                $output .= "\n<li class=\"separator\">&nbsp;</li>";
+                continue;
+            }
+
+            /* Item class and selected indication. */
+            if (!isset($m['class'])) {
+                /* Try to match the item's path against the current
+                 * script filename as well as other possible URLs to
+                 * this script. */
+                if (self::isSelected($m['url'])) {
+                    $m['class'] = 'current';
+                }
+            } elseif ($m['class'] === '__noselection') {
+                unset($m['class']);
+            }
+
+            /* Icon. */
+            $icon = '';
+            if ($menu_view == 'icon' || $menu_view == 'both') {
+                if (!isset($m['icon_path'])) {
+                    $m['icon_path'] = null;
+                }
+                $icon = Horde::img($m['icon'], Horde::stripAccessKey($m['text']), '', $m['icon_path']) . '<br />';
+            }
+
+            /* Link. */
+            $accesskey = Horde::getAccessKey($m['text']);
+            $link = Horde::link($m['url'], ($menu_view == 'icon') ? Horde::stripAccessKey($m['text']) : '',
+                                isset($m['class']) ? $m['class'] : '',
+                                isset($m['target']) ? $m['target'] : '',
+                                isset($m['onclick']) ? $m['onclick'] : '',
+                                '', $accesskey);
+
+            $output .= sprintf("\n<li>%s%s%s</a></li>",
+                               $link, $icon, ($menu_view != 'icon') ? Horde::highlightAccessKey($m['text'], $accesskey) : '');
+        }
+
+        return $output . '</ul>';
+    }
+
+    /**
+     * Any links to other Horde applications defined in an application's config
+     * file by the $conf['menu']['apps'] array are added to the menu array.
+     */
+    public function addAppLinks()
+    {
+        global $conf, $registry;
+
+        if (isset($conf['menu']['apps']) && is_array($conf['menu']['apps'])) {
+            foreach ($conf['menu']['apps'] as $app) {
+                if ($registry->get('status', $app) != 'inactive' && $registry->hasPermission($app, PERMS_SHOW)) {
+                    $url = $registry->getInitialPage($app);
+                    if (!is_a($url, 'PEAR_Error')) {
+                        $this->add(Horde::url($url), $registry->get('name', $app), $registry->get('icon', $app), '');
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Add any other links found in $this->_menufile to be included in the
+     * menu.
+     */
+    public function addSiteLinks()
+    {
+        if (is_readable($this->_menufile)) {
+            include $this->_menufile;
+            if (isset($_menu) && is_array($_menu)) {
+                foreach ($_menu as $menuitem) {
+                    $this->addArray($menuitem);
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks to see if the current url matches the given url.
+     *
+     * @return boolean  Whether the given URL is the current location.
+     */
+    static public function isSelected($url)
+    {
+        $server_url = parse_url($_SERVER['PHP_SELF']);
+        $check_url = parse_url($url);
+
+        /* Try to match the item's path against the current script
+           filename as well as other possible URLs to this script. */
+        if (isset($check_url['path']) &&
+            (($check_url['path'] == $server_url['path']) ||
+             ($check_url['path'] . 'index.php' == $server_url['path']) ||
+             ($check_url['path'] . '/index.php' == $server_url['path']))) {
+            return true;
+        }
+
+        return false;
+    }
+
+}
diff --git a/framework/Core/lib/Horde/Horde/Registry.php b/framework/Core/lib/Horde/Horde/Registry.php
new file mode 100644 (file)
index 0000000..b2d81d6
--- /dev/null
@@ -0,0 +1,1137 @@
+<?php
+/**
+ * The Horde_Registry:: class provides a set of methods for communication
+ * between Horde applications and keeping track of application
+ * configuration information.
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Jon Parise <jon@horde.org>
+ * @author  Anil Madhavapeddy <anil@recoil.org>
+ * @author  Michael Slusarz <slusarz@horde.org>
+ * @package Core
+ */
+class Horde_Registry
+{
+    /* Session flags. */
+    const SESSION_NONE = 1;
+    const SESSION_READONLY = 2;
+
+    /**
+     * Singleton value.
+     *
+     * @var Horde_Registry
+     */
+    static protected $_instance;
+
+    /**
+     * Cached information.
+     *
+     * @var array
+     */
+    protected $_cache = array();
+
+    /**
+     * The Horde_Cache object.
+     *
+     * @var Horde_Cache
+     */
+    protected $_cacheob;
+
+    /**
+     * The last modified time of the newest modified registry file.
+     *
+     * @var integer
+     */
+    protected $_regmtime;
+
+    /**
+     * Stack of in-use applications.
+     *
+     * @var array
+     */
+    protected $_appStack = array();
+
+    /**
+     * The list of APIs.
+     *
+     * @param array
+     */
+    protected $_apis = array();
+
+    /**
+     * Cached values of the image directories.
+     *
+     * @param array
+     */
+    protected $_imgDir = array();
+
+    /**
+     * Hash storing information on each registry-aware application.
+     *
+     * @var array
+     */
+    public $applications = array();
+
+    /**
+     * Returns a reference to the global Horde_Registry object, only creating
+     * it if it doesn't already exist.
+     *
+     * This method must be invoked as:
+     *   $registry = Horde_Registry::singleton()
+     *
+     * @param integer $session_flags  Any session flags.
+     *
+     * @return Horde_Registry  The Horde_Registry instance.
+     */
+    static public function singleton($session_flags = 0)
+    {
+        if (!isset(self::$_instance)) {
+            self::$_instance = new Horde_Registry($session_flags);
+        }
+
+        return self::$_instance;
+    }
+
+    /**
+     * Create a new Horde_Registry instance.
+     *
+     * @param integer $session_flags  Any session flags.
+     */
+    protected function __construct($session_flags = 0)
+    {
+        /* Import and global Horde's configuration values. */
+        $this->_cache['conf-horde'] = Horde::loadConfiguration('conf.php', 'conf', 'horde');
+        if (is_a($this->_cache['conf-horde'], 'PEAR_Error')) {
+            return $this->_cache['conf-horde'];
+        }
+
+        $conf = $GLOBALS['conf'] = &$this->_cache['conf-horde'];
+
+        /* Initial Horde-wide settings. */
+
+        /* Set the maximum execution time in accordance with the config
+         * settings. */
+        error_reporting(0);
+        set_time_limit($conf['max_exec_time']);
+
+        /* Set the error reporting level in accordance with the config
+         * settings. */
+        error_reporting($conf['debug_level']);
+
+        /* Set the umask according to config settings. */
+        if (isset($conf['umask'])) {
+            umask($conf['umask']);
+        }
+
+        /* Start a session. */
+        if ($session_flags & self::SESSION_NONE) {
+            /* Never start a session if the session flags include
+               SESSION_NONE. */
+            $_SESSION = array();
+        } else {
+            Horde::setupSessionHandler();
+            $old_error = error_reporting(0);
+            session_start();
+            if ($session_flags & self::SESSION_READONLY) {
+                /* Close the session immediately so no changes can be
+                   made but values are still available. */
+                session_write_close();
+            }
+            error_reporting($old_error);
+
+            if (!isset($_SESSION['_registry'])) {
+                $_SESSION['_registry'] = array();
+            }
+        }
+
+        /* Initialize the localization routines and variables. We can't use
+         * NLS::setLanguageEnvironment() here because that depends on the
+         * registry to be already initialized. */
+        NLS::setLang();
+        NLS::setTextdomain('horde', HORDE_BASE . '/locale', NLS::getCharset());
+        Horde_String::setDefaultCharset(NLS::getCharset());
+
+        /* Check for caching availability. Using cache while not authenticated
+         * isn't possible because, although storage is possible, retrieval
+         * isn't since there is no MD5 sum in the session to use to build
+         * the cache IDs. */
+        if (Horde_Auth::getAuth()) {
+            try {
+                $this->_cacheob = Horde_Cache::singleton($conf['cache']['driver'], Horde::getDriverConfig('cache', $conf['cache']['driver']));
+            } catch (Horde_Exception $e) {}
+        }
+
+        $this->_regmtime = max(filemtime(HORDE_BASE . '/config/registry.php'),
+                               filemtime(HORDE_BASE . '/config/registry.d'));
+
+        $vhost = null;
+        if (!empty($conf['vhosts'])) {
+            $vhost = HORDE_BASE . '/config/registry-' . $conf['server']['name'] . '.php';
+            if (file_exists($vhost)) {
+                $this->_regmtime = max($this->_regmtime, filemtime($vhost));
+            } else {
+                $vhost = null;
+            }
+        }
+
+        /* Always need to load applications information. */
+        $this->_loadApplicationsCache($vhost);
+
+        /* Stop system if Horde is inactive. */
+        if ($this->applications['horde']['status'] == 'inactive') {
+            Horde::fatal(_("This system is currently deactivated."), __FILE__, __LINE__);
+        }
+
+        /* Create the global Perms object. */
+        $GLOBALS['perms'] = &Perms::singleton();
+
+        /* Attach javascript notification listener. */
+        $notification = &Horde_Notification::singleton();
+        $notification->attach('javascript');
+    }
+
+    /**
+     * Stores cacheable member variables in the session at shutdown.
+     */
+    public function __destruct()
+    {
+        /* Register access key logger for translators. */
+        if (!empty($GLOBALS['conf']['log_accesskeys'])) {
+            Horde::getAccessKey(null, null, true);
+        }
+
+        /* Register memory tracker if logging in debug mode. */
+        if (!empty($GLOBALS['conf']['log']['enabled']) &&
+            ($GLOBALS['conf']['log']['priority'] == PEAR_LOG_DEBUG) &&
+            function_exists('memory_get_peak_usage')) {
+            Horde::logMessage('Max memory usage: ' . memory_get_peak_usage(true) . ' bytes', __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        }
+    }
+
+    /**
+     * TODO
+     */
+    public function __get($api)
+    {
+        if (in_array($api, $this->listAPIs())) {
+            return new Horde_Registry_Caller($this, $api);
+        }
+    }
+
+    /**
+     * Clone should never be called on this object. If it is, die.
+     */
+    public function __clone()
+    {
+        Horde::fatal('Horde_Registry objects should never be cloned.', __FILE__, __LINE__);
+    }
+
+    /**
+     * Clear the registry cache.
+     */
+    public function clearCache()
+    {
+        unset($_SESSION['_registry']);
+        $this->_saveCacheVar('apicache', true);
+        $this->_saveCacheVar('appcache', true);
+    }
+
+    /**
+     * Fills the registry's application cache with application information.
+     *
+     * @param string $vhost  TODO
+     */
+    protected function _loadApplicationsCache($vhost)
+    {
+        /* First, try to load from cache. */
+        if ($this->_loadCacheVar('appcache')) {
+            $this->applications = $this->_cache['appcache'][0];
+            $this->_cache['interfaces'] = $this->_cache['appcache'][1];
+            return;
+        }
+
+        $this->_cache['interfaces'] = array();
+
+        /* Read the registry configuration files. */
+        require HORDE_BASE . '/config/registry.php';
+        $files = glob(HORDE_BASE . '/config/registry.d/*.php');
+        if ($files) {
+            foreach ($files as $r) {
+                include $r;
+            }
+        }
+
+        if ($vhost) {
+            include $vhost;
+        }
+
+        /* Scan for all APIs provided by each app, and set other common
+         * defaults like templates and graphics. */
+        foreach (array_keys($this->applications) as $appName) {
+            $app = &$this->applications[$appName];
+            if ($app['status'] == 'heading') {
+                continue;
+            }
+
+            if (isset($app['fileroot']) && !file_exists($app['fileroot'])) {
+                $app['status'] = 'inactive';
+            }
+
+            if (($app['status'] != 'inactive') &&
+                isset($app['provides']) &&
+                (($app['status'] != 'admin') || Horde_Auth::isAdmin())) {
+                if (is_array($app['provides'])) {
+                    foreach ($app['provides'] as $interface) {
+                        $this->_cache['interfaces'][$interface] = $appName;
+                    }
+                } else {
+                    $this->_cache['interfaces'][$app['provides']] = $appName;
+                }
+            }
+
+            if (!isset($app['templates']) && isset($app['fileroot'])) {
+                $app['templates'] = $app['fileroot'] . '/templates';
+            }
+            if (!isset($app['jsuri']) && isset($app['webroot'])) {
+                $app['jsuri'] = $app['webroot'] . '/js';
+            }
+            if (!isset($app['jsfs']) && isset($app['fileroot'])) {
+                $app['jsfs'] = $app['fileroot'] . '/js';
+            }
+            if (!isset($app['themesuri']) && isset($app['webroot'])) {
+                $app['themesuri'] = $app['webroot'] . '/themes';
+            }
+            if (!isset($app['themesfs']) && isset($app['fileroot'])) {
+                $app['themesfs'] = $app['fileroot'] . '/themes';
+            }
+        }
+
+        $this->_cache['appcache'] = array(
+            // Index 0
+            $this->applications,
+            // Index 1
+            $this->_cache['interfaces']
+        );
+        $this->_saveCacheVar('appcache');
+    }
+
+    /**
+     * Fills the registry's API cache with the available services and types.
+     */
+    protected function _loadApiCache()
+    {
+        /* First, try to load from cache. */
+        if ($this->_loadCacheVar('apicache')) {
+            $this->_cache['api'] = $this->_cache['apicache'][0];
+            $this->_cache['type'] = $this->_cache['apicache'][1];
+            return;
+        }
+
+        /* Generate api/type cache. */
+        $status = array('active', 'notoolbar', 'hidden');
+        if (Horde_Auth::isAdmin()) {
+            $status[] = 'admin';
+        }
+
+        $this->_cache['api'] = $this->_cache['type'] = array();
+
+        $apps = $this->listApps($status);
+        foreach ($apps as $app) {
+            $_services = $_types = null;
+            $api = $this->get('fileroot', $app) . '/lib/api.php';
+            if (is_readable($api)) {
+                include_once $api;
+            }
+            $this->_cache['api'][$app] = $_services;
+            if (!is_null($_types)) {
+                foreach ($_types as $type => $params) {
+                    /* Prefix non-Horde types with the application name. */
+                    $prefix = ($app == 'horde') ? '' : "${app}_";
+                    $this->_cache['type'][$prefix . $type] = $params;
+                }
+            }
+        }
+
+        $this->_cache['apicache'] = array(
+            // Index 0
+            $this->_cache['api'],
+            // Index 1
+            $this->_cache['type']
+        );
+        $this->_saveCacheVar('apicache');
+    }
+
+    /**
+     * Return a list of the installed and registered applications.
+     *
+     * @param array $filter   An array of the statuses that should be
+     *                        returned. Defaults to non-hidden.
+     * @param boolean $assoc  Associative array with app names as keys.
+     * @param integer $perms  The permission level to check for in the list.
+     *
+     * @return array  List of apps registered with Horde. If no
+     *                applications are defined returns an empty array.
+     */
+    public function listApps($filter = null, $assoc = false,
+                             $perms = PERMS_SHOW)
+    {
+        $apps = array();
+        $ahandler = defined('AUTH_HANDLER');
+        if (is_null($filter)) {
+            $filter = array('notoolbar', 'active');
+        }
+
+        foreach ($this->applications as $app => $params) {
+            if (in_array($params['status'], $filter) &&
+                ($ahandler || $this->hasPermission($app, $perms))) {
+                $apps[$app] = $app;
+            }
+        }
+
+        return $assoc ? $apps : array_values($apps);
+    }
+
+    /**
+     * Returns all available registry APIs.
+     *
+     * @return array  The API list.
+     */
+    public function listAPIs()
+    {
+        if (empty($this->_apis)) {
+            foreach (array_keys($this->_cache['interfaces']) as $interface) {
+                list($api,) = explode('/', $interface, 2);
+                $this->_apis[$api] = true;
+            }
+        }
+
+        return array_keys($this->_apis);
+    }
+
+    /**
+     * Returns all of the available registry methods, or alternately
+     * only those for a specified API.
+     *
+     * @param string $api  Defines the API for which the methods shall be
+     *                     returned.
+     *
+     * @return array  The method list.
+     */
+    public function listMethods($api = null)
+    {
+        $methods = array();
+
+        $this->_loadApiCache();
+
+        foreach (array_keys($this->applications) as $app) {
+            if (isset($this->applications[$app]['provides'])) {
+                $provides = $this->applications[$app]['provides'];
+                if (!is_array($provides)) {
+                    $provides = array($provides);
+                }
+                foreach ($provides as $method) {
+                    if (strpos($method, '/') !== false) {
+                        if (is_null($api) ||
+                            (substr($method, 0, strlen($api)) == $api)) {
+                            $methods[$method] = true;
+                        }
+                    } elseif (is_null($api) || ($method == $api)) {
+                        if (isset($this->_cache['api'][$app])) {
+                            foreach (array_keys($this->_cache['api'][$app]) as $service) {
+                                $methods[$method . '/' . $service] = true;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        return array_keys($methods);
+    }
+
+    /**
+     * Returns all of the available registry data types.
+     *
+     * @return array  The data type list.
+     */
+    public function listTypes()
+    {
+        $this->_loadApiCache();
+        return $this->_cache['type'];
+    }
+
+    /**
+     * Returns a method's signature.
+     *
+     * @param string $method  The full name of the method to check for.
+     *
+     * @return array  A two dimensional array. The first element contains an
+     *                array with the parameter names, the second one the return
+     *                type.
+     */
+    public function getSignature($method)
+    {
+        if (!($app = $this->hasMethod($method))) {
+            return false;
+        }
+
+        $this->_loadApiCache();
+
+        list(,$function) = explode('/', $method, 2);
+        if (!empty($function) &&
+            isset($this->_cache['api'][$app][$function]['type']) &&
+            isset($this->_cache['api'][$app][$function]['args'])) {
+            return array($this->_cache['api'][$app][$function]['args'], $this->_cache['api'][$app][$function]['type']);
+        }
+
+        return false;
+    }
+
+    /**
+     * Determine if an interface is implemented by an active application.
+     *
+     * @param string $interface  The interface to check for.
+     *
+     * @return mixed  The application implementing $interface if we have it,
+     *                false if the interface is not implemented.
+     */
+    public function hasInterface($interface)
+    {
+        return !empty($this->_cache['interfaces'][$interface]) ?
+            $this->_cache['interfaces'][$interface] :
+            false;
+    }
+
+    /**
+     * Determine if a method has been registered with the registry.
+     *
+     * @param string $method  The full name of the method to check for.
+     * @param string $app     Only check this application.
+     *
+     * @return mixed  The application implementing $method if we have it,
+     *                false if the method doesn't exist.
+     */
+    public function hasMethod($method, $app = null)
+    {
+        if (is_null($app)) {
+            list($interface, $call) = explode('/', $method, 2);
+            if (!empty($this->_cache['interfaces'][$method])) {
+                $app = $this->_cache['interfaces'][$method];
+            } elseif (!empty($this->_cache['interfaces'][$interface])) {
+                $app = $this->_cache['interfaces'][$interface];
+            } else {
+                return false;
+            }
+        } else {
+            $call = $method;
+        }
+
+        $this->_loadApiCache();
+
+        return empty($this->_cache['api'][$app][$call]) ? false : $app;
+    }
+
+    /**
+     * Return the hook corresponding to the default package that
+     * provides the functionality requested by the $method
+     * parameter. $method is a string consisting of
+     * "packagetype/methodname".
+     *
+     * @param string $method  The method to call.
+     * @param array $args     Arguments to the method.
+     *
+     * @return  TODO
+     *          Returns PEAR_Error on error.
+     */
+    public function call($method, $args = array())
+    {
+        list($interface, $call) = explode('/', $method, 2);
+
+        if (!empty($this->_cache['interfaces'][$method])) {
+            $app = $this->_cache['interfaces'][$method];
+        } elseif (!empty($this->_cache['interfaces'][$interface])) {
+            $app = $this->_cache['interfaces'][$interface];
+        } else {
+            return PEAR::raiseError('The method "' . $method . '" is not defined in the Horde Registry.');
+        }
+
+        return $this->callByPackage($app, $call, $args);
+    }
+
+    /**
+     * Output the hook corresponding to the specific package named.
+     *
+     * @param string $app   The application being called.
+     * @param string $call  The method to call.
+     * @param array $args   Arguments to the method.
+     *
+     * @return  TODO
+     *          Returns PEAR_Error on error.
+     */
+    public function callByPackage($app, $call, $args = array())
+    {
+        /* Note: calling hasMethod() makes sure that we've cached
+         * $app's services and included the API file, so we don't try
+         * to do it again explicitly in this method. */
+        if (!$this->hasMethod($call, $app)) {
+            return PEAR::raiseError(sprintf('The method "%s" is not defined in the API for %s.', $call, $app));
+        }
+
+        /* Load the API now. */
+        $api = $this->get('fileroot', $app) . '/lib/api.php';
+        if (is_readable($api)) {
+            include_once $api;
+        }
+
+        /* Make sure that the function actually exists. */
+        $function = '_' . $app . '_' . str_replace('/', '_', $call);
+        if (!function_exists($function)) {
+            return PEAR::raiseError('The function implementing ' . $call . ' (' . $function . ') is not defined in ' . $app . '\'s API.');
+        }
+
+        $checkPerms = isset($this->_cache['api'][$app][$call]['checkperms'])
+            ? $this->_cache['api'][$app][$call]['checkperms']
+            : true;
+
+        /* Switch application contexts now, if necessary, before
+         * including any files which might do it for us. Return an
+         * error immediately if pushApp() fails. */
+        $pushed = $this->pushApp($app, $checkPerms);
+        if (is_a($pushed, 'PEAR_Error')) {
+            return $pushed;
+        }
+
+        $res = call_user_func_array($function, $args);
+
+        /* If we changed application context in the course of this
+         * call, undo that change now. */
+        if ($pushed === true) {
+            $this->popApp();
+        }
+
+        return $res;
+    }
+
+    /**
+     * Return the hook corresponding to the default package that
+     * provides the functionality requested by the $method
+     * parameter. $method is a string consisting of
+     * "packagetype/methodname".
+     *
+     * @param string $method  The method to link to.
+     * @param array $args     Arguments to the method.
+     * @param mixed $extra    Extra, non-standard arguments to the method.
+     *
+     * @return  TODO
+     *          Returns PEAR_Error on error.
+     */
+    public function link($method, $args = array(), $extra = '')
+    {
+        list($interface, $call) = explode('/', $method, 2);
+
+        if (!empty($this->_cache['interfaces'][$method])) {
+            $app = $this->_cache['interfaces'][$method];
+        } elseif (!empty($this->_cache['interfaces'][$interface])) {
+            $app = $this->_cache['interfaces'][$interface];
+        } else {
+            return PEAR::raiseError('The method "' . $method . '" is not defined in the Horde Registry.');
+        }
+
+        return $this->linkByPackage($app, $call, $args, $extra);
+    }
+
+    /**
+     * Output the hook corresponding to the specific package named.
+     *
+     * @param string $app   The application being called.
+     * @param string $call  The method to link to.
+     * @param array $args   Arguments to the method.
+     * @param mixed $extra  Extra, non-standard arguments to the method.
+     *
+     * @return  TODO
+     *          Returns PEAR_Error on error.
+     */
+    public function linkByPackage($app, $call, $args = array(), $extra = '')
+    {
+        /* Note: calling hasMethod makes sure that we've cached $app's
+         * services and included the API file, so we don't try to do
+         * it it again explicitly in this method. */
+        if (!$this->hasMethod($call, $app)) {
+            return PEAR::raiseError('The method "' . $call . '" is not defined in ' . $app . '\'s API.');
+        }
+
+        /* Make sure the link is defined. */
+        $this->_loadApiCache();
+        if (empty($this->_cache['api'][$app][$call]['link'])) {
+            return PEAR::raiseError('The link ' . $call . ' is not defined in ' . $app . '\'s API.');
+        }
+
+        /* Initial link value. */
+        $link = $this->_cache['api'][$app][$call]['link'];
+
+        /* Fill in html-encoded arguments. */
+        foreach ($args as $key => $val) {
+            $link = str_replace('%' . $key . '%', htmlentities($val), $link);
+        }
+        if (isset($this->applications[$app]['webroot'])) {
+            $link = str_replace('%application%', $this->get('webroot', $app), $link);
+        }
+
+        /* Replace htmlencoded arguments that haven't been specified with
+           an empty string (this is where the default would be substituted
+           in a stricter registry implementation). */
+        $link = preg_replace('|%.+%|U', '', $link);
+
+        /* Fill in urlencoded arguments. */
+        foreach ($args as $key => $val) {
+            $link = str_replace('|' . Horde_String::lower($key) . '|', urlencode($val), $link);
+        }
+
+        /* Append any extra, non-standard arguments. */
+        if (is_array($extra)) {
+            $extra_args = '';
+            foreach ($extra as $key => $val) {
+                $extra_args .= '&' . urlencode($key) . '=' . urlencode($val);
+            }
+        } else {
+            $extra_args = $extra;
+        }
+        $link = str_replace('|extra|', $extra_args, $link);
+
+        /* Replace html-encoded arguments that haven't been specified with
+           an empty string (this is where the default would be substituted
+           in a stricter registry implementation). */
+        $link = preg_replace('|\|.+\||U', '', $link);
+
+        return $link;
+    }
+
+    /**
+     * Replace any %application% strings with the filesystem path to the
+     * application.
+     *
+     * @param string $path  The application string.
+     * @param string $app   The application being called.
+     *
+     * @return  TODO
+     *          Returns PEAR_Error on error.
+     */
+    public function applicationFilePath($path, $app = null)
+    {
+        if (is_null($app)) {
+            $app = $this->getApp();
+        }
+
+        if (!isset($this->applications[$app])) {
+            return PEAR::raiseError(sprintf(_("\"%s\" is not configured in the Horde Registry."), $app));
+        }
+
+        return str_replace('%application%', $this->applications[$app]['fileroot'], $path);
+    }
+
+    /**
+     * Replace any %application% strings with the web path to the application.
+     *
+     * @param string $path  The application string.
+     * @param string $app   The application being called.
+     *
+     * @return  TODO
+     *          Returns PEAR_Error on error.
+     */
+    public function applicationWebPath($path, $app = null)
+    {
+        if (!isset($app)) {
+            $app = $this->getApp();
+        }
+
+        return str_replace('%application%', $this->applications[$app]['webroot'], $path);
+    }
+
+    /**
+     * Set the current application, adding it to the top of the Horde
+     * application stack. If this is the first application to be
+     * pushed, retrieve session information as well.
+     *
+     * pushApp() also reads the application's configuration file and
+     * sets up its global $conf hash.
+     *
+     * @param string $app          The name of the application to push.
+     * @param boolean $checkPerms  Make sure that the current user has
+     *                             permissions to the application being loaded
+     *                             Defaults to true. Should ONLY be disabled
+     *                             by system scripts (cron jobs, etc.) and
+     *                             scripts that handle login.
+     *
+     * @return boolean  Whether or not the _appStack was modified.
+     *                  Return PEAR_Error on error.
+     */
+    public function pushApp($app, $checkPerms = true)
+    {
+        if ($app == $this->getApp()) {
+            return false;
+        }
+
+        /* Bail out if application is not present or inactive. */
+        if (!isset($this->applications[$app]) ||
+            $this->applications[$app]['status'] == 'inactive' ||
+            ($this->applications[$app]['status'] == 'admin' && !Horde_Auth::isAdmin())) {
+            Horde::fatal($app . ' is not activated', __FILE__, __LINE__);
+        }
+
+        /* If permissions checking is requested, return an error if the
+         * current user does not have read perms to the application being
+         * loaded. We allow access:
+         *
+         *  - To all admins.
+         *  - To all authenticated users if no permission is set on $app.
+         *  - To anyone who is allowed by an explicit ACL on $app. */
+        if ($checkPerms && !$this->hasPermission($app)) {
+            Horde::logMessage(sprintf('%s does not have READ permission for %s', Horde_Auth::getAuth() ? 'User ' . Horde_Auth::getAuth() : 'Guest user', $app), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            return PEAR::raiseError(sprintf(_('%s is not authorised for %s.'), Horde_Auth::getAuth() ? 'User ' . Horde_Auth::getAuth() : 'Guest user', $this->applications[$app]['name']), 'permission_denied');
+        }
+
+        /* Set up autoload paths for the current application. This needs to
+         * be done here because it is possible to try to load app-specific
+         * libraries from other applications. */
+        $app_lib = $this->get('fileroot', $app) . '/lib';
+        Horde_Autoloader::addClassPath($app_lib);
+        Horde_Autoloader::addClassPattern('/^' . $app . '_/i', $app_lib);
+
+        /* Chicken and egg problem: the language environment has to be loaded
+         * before loading the configuration file, because it might contain
+         * gettext strings. Though the preferences can specify a different
+         * language for this app, the have to be loaded after the
+         * configuration, because they rely on configuration settings. So try
+         * with the current language, and reset the language later. */
+        NLS::setLanguageEnvironment($GLOBALS['language'], $app);
+
+        /* Import this application's configuration values. */
+        $success = $this->importConfig($app);
+        if (is_a($success, 'PEAR_Error')) {
+            return $success;
+        }
+
+        /* Load preferences after the configuration has been loaded to make
+         * sure the prefs file has all the information it needs. */
+        $this->loadPrefs($app);
+
+        /* Reset the language in case there is a different one selected in the
+         * preferences. */
+        $language = '';
+        if (isset($GLOBALS['prefs'])) {
+            $language = $GLOBALS['prefs']->getValue('language');
+            if ($language != $GLOBALS['language']) {
+                NLS::setLanguageEnvironment($language, $app);
+            }
+        }
+
+        /* Once we know everything succeeded and is in a consistent state
+         * again, push the new application onto the stack. */
+        $this->_appStack[] = $app;
+
+        /* Call post-push hook. */
+        Horde::callHook('_horde_hook_post_pushapp', array($app), 'horde', null);
+
+        return true;
+    }
+
+    /**
+     * Remove the current app from the application stack, setting the current
+     * app to whichever app was current before this one took over.
+     *
+     * @return string  The name of the application that was popped.
+     */
+    public function popApp()
+    {
+        /* Pop the current application off of the stack. */
+        $previous = array_pop($this->_appStack);
+
+        /* Import the new active application's configuration values
+         * and set the gettext domain and the preferred language. */
+        $app = $this->getApp();
+        if ($app) {
+            $this->importConfig($app);
+            $this->loadPrefs($app);
+            $language = $GLOBALS['prefs']->getValue('language');
+            NLS::setLanguageEnvironment($language, $app);
+        }
+
+        return $previous;
+    }
+
+    /**
+     * Return the current application - the app at the top of the application
+     * stack.
+     *
+     * @return string  The current application.
+     */
+    public function getApp()
+    {
+        return end($this->_appStack);
+    }
+
+    /**
+     * Check permissions on an application.
+     *
+     * @param string $app     The name of the application
+     * @param integer $perms  The permission level to check for.
+     *
+     * @return boolean  Whether access is allowed.
+     */
+    public function hasPermission($app, $perms = PERMS_READ)
+    {
+        return Horde_Auth::isAdmin() ||
+               ($GLOBALS['perms']->exists($app)
+                   ? $GLOBALS['perms']->hasPermission($app, Horde_Auth::getAuth(), $perms)
+                   : (bool)Horde_Auth::getAuth());
+    }
+
+    /**
+     * Reads the configuration values for the given application and imports
+     * them into the global $conf variable.
+     *
+     * @param string $app  The name of the application.
+     *
+     * @return boolean  True on success, PEAR_Error on error.
+     */
+    public function importConfig($app)
+    {
+        if (($app != 'horde') &&
+            !$this->_loadCacheVar('conf-' . $app)) {
+            $success = Horde::loadConfiguration('conf.php', 'conf', $app);
+            if (is_a($success, 'PEAR_Error')) {
+                return $success;
+            }
+            $this->_cache['conf-' . $app] = Horde_Array::array_merge_recursive_overwrite($this->_cache['conf-horde'], $success);
+            $this->_saveCacheVar('conf-' . $app);
+        }
+
+        $GLOBALS['conf'] = &$this->_cache['conf-' . $app];
+
+        return true;
+    }
+
+    /**
+     * Loads the preferences for the current user for the current application
+     * and imports them into the global $prefs variable.
+     *
+     * @param string $app  The name of the application.
+     */
+    public function loadPrefs($app = null)
+    {
+        require_once 'Horde/Prefs.php';
+
+        if (is_null($app)) {
+            $app = $this->getApp();
+        }
+
+        /* If there is no logged in user, return an empty Prefs::
+         * object with just default preferences. */
+        if (!Horde_Auth::getAuth()) {
+            $GLOBALS['prefs'] = &Prefs::factory('session', $app, '', '', null, false);
+        } else {
+            if (!isset($GLOBALS['prefs']) || $GLOBALS['prefs']->getUser() != Horde_Auth::getAuth()) {
+                $GLOBALS['prefs'] = &Prefs::factory($GLOBALS['conf']['prefs']['driver'], $app,
+                                                    Horde_Auth::getAuth(), Horde_Auth::getCredential('password'));
+            } else {
+                $GLOBALS['prefs']->retrieve($app);
+            }
+        }
+    }
+
+    /**
+     * Unload preferences from an application or (if no application is
+     * specified) from ALL applications. Useful when a user has logged
+     * out but you need to continue on the same page, etc.
+     *
+     * After unloading, if there is an application on the app stack to
+     * load preferences from, then we reload a fresh set.
+     *
+     * @param string $app  The application to unload prefrences for. If null,
+     *                     ALL preferences are reset.
+     */
+    public function unloadPrefs($app = null)
+    {
+        // TODO: $app not being used?
+        if ($this->getApp()) {
+            $this->loadPrefs();
+        }
+    }
+
+    /**
+     * Return the requested configuration parameter for the specified
+     * application. If no application is specified, the value of
+     * the current application is used. However, if the parameter is not
+     * present for that application, the Horde-wide value is used instead.
+     * If that is not present, we return null.
+     *
+     * @param string $parameter  The configuration value to retrieve.
+     * @param string $app        The application to get the value for.
+     *
+     * @return string  The requested parameter, or null if it is not set.
+     */
+    public function get($parameter, $app = null)
+    {
+        if (is_null($app)) {
+            $app = $this->getApp();
+        }
+
+        if (isset($this->applications[$app][$parameter])) {
+            $pval = $this->applications[$app][$parameter];
+        } else {
+            $pval = ($parameter == 'icon')
+                ? $this->getImageDir($app) . '/' . $app . '.png'
+                : (isset($this->applications['horde'][$parameter]) ? $this->applications['horde'][$parameter] : null);
+        }
+
+        return ($parameter == 'name')
+            ? _($pval)
+            : $pval;
+    }
+
+    /**
+     * Function to work out an application's graphics URI, optionally taking
+     * into account any themes directories that may be set up.
+     *
+     * @param string $app        The application for which to get the image
+     *                           directory. If blank will default to current
+     *                           application.
+     * @param boolean $usetheme  Take into account any theme directory?
+     *
+     * @return string  The image directory uri path.
+     */
+    public function getImageDir($app = null, $usetheme = true)
+    {
+        if (empty($app)) {
+            $app = $this->getApp();
+        }
+
+        if ($this->get('status', $app) == 'heading') {
+            $app = 'horde';
+        }
+
+        $sig = strval($app . '|' . $usetheme);
+
+        if (isset($this->_imgDir[$sig])) {
+            return $this->_imgDir[$sig];
+        }
+
+        /* This is the default location for the graphics. */
+        $this->_imgDir[$sig] = $this->get('themesuri', $app) . '/graphics';
+
+        /* Figure out if this is going to be overridden by any theme
+         * settings. */
+        if ($usetheme &&
+            isset($GLOBALS['prefs']) &&
+            ($theme = $GLOBALS['prefs']->getValue('theme'))) {
+            /* Since theme information is so limited, store directly in the
+             * session. */
+            if (!isset($_SESSION['_registry']['theme'][$theme][$app])) {
+                $_SESSION['_registry']['theme'][$theme][$app] = file_exists($this->get('themesfs', $app) . '/' . $theme . '/themed_graphics');
+            }
+
+            if ($_SESSION['_registry']['theme'][$theme][$app]) {
+                $this->_imgDir[$sig] = $this->get('themesuri', $app) . '/' . $theme . '/graphics';
+            }
+        }
+
+        return $this->_imgDir[$sig];
+    }
+
+    /**
+     * Query the initial page for an application - the webroot, if there is no
+     * initial_page set, and the initial_page, if it is set.
+     *
+     * @param string $app  The name of the application.
+     *
+     * @return string  URL pointing to the inital page of the application.
+     *                 Returns PEAR_Error on error.
+     */
+    public function getInitialPage($app = null)
+    {
+        if (is_null($app)) {
+            $app = $this->getApp();
+        }
+
+        return isset($this->applications[$app])
+            ? $this->applications[$app]['webroot'] . '/' . (isset($this->applications[$app]['initial_page']) ? $this->applications[$app]['initial_page'] : '')
+            : PEAR::raiseError(sprintf(_("\"%s\" is not configured in the Horde Registry."), $app));
+    }
+
+    /**
+     * Saves a cache variable.
+     *
+     * @param string $name     Cache variable name.
+     * @param boolean $expire  Expire the entry?
+     */
+    protected function _saveCacheVar($name, $expire = false)
+    {
+        if ($this->_cacheob) {
+            if ($expire) {
+                if ($id = $this->_getCacheId($name)) {
+                    $this->_cacheob->expire($id);
+                }
+            } else {
+                $data = serialize($this->_cache[$name]);
+                $_SESSION['_registry']['md5'][$name] = $md5sum = hash('md5', $data);
+                $id = $this->_getCacheId($name, false) . '|' . $md5sum;
+                $this->_cacheob->set($id, $data, 86400);
+                Horde::logMessage('Horde_Registry: stored ' . $name . ' with cache ID ' . $id, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            }
+        }
+    }
+
+    /**
+     * Retrieves a cache variable.
+     *
+     * @param string $name  Cache variable name.
+     *
+     * @return boolean  True if value loaded from cache.
+     */
+    protected function _loadCacheVar($name)
+    {
+        if (isset($this->_cache[$name])) {
+            return true;
+        }
+
+        if ($this->_cacheob &&
+            ($id = $this->_getCacheId($name))) {
+            $res = $this->_cacheob->get($id, 86400);
+            if ($res !== false) {
+                $this->_cache[$name] = unserialize($res);
+                Horde::logMessage('Horde_Registry: retrieved ' . $name . ' with cache ID ' . $id, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Get the cache storage ID for a particular cache name.
+     *
+     * @param string $name  Cache variable name.
+     * @param string $md5   Append MD5 value?
+     *
+     * @return mixed  The cache ID or false if cache entry doesn't exist in
+     *                the session.
+     */
+    protected function _getCacheId($name, $md5 = true)
+    {
+        $id = 'horde_registry_' . $name . '|' . $this->_regmtime;
+
+        if (!$md5) {
+            return $id;
+        } elseif (isset($_SESSION['_registry']['md5'][$name])) {
+            return $id . '|' . $_SESSION['_registry']['md5'][$name];
+        } else {
+            return false;
+        }
+    }
+
+}
diff --git a/framework/Core/lib/Horde/Horde/Registry/Caller.php b/framework/Core/lib/Horde/Horde/Registry/Caller.php
new file mode 100644 (file)
index 0000000..661b2bb
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+/**
+ * TODO
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @package Core
+ */
+class RegistryCaller
+{
+    /**
+     * TODO
+     */
+    protected $registry;
+
+    /**
+     * TODO
+     */
+    protected $api;
+
+    /**
+     * TODO
+     */
+    public function __construct($registry, $api)
+    {
+        $this->registry = $registry;
+        $this->api = $api;
+    }
+
+    /**
+     * TODO
+     */
+    public function __call($method, $args)
+    {
+        return $this->registry->call($this->api . '/' . $method, $args);
+    }
+
+}
diff --git a/framework/Core/lib/Horde/Horde/Release.php b/framework/Core/lib/Horde/Horde/Release.php
new file mode 100644 (file)
index 0000000..f93b298
--- /dev/null
@@ -0,0 +1,1067 @@
+<?php
+/**
+ * Class to make an "official" Horde or application release.
+ *
+ * Copyright 1999 Mike Hardy
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Hardy
+ * @author  Jan Schneider <jan@horde.org>
+ * @package Core
+ */
+class Horde_Release
+{
+    /**
+     * Default options.
+     *
+     * @var array
+     */
+    protected $_options = array(
+        'test' => false,
+        'nocommit' => false,
+        'noftp' => false,
+        'noannounce' => false,
+        'nofreshmeat' => false,
+        'nowhups' => false,
+    );
+
+    /**
+     * Version number of release.
+     *
+     * @var string
+     */
+    protected $_sourceVersionString;
+
+    /**
+     * Version number of previous release.
+     *
+     * @var string
+     */
+    protected $_oldSourceVersionString;
+
+    /**
+     * Version number of next release.
+     *
+     * @var string
+     */
+    protected $_newSourceVersionString;
+
+    /**
+     * Version number of next release for docs/CHANGES.
+     *
+     * @var string
+     */
+    protected $_newSourceVersionStringPlain;
+
+    /**
+     * Major version number of Horde compatible to this release.
+     *
+     * @var string
+     */
+    protected $_hordeVersionString;
+
+    /**
+     * Major version number of Horde compatible to the previous release.
+     *
+     * @var string
+     */
+    protected $_oldHordeVersionString;
+
+    /**
+     * CVS tag of release.
+     *
+     * @var string
+     */
+    protected $_tagVersionString;
+
+    /**
+     * CVS tag of previous release.
+     *
+     * @var string
+     */
+    protected $_oldTagVersionString;
+
+    /**
+     * Revision number of CHANGES file.
+     *
+     * @var string
+     */
+    protected $_changelogVersion;
+
+    /**
+     * Revision number of previous CHANGES file.
+     *
+     * @var string
+     */
+    protected $_oldChangelogVersion;
+
+    /**
+     * Version string to use in Whups
+     *
+     * @var string
+     */
+    protected $_ticketVersion;
+
+    /**
+     * Version description to use in Whups
+     *
+     * @var string
+     */
+    protected $_ticketVersionDesc = '';
+
+    /**
+     * Directory name of unpacked tarball.
+     *
+     * @var string
+     */
+    protected $_directoryName;
+
+    /**
+     * Directory name of unpacked previous tarball.
+     *
+     * @var string
+     */
+    protected $_oldDirectoryName;
+
+    /**
+     * Filename of the tarball.
+     *
+     * @var string
+     */
+    protected $_tarballName;
+
+    /**
+     * MD5 sum of the tarball.
+     *
+     * @var string
+     */
+    protected $_tarballMD5;
+
+    /**
+     * Whether or not to create a patch file.
+     *
+     * @var boolean
+     */
+    protected $_makeDiff = false;
+
+    /**
+     * The list of binary diffs.
+     *
+     * @var array
+     */
+    protected $_binaryDiffs = array();
+
+    /**
+     * Whether or not we have an old version to compare against.
+     *
+     * @var boolean
+     */
+    protected $_oldVersion = false;
+
+    /**
+     * Filename of the gzip'ed patch file (without .gz extension).
+     *
+     * @var string
+     */
+    protected $_patchName;
+
+    /**
+     * MD5 sum of the patch file.
+     *
+     * @var string
+     */
+    protected $_patchMD5;
+
+    /**
+     * Whether or not this is a final release version.
+     *
+     * @var boolean
+     */
+    protected $_latest = true;
+
+    /**
+     * Load the configuration
+     */
+    public function __construct($options = array())
+    {
+        $this->_options = array_merge($this->_options, $options);
+        $cvsroot = getenv('CVSROOT');
+        if (empty($cvsroot)) {
+            putenv('CVSROOT=:ext:' . $this->_options['horde']['user'] . '@cvs.horde.org:/repository');
+        }
+        print 'CVSROOT ' . getenv('CVSROOT') . "\n";
+        if (!empty($this->_options['cvs']['cvs_rsh'])) {
+            putenv('CVS_RSH=' . $this->_options['cvs']['cvs_rsh']);
+        }
+        print 'CVS_RSH ' . getenv('CVS_RSH') . "\n";
+    }
+
+    /**
+     * Delete the directory given as an argument
+     */
+    public function deleteDirectory($directory)
+    {
+        print "Deleting directory $directory\n";
+        system("sudo rm -rf $directory");
+    }
+
+    /**
+     * tar and gzip the directory given as an argument
+     */
+    public function makeTarball()
+    {
+        print "Setting owner/group to 0/0\n";
+        system("sudo chown -R 0:0 $this->_directoryName");
+
+        print "Making tarball\n";
+        $this->_tarballName = $this->_directoryName . '.tar.gz';
+        if (file_exists($this->_tarballName)) {
+            unlink($this->_tarballName);
+        }
+        system("tar -zcf $this->_tarballName $this->_directoryName");
+        exec($this->_options['md5'] . ' ' . $this->_tarballName, $this->_tarballMD5);
+    }
+
+    /**
+     * Label all of the source here with the new label given as an argument
+     */
+    public function tagSource($directory = null, $version = null)
+    {
+        if (empty($directory)) {
+            $directory = $this->_directoryName;
+        }
+        if (empty($version)) {
+            $version = $this->_tagVersionString;
+        }
+        if (!$this->_options['nocommit']) {
+            print "Tagging source in $directory with tag $version\n";
+            system("cd $directory;cvs tag -F $version > /dev/null 2>&1");
+        } else {
+            print "NOT tagging source in $directory (would have been tag $version)\n";
+        }
+    }
+
+    /**
+     * Make a diff of the two directories given as arguments
+     */
+    public function diff()
+    {
+        $this->_patchName = 'patch-' . $this->_oldDirectoryName . str_replace($this->_options['module'], '', $this->_directoryName);
+        print "Making diff between $this->_oldDirectoryName and $this->_directoryName\n";
+        system("diff -uNr $this->_oldDirectoryName $this->_directoryName > $this->_patchName");
+
+        // Search for binary diffs
+        $this->_binaryDiffs = array();
+        $handle = fopen($this->_patchName, 'r');
+        if ($handle) {
+            while (!feof($handle)) {
+                // GNU diff reports binary diffs as the following:
+                // Binary files ./locale/de_DE/LC_MESSAGES/imp.mo and ../../horde/imp/locale/de_DE/LC_MESSAGES/imp.mo differ
+                if (preg_match("/^Binary files (.+) and (.+) differ$/i", rtrim(fgets($handle)), $matches)) {
+                    // [1] = oldname, [2] = newname
+                    $this->_binaryDiffs[] = ltrim(str_replace($this->_oldDirectoryName . '/', '', $matches[1]));
+                }
+            }
+            fclose($handle);
+        }
+        system("gzip -9f $this->_patchName");
+        exec($this->_options['md5'] . ' ' . $this->_patchName . '.gz', $this->_patchMD5);
+    }
+
+    /**
+     * Change the version file for the module in the directory specified to
+     * the version specified
+     */
+    public function updateVersionFile($directory, $version_string)
+    {
+        $module = $this->_options['module'];
+        $all_caps_module = strtoupper($module);
+        print "Updating version file for $module\n";
+
+        // construct the filenames
+        $filename_only = 'version.php';
+        $filename = $directory . '/lib/' . $filename_only;
+        $newfilename = $filename . '.new';
+
+        $oldfp = fopen($filename, 'r');
+        $newfp = fopen($newfilename, 'w');
+        while ($line = fgets($oldfp)) {
+            if (strstr($line, 'VERSION')) {
+                fwrite($newfp, "<?php define('{$all_caps_module}_VERSION', '$version_string') ?>\n");
+            } else {
+                fwrite($newfp, $line);
+            }
+        }
+        fclose($oldfp);
+        fclose($newfp);
+
+        system("mv -f $newfilename $filename");
+        if (!$this->_options['nocommit']) {
+            system("cd $directory/lib/; cvs commit -f -m \"Tarball script: building new $module release - $version_string\" $filename_only > /dev/null 2>&1");
+        }
+    }
+
+    /**
+     * Update the CHANGES file with the new version number
+     */
+    public function updateSentinel()
+    {
+        $module = $this->_options['module'];
+        $all_caps_module = strtoupper($module);
+        print "Updating CHANGES file for $module\n";
+
+        // construct the filenames
+        $filename_only = 'CHANGES';
+        $filename = $this->_directoryName . '/docs/' . $filename_only;
+        $newfilename = $filename . '.new';
+
+        $version = 'v' . substr($this->_newSourceVersionStringPlain, 0, strpos($this->_newSourceVersionString, '-'));
+
+        $oldfp = fopen($filename, 'r');
+        $newfp = fopen($newfilename, 'w');
+        fwrite($newfp, str_repeat('-', strlen($version)) . "\n$version\n" .
+               str_repeat('-', strlen($version)) . "\n\n\n\n\n");
+        while ($line = fgets($oldfp)) {
+            fwrite($newfp, $line);
+        }
+        fclose($oldfp);
+        fclose($newfp);
+
+        system("mv -f $newfilename $filename");
+        if (!$this->_options['nocommit']) {
+            system("cd {$this->_directoryName}/docs/; cvs commit -f -m \"Tarball script: building new $module release - {$this->_newSourceVersionString}\" $filename_only > /dev/null 2>&1");
+        }
+    }
+
+    /**
+     * get and save the revision number of the CHANGES file
+     */
+    public function saveChangelog($old = false, $directory = null)
+    {
+        if (empty($directory)) {
+            if ($old) {
+                $directory = './' . $this->_oldDirectoryName . '/docs';
+            } else {
+                $directory = './' . $this->_directoryName . '/docs';
+            }
+        }
+        if (!$old) {
+            include "$directory/RELEASE_NOTES";
+            if (strlen(htmlspecialchars($this->notes['fm']['changes'])) > 600) {
+                print "WARNING: freshmeat release notes are longer than 600 characters!\n";
+            }
+        }
+        exec("cd $directory; cvs status CHANGES", $output);
+        foreach ($output as $line) {
+            if (preg_match('/Repository revision:\s+([\d.]+)/', $line, $matches)) {
+                if ($old) {
+                    $this->_oldChangelogVersion = $matches[1];
+                } else {
+                    $this->_changelogVersion = $matches[1];
+                }
+                break;
+            }
+        }
+    }
+
+    /**
+     * work through the source directory given, cleaning things up by removing
+     * directories and files we don't want in the tarball
+     */
+    public function cleanDirectories($directory)
+    {
+        print "Cleaning source tree\n";
+        $directories = explode("\n", `find $directory -type d \\( -name CVS -o -name packaging -o -name framework \\) -print | sort -r`);
+        foreach ($directories as $dir) {
+            system("rm -rf $dir");
+        }
+        $cvsignores = explode("\n", `find $directory -name .cvsignore -print`);
+        foreach ($cvsignores as $file) {
+            if (!empty($file)) {
+                unlink($file);
+            }
+        }
+    }
+
+    /**
+     * Check out the tag we've been given to work with and move it to the
+     * directory name given
+     */
+    public function checkOutTag($mod_version, $directory, $module = null)
+    {
+        if (empty($module)) {
+            $module = $this->_options['module'];
+        }
+
+        if (@is_dir($module)) {
+            system("rm -rf $module");
+        }
+
+        // Use CVS to check the source out
+        if ($mod_version == 'HEAD') {
+            print "Checking out HEAD for $module\n";
+            $cmd = "cvs -q co -P $module > /dev/null";
+            system($cmd, $status);
+        } else {
+            print "Checking out tag $mod_version for $module\n";
+            $cmd = "cvs -q co -P -r$mod_version $module > /dev/null";
+            system($cmd, $status);
+        }
+        if ($status) {
+            die("\nThere was an error running the command\n$cmd\n");
+        }
+
+        // Move the source into the directory specified
+        print "Moving $module to $directory\n";
+        if (@is_dir($directory)) {
+            system("rm -rf $directory");
+        }
+        system("mv -f $module $directory");
+    }
+
+    /**
+     * Checkout and install framework
+     */
+    public function checkOutFramework($mod_version, $directory)
+    {
+        if ($this->_options['module'] == 'horde' &&
+            ($this->_options['branch'] == 'HEAD' ||
+             strstr($this->_options['branch'], 'FRAMEWORK'))) {
+            if ($this->_options['branch'] == 'HEAD') {
+                print "Checking out HEAD for framework\n";
+            } else {
+                print "Checking out tag $mod_version for framework\n";
+            }
+            $cmd = "cd $directory; cvs co -P -r$mod_version framework > /dev/null 2>&1; cd ..";
+            system($cmd, $status);
+            if ($status) {
+                die("\nThere was an error running the command\n$cmd\n");
+            }
+            print "Installing framework packages\n";
+            if (file_exists("./$directory/scripts/create-symlinks.php")) {
+                system("php ./$directory/scripts/create-symlinks.php --copy --src=./$directory/framework --dest=./$directory/lib");
+            } else {
+                system("horde-fw-symlinks.php --copy --src ./$directory/framework --dest ./$directory/lib");
+            }
+
+            print "Setting include path\n";
+            $filename = $directory . '/lib/core.php';
+            $newfilename = $filename . '.new';
+            $oldfp = fopen($filename, 'r');
+            $newfp = fopen($newfilename, 'w');
+            while ($line = fgets($oldfp)) {
+                fwrite($newfp, str_replace('// ini_set(\'include_path\'', 'ini_set(\'include_path\'', $line));
+            }
+            fclose($oldfp);
+            fclose($newfp);
+            system("mv -f $newfilename $filename");
+        }
+    }
+
+    /**
+     * Upload tarball to the FTP server
+     */
+    public function upload()
+    {
+        $module = $this->_options['module'];
+        $user = $this->_options['horde']['user'];
+        $identity = empty($this->_options['ssh']['identity']) ? '' : ' -i ' . $this->_options['ssh']['identity'];
+        $chmod = "chmod 664 /horde/ftp/pub/$module/$this->_tarballName;";
+        if ($this->_makeDiff) {
+            $chmod .= " chmod 664 /horde/ftp/pub/$module/patches/$this->_patchName.gz;";
+        }
+        if ($this->_latest &&
+            strpos($this->_options['branch'], 'RELENG') !== 0) {
+            $chmod .= " ln -sf $this->_tarballName /horde/ftp/pub/$module/$module-latest.tar.gz;";
+        }
+
+        if (!$this->_options['noftp']) {
+            print "Uploading $this->_tarballName to $user@ftp.horde.org:/horde/ftp/pub/$module/\n";
+            system("scp -P 35$identity $this->_tarballName $user@ftp.horde.org:/horde/ftp/pub/$module/");
+            if ($this->_makeDiff) {
+                print "Uploading $this->_patchName.gz to $user@ftp.horde.org:/horde/ftp/pub/$module/patches/\n";
+                system("scp -P 35$identity $this->_patchName.gz $user@ftp.horde.org:/horde/ftp/pub/$module/patches/");
+            }
+            print "Executing $chmod\n";
+            system("ssh -p 35 -l $user$identity ftp.horde.org '$chmod'");
+        } else {
+            print "NOT uploading $this->_tarballName to ftp.horde.org:/horde/ftp/pub/$module/\n";
+            if ($this->_makeDiff) {
+                print "NOT uploading $this->_patchName.gz to $user@ftp.horde.org:/horde/ftp/pub/$module/patches/\n";
+            }
+            print "NOT executing $chmod\n";
+        }
+    }
+
+    /**
+     * check if freshmeat announcement was successful.
+     */
+    protected function _fmVerify($fm)
+    {
+        if (is_a($fm, 'PEAR_Error')) {
+            print $fm->getMessage() . "\n";
+            return false;
+        } elseif (!is_array($fm)) {
+            var_dump($fm);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * announce release to mailing lists and freshmeat.
+     */
+    public function announce($doc_dir = null)
+    {
+        $module = $this->_options['module'];
+        if (!isset($this->notes)) {
+            print "NOT announcing release, RELEASE_NOTES missing.\n";
+            return;
+        }
+        if (!empty($this->_options['noannounce']) ||
+            !empty($this->_options['nofreshmeat'])) {
+            print "NOT announcing release on freshmeat.net\n";
+        } else {
+            print "Announcing release on freshmeat.net\n";
+        }
+
+        if (empty($this->_options['nofreshmeat'])) {
+            $fm = Horde_RPC::request(
+                'xmlrpc',
+                'http://freshmeat.net/xmlrpc/',
+                'login',
+                array('username' => $this->_options['fm']['user'],
+                      'password' => $this->_options['fm']['password']));
+        } else {
+            $fm = array('SID' => null);
+        }
+        if (empty($doc_dir)) {
+            $doc_dir = $module . '/docs';
+        }
+
+        $url_changelog = $this->_oldVersion
+            ? "http://cvs.horde.org/diff.php/$doc_dir/CHANGES?r1={$this->_oldChangelogVersion}&r2={$this->_changelogVersion}&ty=h"
+            : '';
+
+        if (is_a($fm, 'PEAR_Error')) {
+            print $fm->getMessage() . "\n";
+        } else {
+            $announcement = array('SID' => $fm['SID'],
+                                  'project_name' => $this->notes['fm']['project'],
+                                  'branch_name' => $this->notes['fm']['branch'],
+                                  'version' => $this->_sourceVersionString,
+                                  'changes' => htmlspecialchars($this->notes['fm']['changes']),
+                                  'release_focus' => (int)$this->notes['fm']['focus'],
+                                  'url_changelog' => $url_changelog,
+                                  'url_tgz' => "ftp://ftp.horde.org/pub/$module/{$this->_tarballName}");
+            if ($this->_fmVerify($fm)) {
+                if (!empty($this->_options['noannounce']) ||
+                    !empty($this->_options['nofreshmeat'])) {
+                    print "Announcement data:\n";
+                    print_r($announcement);
+                } else {
+                    $fm = Horde_RPC::request(
+                        'xmlrpc',
+                        'http://freshmeat.net/xmlrpc/',
+                        'publish_release',
+                        $announcement);
+                    $this->_fmVerify($fm);
+                }
+            }
+        }
+
+        $ml = (!empty($this->notes['list'])) ? $this->notes['list'] : $module;
+        if (substr($ml, 0, 6) == 'horde-') {
+            $ml = 'horde';
+        }
+
+        $to = "announce@lists.horde.org, vendor@lists.horde.org, $ml@lists.horde.org";
+        if (!$this->_latest) {
+            $to .= ', i18n@lists.horde.org';
+        }
+
+        if (!empty($this->_options['noannounce'])) {
+            print "NOT announcing release on $to\n";
+        } else {
+            print "Announcing release to $to\n";
+        }
+
+        // Building headers
+        $subject = $this->notes['name'] . ' ' . $this->_sourceVersionString;
+        if ($this->_latest) {
+            $subject .= ' (final)';
+        }
+        if ($this->notes['fm']['focus'] == 9) {
+            $subject = '[SECURITY] ' . $subject;
+        }
+        $headers = array('From' => $this->_options['ml']['from'],
+                         'To' => $to,
+                         'Subject' => $subject);
+
+        // Building message text
+        $body = $this->notes['ml']['changes'];
+        if ($this->_oldVersion) {
+            $body .= "\n\n" .
+                sprintf('The full list of changes (from version %s) can be viewed here:', $this->_oldSourceVersionString) .
+                "\n\n" .
+                $url_changelog;
+        }
+        $body .= "\n\n" .
+            sprintf('The %s %s distribution is available from the following locations:', $this->notes['name'], $this->_sourceVersionString) .
+            "\n\n" .
+            sprintf('    ftp://ftp.horde.org/pub/%s/%s', $module, $this->_tarballName) . "\n" .
+            sprintf('    http://ftp.horde.org/pub/%s/%s', $module, $this->_tarballName);
+        if ($this->_makeDiff) {
+            $body .= "\n\n" .
+                sprintf('Patches against version %s are available at:', $this->_oldSourceVersionString) .
+                "\n\n" .
+                sprintf('    ftp://ftp.horde.org/pub/%s/patches/%s.gz', $module, $this->_patchName) . "\n" .
+                sprintf('    http://ftp.horde.org/pub/%s/patches/%s.gz', $module, $this->_patchName);
+
+            if (!empty($this->_binaryDiffs)) {
+                $body .= "\n\n" .
+                    'NOTE: Patches do not contain differences between files containing binary data.' . "\n" .
+                    'These files will need to be updated via the distribution files:' . "\n\n    " .
+                    implode("\n    ", $this->_binaryDiffs);
+            }
+        }
+        $body .= "\n\n" .
+            'Or, for quicker access, download from your nearest mirror:' .
+            "\n\n" .
+            '    http://www.horde.org/mirrors.php' .
+            "\n\n" .
+            'MD5 sums for the packages are as follows:' .
+            "\n\n" .
+            '    ' . $this->_tarballMD5[0] . "\n" .
+            '    ' . $this->_patchMD5[0] .
+            "\n\n" .
+            'Have fun!' .
+            "\n\n" .
+            'The Horde Team.';
+
+        if (!empty($this->_options['noannounce'])) {
+            print "Message headers:\n";
+            print_r($headers);
+            print "Message body:\n$body\n";
+            return;
+        }
+
+        // Building and sending message
+        $mail = new Horde_Mime_Mail();
+        $mail->setBody($body, 'iso-8859-1', false);
+        $mail->addHeaders($headers);
+        $result = $mail->send($this->_options['mailer']['type'], $this->_options['mailer']['params']);
+        if (is_a($result, 'PEAR_Error')) {
+            print $result->getMessage() . "\n";
+        }
+    }
+
+    /**
+     * Do testing (development only)
+     */
+    public function test()
+    {
+        if (!$this->_options['test']) {
+            return;
+        }
+
+        print "options['version']={$this->_options['version']}\n";
+        print "options['oldversion']={$this->_options['oldversion']}\n";
+        print "options['module']={$this->_options['module']}\n";
+        print "options['branch']={$this->_options['branch']}\n";
+
+        $this->setVersionStrings();
+
+        print "hordeVersionString={$this->_hordeVersionString}\n";
+        print "oldHordeVersionString={$this->_oldHordeVersionString}\n";
+        print "makeDiff={$this->_makeDiff}\n";
+        print "oldVersion={$this->_oldVersion}\n";
+        print "directoryName={$this->_directoryName}\n";
+        if ($this->_oldVersion) {
+            print "oldDirectoryName={$this->_oldDirectoryName}\n";
+        }
+        print "tagVersionString={$this->_tagVersionString}\n";
+        if ($this->_oldVersion) {
+            print "oldTagVersionString={$this->_oldTagVersionString}\n";
+        }
+        print "sourceVersionString={$this->_sourceVersionString}\n";
+        if ($this->_oldVersion) {
+            print "oldSourceVersionString={$this->_oldSourceVersionString}\n";
+        }
+        print "newSourceVersionString={$this->_newSourceVersionString}\n";
+        print "newSourceVersionStringPlain={$this->_newSourceVersionStringPlain}\n";
+        print "ticketVersion={$this->_ticketVersion}\n";
+        print "ticketVersionDesc=MODULE{$this->_ticketVersionDesc}\n";
+        if ($this->_latest) {
+            print "This is a production release\n";
+        }
+        exit(0);
+    }
+
+    /**
+     * Add the new version to bugs.horde.org
+     */
+    public function addWhupsVersion()
+    {
+        if (!isset($this->notes)) {
+            print "\nNOT updating bugs.horde.org, RELEASE_NOTES missing.\n";
+            return;
+        }
+        $this->_ticketVersionDesc = $this->notes['name'] . $this->_ticketVersionDesc;
+
+        $params = array('url' => 'https://dev.horde.org/horde/rpc.php',
+                        'user' => $this->_options['horde']['user'],
+                        'pass' => $this->_options['horde']['pass']);
+        $whups = new Horde_Release_Whups($params);
+
+        if (!$this->_options['nowhups']) {
+            print "Adding new versions to bugs.horde.org: ";
+            /* Set the new version in the queue */
+            try {
+                $whups->addNewVersion($this->_options['module'], $this->_ticketVersion, $this->_ticketVersionDesc);
+                print "OK\n";
+            } catch (Horde_Exception $e) {
+                print "Failed:\n";
+                print $e->getMessage() . "\n";
+            }
+        } else {
+            print "NOT updating bugs.horde.org:\n";
+            print "New ticket version WOULD have been {$this->_ticketVersion}\n";
+            print "New ticket version description WOULD have been {$this->_ticketVersionDesc}\n";
+
+            /* Perform some sanity checks on bugs.horde.org */
+            try {
+                $queue = $whups->getQueueId($this->_options['module']);
+
+                if ($queue === false) {
+                    print "Was UNABLE to locate the queue id for {$this->_options['module']}\n";
+                } else {
+                    print "The queue id on bugs.horde.org is $queue \n";
+                }
+            } catch (Horde_Exception $e) {
+                print "Will be UNABLE to update bugs.horde.org:\n";
+                print $e->getMessage() . "\n";
+            }
+        }
+    }
+
+    /**
+     * Set the version strings to use given the arguments
+     */
+    public function setVersionStrings()
+    {
+        $ver = explode('.', $this->_options['version']);
+        if (preg_match('/(\d+)\-(.*)/', $ver[count($ver) - 1], $matches)) {
+            $ver[count($ver) - 1] = $matches[1];
+            $plus = $matches[2];
+        }
+        if (preg_match('/(H\d)-(\d+)/', $ver[0], $matches)) {
+            $ver[0] = $matches[2];
+            $this->_hordeVersionString = $matches[1];
+        }
+        if (count($ver) > 2 && $ver[count($ver) - 1] == '0') {
+            die("version {$this->_options['version']} should not have the trailing 3rd-level .0\n");
+        }
+
+        // check if --oldversion is empty or 0
+        if (!empty($this->_options['oldversion'])) {
+            $this->_oldVersion = true;
+        }
+        $oldver = explode('.', $this->_options['oldversion']);
+        if (preg_match('/(\d+)\-(.*)/', $oldver[count($oldver) - 1], $matches)) {
+            $oldver[count($oldver) - 1] = $matches[1];
+            $oldplus = $matches[2];
+        }
+        if (preg_match('/(H\d)-(\d+)/', $oldver[0], $matches)) {
+            $oldver[0] = $matches[2];
+            $this->_oldHordeVersionString = $matches[1];
+        }
+
+        // set the string to use as the tag name in CVS
+        $this->_tagVersionString = strtoupper($this->_options['module'] . '_' . preg_replace('/\W/', '_', implode('_', $ver)));
+        if (isset($plus)) {
+            $this->_tagVersionString .= '_' . $plus;
+        }
+
+        // create patches only if not a major version change
+        if ($this->_options['oldversion'] && $ver[0] == $oldver[0]) {
+            $this->_makeDiff = true;
+        }
+
+        // is this really a production release?
+        if (isset($plus) && !preg_match('/^pl\d/', $plus)) {
+            $this->_latest = false;
+        }
+
+        // set the string to insert into the source version file
+        $this->_sourceVersionString = implode('.', $ver);
+        if (isset($plus)) {
+            $this->_sourceVersionString .= '-' . $plus;
+        }
+
+        // set the string to be used for the directory to package from
+        $this->_directoryName = $this->_options['module'] . '-';
+        if (!empty($this->_hordeVersionString)) {
+            $this->_directoryName .= $this->_hordeVersionString . '-';
+        }
+        $this->_directoryName = strtolower($this->_directoryName . $this->_sourceVersionString);
+
+        if (!empty($this->_hordeVersionString)) {
+            $this->_sourceVersionString = $this->_hordeVersionString . ' (' . $this->_sourceVersionString . ')';
+        }
+
+        if ($this->_oldVersion) {
+            $this->_oldSourceVersionString = implode('.', $oldver);
+            if (isset($oldplus)) {
+                $this->_oldSourceVersionString .= '-' . $oldplus;
+            }
+            $this->_oldTagVersionString = strtoupper($this->_options['module'] . '_' . implode('_', $oldver));
+            if (isset($oldplus)) {
+                $this->_oldTagVersionString .= '_' . $oldplus;
+            }
+            $this->_oldDirectoryName = strtolower($this->_options['module'] . '-' . $this->_oldHordeVersionString . $this->_oldSourceVersionString);
+            $this->_oldDirectoryName = $this->_options['module'] . '-';
+            if (!empty($this->_oldHordeVersionString)) {
+                $this->_oldDirectoryName .= $this->_oldHordeVersionString . '-';
+            }
+            $this->_oldDirectoryName = strtolower($this->_oldDirectoryName . $this->_oldSourceVersionString);
+
+            if (!empty($this->_oldHordeVersionString)) {
+                $this->_oldSourceVersionString = $this->_oldHordeVersionString . ' (' . $this->_oldSourceVersionString . ')';
+            }
+        }
+
+        // Set string to use for updating ticketing system.
+        $this->_ticketVersion = implode('.', $ver);
+        if (!empty($plus)) {
+            $this->_ticketVersion .= '-' . $plus;
+        }
+
+        if (!empty($this->_hordeVersionString)) {
+            $this->_ticketVersionDesc .= ' ' . $this->_hordeVersionString;
+        }
+
+        // Account for the 'special' case of the horde module.
+        if ($this->_options['module'] == 'horde') {
+            $this->_ticketVersionDesc .= ' ' . implode('.', $ver);
+        } else {
+            $this->_ticketVersionDesc .= ' ' . '(' . implode('.', $ver) . ')';
+        }
+
+        // See if we have a 'Final', 'Alpha', or 'RC' to add.
+        if ($this->_latest) {
+            $this->_ticketVersionDesc .= ' Final';
+        } elseif (!empty($plus) &&
+                  preg_match('/^RC(\d+)/', $plus, $matches)) {
+            $this->_ticketVersionDesc .= ' Release Candidate ' . $matches[1];
+
+        } elseif (!empty($plus) && strtolower($plus) == 'alpha') {
+            $this->_ticketVersionDesc .= ' Alpha';
+        }
+
+        // set the name of the string to put into the source version file when
+        // done
+        if (!isset($plus)) {
+            while (count($ver) < 3) {
+                $ver[] = '0';
+            }
+            $ver[count($ver) - 1] += 1;
+        }
+        $this->_newSourceVersionString = implode('.', $ver) . '-cvs';
+        $this->_newSourceVersionStringPlain = $this->_newSourceVersionString;
+
+        if (!empty($this->_hordeVersionString)) {
+            $this->_newSourceVersionString = $this->_hordeVersionString .
+                ' (' . $this->_newSourceVersionString . ')';
+        }
+
+    }
+
+    /**
+     * Get all of the command-line arguments from the user
+     */
+    public function getArguments()
+    {
+        global $argv;
+
+        // Parse the command-line arguments
+        array_shift($argv);
+        foreach ($argv as $arg) {
+            // Check to see if they gave us a module
+            if (preg_match('/--module=(.*)/', $arg, $matches)) {
+                $this->_options['module'] = $matches[1];
+
+            // Check to see if they tell us the version of the tarball to make
+            } elseif (preg_match('/--version=(.*)/', $arg, $matches)) {
+                $this->_options['version']= $matches[1];
+
+            // Check to see if they tell us the last release version
+            } elseif (preg_match('/--oldversion=(.*)/', $arg, $matches)) {
+                $this->_options['oldversion']= $matches[1];
+
+            // Check to see if they tell us which branch to work with
+            } elseif (preg_match('/--branch=(.*)/', $arg, $matches)) {
+                $this->_options['branch']= $matches[1];
+
+            // Check to see if they tell us not to commit or tag
+            } elseif (strstr($arg, '--nocommit')) {
+                $this->_options['nocommit']= true;
+
+            // Check to see if they tell us not to upload
+            } elseif (strstr($arg, '--noftp')) {
+                $this->_options['noftp']= true;
+
+            // Check to see if they tell us not to announce
+            } elseif (strstr($arg, '--noannounce')) {
+                $this->_options['noannounce']= true;
+
+            // Check to see if they tell us not to announce
+            } elseif (strstr($arg, '--nofreshmeat')) {
+                $this->_options['nofreshmeat']= true;
+
+            // Check to see if they tell us not to add new ticket versions
+            } elseif (strstr($arg, '--noticketversion')) {
+                $this->_options['nowhups'] = true;
+
+            // Check to see if they tell us to do a dry run
+            } elseif (strstr($arg, '--dryrun')) {
+                $this->_options['nocommit'] = true;
+                $this->_options['noftp'] = true;
+                $this->_options['noannounce'] = true;
+                $this->_options['nowhups'] = true;
+                $this->_options['nofreshmeat']= true;
+
+            // Check to see if they tell us to test (for development only)
+            } elseif (strstr($arg, '--test')) {
+                $this->_options['test']= true;
+                // safety first
+                $this->_options['nocommit'] = true;
+                $this->_options['noftp'] = true;
+                $this->_options['noannounce'] = true;
+                $this->_options['nowhups'] = true;
+                $this->_options['nofreshmeat']= true;
+
+            // Check for help usage.
+            } elseif (strstr($arg, '--help')) {
+                $this->print_usage();
+                exit;
+
+            // We have no idea what this is
+            } else {
+                $this->print_usage('You have used unknown arguments: ' . $arg);
+                exit;
+            }
+        }
+    }
+
+    /**
+     * Check the command-line arguments and set some internal defaults
+     */
+    public function checkArguments()
+    {
+        // Make sure that we have a module defined
+        if (!isset($this->_options['module'])) {
+            $this->print_usage('You must define which module to package.');
+            exit;
+        }
+
+        // Let's make sure that there are valid version strings in here...
+        if (!isset($this->_options['version'])) {
+            $this->print_usage('You must define which version to package.');
+            exit;
+        }
+        if (!preg_match('/\d+\.\d+.*/', $this->_options['version'])) {
+            $this->print_usage('Incorrect version string.');
+            exit;
+        }
+        if (!isset($this->_options['oldversion'])) {
+            $this->print_usage('You must define last release\'s version.');
+            exit;
+        }
+        if (!preg_match('/\d+(\.\d+.*)?/', $this->_options['oldversion'])) {
+            $this->print_usage('Incorrect old version string.');
+            exit;
+        }
+
+        // Make sure we have a horde.org user
+        if (empty($this->_options['horde']['user'])) {
+            $this->print_usage('You must define a horde.org user.');
+            exit;
+        }
+
+        // If there is no branch defined, we're using the tip revisions.
+        // These releases are always developmental, and should use the HEAD "branch" name.
+        if (!isset($this->_options['branch'])) {
+            $this->_options['branch'] = 'HEAD';
+        }
+    }
+
+    /**
+     * Check the command-line arguments and set some internal defaults
+     */
+    public function checkSetSystem()
+    {
+        // Set umask
+        umask(022);
+    }
+
+    /**
+     * Show people how to use the damned thing
+     */
+    public function print_usage($message = null)
+    {
+        if (!is_null($message)) {
+            print "\n***  ERROR: $message  ***\n";
+        }
+
+        print <<<USAGE
+
+make-release.php: Horde release generator.
+
+   This script takes as arguments the module to make a release of, the
+   version of the release, and the branch:
+
+      horde-make-release.php --module=<name>
+                         --version=[Hn-]xx.yy[.zz[-<string>]]
+                         --oldversion=[Hn-]xx[.yy[.zz[-<string>]]]
+                         [--branch=<branchname>] [--nocommit] [--noftp]
+                         [--noannounce] [--nofreshmeat] [--noticketversion]
+                         [--test] [--dryrun] [--help]
+
+   If you omit the branch, it will implicitly work with the HEAD branch.
+   If you release a new major version use the --oldversion=0 option.
+   Use the --nocommit option to do a test build (without touching the CVS
+   repository).
+   Use the --noftp option to not upload any files on the FTP server.
+   Use the --noannounce option to not send any release announcements.
+   Use the --nofreshmeat option to not send any freshmeat announcements.
+   Use the --noticketversion option to not update the version information on
+   bugs.horde.org.
+   The --dryrun option is an alias for:
+     --nocommit --noftp --noannounce --nofreshmeat --noticketversion.
+   The --test option is for debugging purposes only.
+
+   EXAMPLES:
+
+   To make a new development release of Horde:
+      horde-make-release.php --module=horde --version=2.1-dev --oldversion=2.0
+
+   To make a new stable release of Turba:
+      horde-make-release.php --module=turba --version=H3-2.0.2 \
+        --oldversion=H3-2.0.1 --branch=FRAMEWORK_3
+
+   To make a new stable release of IMP 3:
+      horde-make-release.php --module=imp --version=3.0 --oldversion=2.3.7 \
+        --branch=RELENG_3
+
+   To make a brand new Alpha/Beta/RC release of Luxor:
+      horde-make-release.php --module=luxor --version=H3-1.0-ALPHA \
+        --oldversion=0
+
+USAGE;
+    }
+
+}
diff --git a/framework/Core/lib/Horde/Horde/Release/Whups.php b/framework/Core/lib/Horde/Horde/Release/Whups.php
new file mode 100644 (file)
index 0000000..27ec2fa
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+/**
+ * Class for interfacing with the tickets API.
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Core
+ */
+class Horde_Release_Whups
+{
+    /**
+     * Instance of XML_RPC_Client object
+     *
+     * @var XML_RPC_CLient
+     */
+    protected $_client;
+
+    /**
+     * Local copy of config params.
+     *
+     * @var array
+     */
+    protected $_params;
+
+    /**
+     * Constructor.
+     *
+     * @param array $params  TODO
+     */
+    public function __construct($params)
+    {
+        $this->_params = $params;
+    }
+
+    /**
+     * Add a new version to the current modules queue.
+     *
+     * @param string $module   The name of the module.
+     * @param string $version  The version string.
+     * @param string $desc     Descriptive text for this version.
+     *
+     * @throws Horde_Exception
+     */
+    public function addNewVersion($module, $version, $desc = '')
+    {
+        if ($module == 'horde') {
+            $module = 'horde base';
+        }
+
+        $id = $this->getQueueId($module);
+        if ($id === false) {
+            throw new Horde_Exception('Unable to locate requested queue');
+        }
+
+        $method = 'tickets.addVersion';
+        $params = array($id, $version, $desc);
+        $options = array('user' => $this->_params['user'],
+                         'pass' => $this->_params['pass']);
+
+        $res = Horde_RPC::request('jsonrpc', $this->_params['url'], $method, $params, $options);
+        if ($res instanceof PEAR_Error) {
+            throw new Horde_Exception($res);
+        }
+    }
+
+    /**
+     * Look up the queue id for the requested module name.
+     *
+     * @param string $module  TODO
+     *
+     * @return boolean  TODO
+     */
+    function getQueueId($module)
+    {
+        $queues = $this->_listQueues();
+
+        foreach ($queues as $id => $queue) {
+            if (strtolower($queue) == $module) {
+                return $id;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Perform a listQueue api call.
+     *
+     * @return string  TODO
+     * @throws Horde_Exception
+     */
+    protected function _listQueues()
+    {
+        $method = 'tickets.listQueues';
+        $result = Horde_RPC::request('jsonrpc', $this->_params['url'], $method,
+                                     null, array('user' => $this->_params['user'],
+                                                 'pass' => $this->_params['pass']));
+        if ($result instanceof PEAR_Error) {
+            throw new Horde_Exception($result);
+        }
+
+        return $result->result;
+    }
+
+}
diff --git a/framework/Core/lib/Horde/Horde/Script/Files.php b/framework/Core/lib/Horde/Horde/Script/Files.php
new file mode 100644 (file)
index 0000000..c9c06cb
--- /dev/null
@@ -0,0 +1,270 @@
+<?php
+/**
+ * The Horde_Script_Files:: class provides a coherent way to manage script
+ * files for inclusion in Horde output.  This class is meant to be used
+ * internally by Horde:: only.
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Michael Slusarz <slusarz@horde.org>
+ * @package Core
+ */
+class Horde_Script_Files
+{
+    /**
+     * The singleton instance.
+     *
+     * @var Horde_Script_Files
+     */
+    static protected $_instance;
+
+    /**
+     * The list of script files to add.
+     *
+     * @var array
+     */
+    protected $_files = array();
+
+    /**
+     * The list of files we have already included.
+     *
+     * @var array
+     */
+    protected $_included = array();
+
+    /**
+     * The list of deprecated files.
+     *
+     * @var array
+     */
+    protected $_ignored = array(
+        'horde' => array('tooltip.js')
+    );
+
+    /**
+     * The list of javascript files to always load from Horde.
+     *
+     * @var array
+     */
+    protected $_fromhorde = array('prototype.js');
+
+    /**
+     * The list of javscript files in Horde that have prototypejs'd versions.
+     *
+     * @var array
+     */
+    protected $_ptversions = array('tables.js', 'stripe.js');
+
+    /**
+     * Auto load horde.js?
+     *
+     * @var boolean
+     */
+    protected $_loadhordejs = true;
+
+    /**
+     * Singleton.
+     */
+    static public function singleton()
+    {
+        if (!self::$_instance) {
+            self::$_instance = new Horde_Script_Files();
+        }
+
+        return self::$_instance;
+    }
+
+    /**
+     * Adds the javascript code to the output (if output has already started)
+     * or to the list of script files to include.
+     *
+     * @param string $file     The full javascript file name.
+     * @param string $app      The application name. Defaults to the current
+     *                         application.
+     * @param boolean $direct  Include the file directly without passing it
+     *                         through javascript.php?
+     * @param boolean $full    Output a full url
+     */
+    public function add($file, $app = null, $direct = false, $full = false)
+    {
+        $res = $this->_add($file, $app, $direct, $full);
+
+        if (empty($res) || (!ob_get_length() && !headers_sent())) {
+            return;
+        }
+
+        // If headers have already been sent, we need to output a <script>
+        // tag directly.
+        echo '<script type="text/javascript" src="' . $res['u'] . '"></script>' . "\n";
+    }
+
+    /**
+     * Helper function to determine if given file needs to be output.
+     */
+    public function _add($file, $app, $direct, $full = false)
+    {
+        global $registry;
+
+        if (empty($app)) {
+            $app = $registry->getApp();
+        }
+
+        // Skip any js files that have since been deprecated.
+        if (!empty($this->_ignored[$app]) &&
+            in_array($file, $this->_ignored[$app])) {
+            return false;
+        }
+
+        // Several files will always be the same thing. Don't distinguish
+        // between loading them in different $app scopes; always load them
+        // from Horde scope.
+        if (in_array($file, $this->_fromhorde)) {
+            $app = 'horde';
+        }
+
+        // Don't include scripts multiple times.
+        if (!empty($this->_included[$app][$file])) {
+            return false;
+        }
+        $this->_included[$app][$file] = true;
+
+        // Explicitly check for a directly serve-able version of the script.
+        $path = $GLOBALS['registry']->get('fileroot', $app);
+        if (!$direct &&
+            file_exists($file[0] == '/'
+                        ? $path . $file
+                        : $registry->get('jsfs', $app) . '/' . $file)) {
+            $direct = true;
+        }
+
+        if ($direct) {
+            if ($file[0] == '/') {
+                $url = Horde::url($registry->get('webroot', $app) . $file,
+                                  $full, -1);
+            } else {
+                $url = Horde::url($registry->get('jsuri', $app) . '/' . $file,
+                                  $full, -1);
+                $path = $registry->get('jsfs', $app) . '/';
+            }
+
+        } else {
+            $path = $registry->get('templates', $app) . '/javascript/';
+            $url = Horde::url(
+                Horde_Util::addParameter(
+                    $registry->get('webroot', 'horde') . '/services/javascript.php',
+                    array('file' => $file, 'app' => $app)));
+        }
+
+        $out = $this->_files[$app][] = array('f' => $file, 'd' => $direct, 'u' => $url, 'p' => $path);
+        return $out;
+    }
+
+    /**
+     * Includes javascript files that are needed before any headers are sent.
+     */
+    public function includeFiles()
+    {
+        foreach ($this->listFiles() as $app => $files) {
+            foreach ($files as $file) {
+                echo '<script type="text/javascript" src="' . $file['u'] . '"></script>' . "\n";
+            }
+        }
+    }
+
+    /**
+     * Prepares the list of javascript files to include.
+     *
+     * @return array
+     */
+    public function listFiles()
+    {
+        /* If there is no javascript available, there's no point in including
+         * the rest of the files. */
+        if (!$GLOBALS['browser']->hasFeature('javascript')) {
+            return array();
+        }
+
+        $prototype = false;
+
+        // Always include Horde-level scripts first.
+        if (!empty($this->_files['horde'])) {
+            foreach ($this->_files['horde'] as $file) {
+                if ($file['f'] == 'prototype.js') {
+                    $prototype = true;
+                    break;
+                }
+            }
+
+            if (!$prototype) {
+                $keys = array_keys($this->_files['horde']);
+                foreach ($keys as $key) {
+                    $file = $this->_files['horde'][$key];
+                    if (in_array($file['f'], $this->_ptversions)) {
+                        $this->_add('prototype.js', 'horde', true);
+                        $prototype = true;
+                        break;
+                    }
+                }
+            }
+
+            // prototype.js must be included before any script that uses it
+            if ($prototype) {
+                $keys = array_keys($this->_files['horde']);
+                foreach ($keys as $key) {
+                    $file = $this->_files['horde'][$key];
+                    if ($file['f'] == 'prototype.js') {
+                        unset($this->_files['horde'][$key]);
+                        array_unshift($this->_files['horde'], $file);
+                    }
+                }
+                reset($this->_files);
+            }
+        }
+
+        /* Add general UI js library. */
+        if ($this->_loadhordejs) {
+            $this->_add('prototype.js', 'horde', true);
+            $this->_add('horde.js', 'horde', true);
+        }
+
+        /* Add accesskeys.js if access keys are enabled. */
+        if ($GLOBALS['prefs']->getValue('widget_accesskey')) {
+            $this->_add('prototype.js', 'horde', true);
+            $this->_add('accesskeys.js', 'horde', true);
+        }
+
+        /* Make sure 'horde' entries appear first. */
+        reset($this->_files);
+        if (key($this->_files) == 'horde') {
+            return $this->_files;
+        }
+
+        if (isset($this->_files['horde'])) {
+            $jslist = array('horde' => $this->_files['horde']);
+        } else {
+            $jslist = array();
+        }
+        foreach ($this->_files as $key => $val) {
+            if ($key != 'horde') {
+                $jslist[$key] = $val;
+            }
+        }
+
+        return $jslist;
+    }
+
+    /**
+     * Disable auto-loading of the horde.js script.
+     * Needs to auto-load by default for BC.
+     *
+     * @todo Remove for Horde 4
+     */
+    public function disableAutoloadHordeJS()
+    {
+        $this->_loadhordejs = false;
+    }
+
+}
diff --git a/framework/Core/package.xml b/framework/Core/package.xml
new file mode 100644 (file)
index 0000000..050a4f3
--- /dev/null
@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Core</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Core Framework libraries</summary>
+ <description>These classes provide the core functionality of the Horde
+Application Framework.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <developer>
+  <name>Michael Slusarz</name>
+  <user>slusarz</user>
+  <email>slusarz@horde.org</email>
+  <active>yes</active>
+ </developer>
+ <date>2009-07-08</date>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Renamed Menu:: as Horde_Menu::.
+ * Renamed Help:: as Horde_Help::.
+ * Removed Text::/Horde_Text::.
+ * Converted Horde to Horde 4 coding conventions.
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Horde">
+      <file name="Config.php" role="php" />
+      <file name="ErrorHandler.php" role="php" />
+      <file name="Exception.php" role="php" />
+      <file name="Help.php" role="php" />
+      <file name="Menu.php" role="php" />
+      <file name="Registry.php" role="php" />
+      <dir name="Registry">
+       <file name="Caller.php" role="php" />
+      </dir> <!-- /lib/Horde/Horde/Registry -->
+      <dir name="Script">
+       <file name="Files.php" role="php" />
+      </dir> <!-- /lib/Horde/Horde/Script -->
+     </dir> <!-- /lib/Horde/Horde -->
+     <file name="Horde.php" role="php" />
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+   <dir name="test">
+    <dir name="Horde">
+     <dir name="Core">
+      <file name="url.phpt" role="test" />
+     </dir> <!-- /test/Horde/Core -->
+    </dir> <!-- /test/Horde -->
+   </dir> <!-- /test -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.5.4</min>
+   </pearinstaller>
+   <package>
+    <name>Log</name>
+    <channel>pear.php.net</channel>
+   </package>
+   <package>
+    <name>Cli</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>DOM</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Prefs</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Util</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+  <optional>
+   <package>
+    <name>Browser</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Form</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </optional>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="lib/Horde/Horde/Config.php" as="Horde/Config.php" />
+   <install name="lib/Horde/Horde/ErrorHandler.php" as="Horde/ErrorHandler.php" />
+   <install name="lib/Horde/Horde/Exception.php" as="Horde/Exception.php" />
+   <install name="lib/Horde/Horde/Help.php" as="Horde/Help.php" />
+   <install name="lib/Horde/Horde/Menu.php" as="Horde/Menu.php" />
+   <install name="lib/Horde/Horde/Registry.php" as="Horde/Registry.php" />
+   <install name="lib/Horde/Horde/Registry/Caller.php" as="Horde/Registry/Caller.php" />
+   <install name="lib/Horde/Horde/Script/Files.php" as="Horde/Script/Files.php" />
+   <install name="lib/Horde/Horde.php" as="Horde.php" />
+  </filelist>
+ </phprelease>
+ <changelog>
+  <release>
+   <date>2006-05-08</date>
+   <time>21:57:00</time>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>
+   * Converted to package.xml 2.0 for pear.horde.org
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2004-02-13</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial packaging
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Core/test/Horde/Framework/url.phpt b/framework/Core/test/Horde/Framework/url.phpt
new file mode 100644 (file)
index 0000000..6a9e0b6
--- /dev/null
@@ -0,0 +1,290 @@
+--TEST--
+Horde::url() tests
+--FILE--
+<?php
+
+require_once dirname(__FILE__) . '/../../../lib/Horde/Horde.php';
+
+class Registry {
+
+    function get()
+    {
+        return '/hordeurl';
+    }
+
+}
+
+$registry = new Horde_Registry();
+$conf['server']['name'] = 'example.com';
+
+$uris = array(
+    'test.php',
+    'test.php?foo=1',
+    'test.php?foo=1&bar=2',
+    'test.php?foo=1&amp;bar=2',
+    'test.php?foo=1&amp;bar=2&amp;baz=3'
+);
+
+$fulls = array(false, true);
+$ssls = array(0, 1, 3);
+$ports = array(80, 443);
+
+foreach ($uris as $uri) {
+    foreach ($fulls as $full) {
+        foreach ($ssls as $ssl) {
+            $conf['use_ssl'] = $ssl;
+            foreach ($ports as $port) {
+                $conf['server']['port'] = $port;
+                echo Horde::url($uri, $full, -1) . "\n";
+                unset($_COOKIE[session_name()]);
+                echo Horde::url($uri, $full, 0) . "\n";
+                $_COOKIE[session_name()] = array();
+                echo Horde::url($uri, $full, 0) . "\n";
+                echo Horde::url($uri, $full, 1) . "\n";
+            }
+        }
+    }
+}
+
+?>
+--EXPECT--
+test.php
+test.php?PHPSESSID=
+test.php
+test.php?PHPSESSID=
+test.php
+test.php?PHPSESSID=
+test.php
+test.php?PHPSESSID=
+test.php
+test.php?PHPSESSID=
+test.php
+test.php?PHPSESSID=
+test.php
+test.php?PHPSESSID=
+test.php
+test.php?PHPSESSID=
+test.php
+test.php?PHPSESSID=
+test.php
+test.php?PHPSESSID=
+test.php
+test.php?PHPSESSID=
+test.php
+test.php?PHPSESSID=
+http://example.com/hordeurl/test.php
+http://example.com/hordeurl/test.php?PHPSESSID=
+http://example.com/hordeurl/test.php
+http://example.com/hordeurl/test.php?PHPSESSID=
+http://example.com:443/hordeurl/test.php
+http://example.com:443/hordeurl/test.php?PHPSESSID=
+http://example.com:443/hordeurl/test.php
+http://example.com:443/hordeurl/test.php?PHPSESSID=
+https://example.com:80/hordeurl/test.php
+https://example.com:80/hordeurl/test.php?PHPSESSID=
+https://example.com:80/hordeurl/test.php
+https://example.com:80/hordeurl/test.php?PHPSESSID=
+https://example.com/hordeurl/test.php
+https://example.com/hordeurl/test.php?PHPSESSID=
+https://example.com/hordeurl/test.php
+https://example.com/hordeurl/test.php?PHPSESSID=
+http://example.com/hordeurl/test.php
+http://example.com/hordeurl/test.php?PHPSESSID=
+http://example.com/hordeurl/test.php
+http://example.com/hordeurl/test.php?PHPSESSID=
+http://example.com/hordeurl/test.php
+http://example.com/hordeurl/test.php?PHPSESSID=
+http://example.com/hordeurl/test.php
+http://example.com/hordeurl/test.php?PHPSESSID=
+test.php?foo=1
+test.php?foo=1&amp;PHPSESSID=
+test.php?foo=1
+test.php?foo=1&amp;PHPSESSID=
+test.php?foo=1
+test.php?foo=1&amp;PHPSESSID=
+test.php?foo=1
+test.php?foo=1&amp;PHPSESSID=
+test.php?foo=1
+test.php?foo=1&amp;PHPSESSID=
+test.php?foo=1
+test.php?foo=1&amp;PHPSESSID=
+test.php?foo=1
+test.php?foo=1&amp;PHPSESSID=
+test.php?foo=1
+test.php?foo=1&amp;PHPSESSID=
+test.php?foo=1
+test.php?foo=1&amp;PHPSESSID=
+test.php?foo=1
+test.php?foo=1&amp;PHPSESSID=
+test.php?foo=1
+test.php?foo=1&amp;PHPSESSID=
+test.php?foo=1
+test.php?foo=1&amp;PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1
+http://example.com/hordeurl/test.php?foo=1&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1
+http://example.com/hordeurl/test.php?foo=1&PHPSESSID=
+http://example.com:443/hordeurl/test.php?foo=1
+http://example.com:443/hordeurl/test.php?foo=1&PHPSESSID=
+http://example.com:443/hordeurl/test.php?foo=1
+http://example.com:443/hordeurl/test.php?foo=1&PHPSESSID=
+https://example.com:80/hordeurl/test.php?foo=1
+https://example.com:80/hordeurl/test.php?foo=1&PHPSESSID=
+https://example.com:80/hordeurl/test.php?foo=1
+https://example.com:80/hordeurl/test.php?foo=1&PHPSESSID=
+https://example.com/hordeurl/test.php?foo=1
+https://example.com/hordeurl/test.php?foo=1&PHPSESSID=
+https://example.com/hordeurl/test.php?foo=1
+https://example.com/hordeurl/test.php?foo=1&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1
+http://example.com/hordeurl/test.php?foo=1&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1
+http://example.com/hordeurl/test.php?foo=1&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1
+http://example.com/hordeurl/test.php?foo=1&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1
+http://example.com/hordeurl/test.php?foo=1&PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2
+http://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2
+http://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+http://example.com:443/hordeurl/test.php?foo=1&bar=2
+http://example.com:443/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+http://example.com:443/hordeurl/test.php?foo=1&bar=2
+http://example.com:443/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+https://example.com:80/hordeurl/test.php?foo=1&bar=2
+https://example.com:80/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+https://example.com:80/hordeurl/test.php?foo=1&bar=2
+https://example.com:80/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+https://example.com/hordeurl/test.php?foo=1&bar=2
+https://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+https://example.com/hordeurl/test.php?foo=1&bar=2
+https://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2
+http://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2
+http://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2
+http://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2
+http://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2
+test.php?foo=1&amp;bar=2&amp;PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2
+http://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2
+http://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+http://example.com:443/hordeurl/test.php?foo=1&bar=2
+http://example.com:443/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+http://example.com:443/hordeurl/test.php?foo=1&bar=2
+http://example.com:443/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+https://example.com:80/hordeurl/test.php?foo=1&bar=2
+https://example.com:80/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+https://example.com:80/hordeurl/test.php?foo=1&bar=2
+https://example.com:80/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+https://example.com/hordeurl/test.php?foo=1&bar=2
+https://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+https://example.com/hordeurl/test.php?foo=1&bar=2
+https://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2
+http://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2
+http://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2
+http://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2
+http://example.com/hordeurl/test.php?foo=1&bar=2&PHPSESSID=
+test.php?foo=1&amp;bar=2&amp;baz=3
+test.php?foo=1&amp;bar=2&amp;baz=3&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2&amp;baz=3
+test.php?foo=1&amp;bar=2&amp;baz=3&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2&amp;baz=3
+test.php?foo=1&amp;bar=2&amp;baz=3&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2&amp;baz=3
+test.php?foo=1&amp;bar=2&amp;baz=3&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2&amp;baz=3
+test.php?foo=1&amp;bar=2&amp;baz=3&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2&amp;baz=3
+test.php?foo=1&amp;bar=2&amp;baz=3&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2&amp;baz=3
+test.php?foo=1&amp;bar=2&amp;baz=3&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2&amp;baz=3
+test.php?foo=1&amp;bar=2&amp;baz=3&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2&amp;baz=3
+test.php?foo=1&amp;bar=2&amp;baz=3&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2&amp;baz=3
+test.php?foo=1&amp;bar=2&amp;baz=3&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2&amp;baz=3
+test.php?foo=1&amp;bar=2&amp;baz=3&amp;PHPSESSID=
+test.php?foo=1&amp;bar=2&amp;baz=3
+test.php?foo=1&amp;bar=2&amp;baz=3&amp;PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2&baz=3
+http://example.com/hordeurl/test.php?foo=1&bar=2&baz=3&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2&baz=3
+http://example.com/hordeurl/test.php?foo=1&bar=2&baz=3&PHPSESSID=
+http://example.com:443/hordeurl/test.php?foo=1&bar=2&baz=3
+http://example.com:443/hordeurl/test.php?foo=1&bar=2&baz=3&PHPSESSID=
+http://example.com:443/hordeurl/test.php?foo=1&bar=2&baz=3
+http://example.com:443/hordeurl/test.php?foo=1&bar=2&baz=3&PHPSESSID=
+https://example.com:80/hordeurl/test.php?foo=1&bar=2&baz=3
+https://example.com:80/hordeurl/test.php?foo=1&bar=2&baz=3&PHPSESSID=
+https://example.com:80/hordeurl/test.php?foo=1&bar=2&baz=3
+https://example.com:80/hordeurl/test.php?foo=1&bar=2&baz=3&PHPSESSID=
+https://example.com/hordeurl/test.php?foo=1&bar=2&baz=3
+https://example.com/hordeurl/test.php?foo=1&bar=2&baz=3&PHPSESSID=
+https://example.com/hordeurl/test.php?foo=1&bar=2&baz=3
+https://example.com/hordeurl/test.php?foo=1&bar=2&baz=3&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2&baz=3
+http://example.com/hordeurl/test.php?foo=1&bar=2&baz=3&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2&baz=3
+http://example.com/hordeurl/test.php?foo=1&bar=2&baz=3&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2&baz=3
+http://example.com/hordeurl/test.php?foo=1&bar=2&baz=3&PHPSESSID=
+http://example.com/hordeurl/test.php?foo=1&bar=2&baz=3
+http://example.com/hordeurl/test.php?foo=1&bar=2&baz=3&PHPSESSID=
diff --git a/framework/Release/lib/Horde/Release.php b/framework/Release/lib/Horde/Release.php
new file mode 100644 (file)
index 0000000..f93b298
--- /dev/null
@@ -0,0 +1,1067 @@
+<?php
+/**
+ * Class to make an "official" Horde or application release.
+ *
+ * Copyright 1999 Mike Hardy
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Hardy
+ * @author  Jan Schneider <jan@horde.org>
+ * @package Core
+ */
+class Horde_Release
+{
+    /**
+     * Default options.
+     *
+     * @var array
+     */
+    protected $_options = array(
+        'test' => false,
+        'nocommit' => false,
+        'noftp' => false,
+        'noannounce' => false,
+        'nofreshmeat' => false,
+        'nowhups' => false,
+    );
+
+    /**
+     * Version number of release.
+     *
+     * @var string
+     */
+    protected $_sourceVersionString;
+
+    /**
+     * Version number of previous release.
+     *
+     * @var string
+     */
+    protected $_oldSourceVersionString;
+
+    /**
+     * Version number of next release.
+     *
+     * @var string
+     */
+    protected $_newSourceVersionString;
+
+    /**
+     * Version number of next release for docs/CHANGES.
+     *
+     * @var string
+     */
+    protected $_newSourceVersionStringPlain;
+
+    /**
+     * Major version number of Horde compatible to this release.
+     *
+     * @var string
+     */
+    protected $_hordeVersionString;
+
+    /**
+     * Major version number of Horde compatible to the previous release.
+     *
+     * @var string
+     */
+    protected $_oldHordeVersionString;
+
+    /**
+     * CVS tag of release.
+     *
+     * @var string
+     */
+    protected $_tagVersionString;
+
+    /**
+     * CVS tag of previous release.
+     *
+     * @var string
+     */
+    protected $_oldTagVersionString;
+
+    /**
+     * Revision number of CHANGES file.
+     *
+     * @var string
+     */
+    protected $_changelogVersion;
+
+    /**
+     * Revision number of previous CHANGES file.
+     *
+     * @var string
+     */
+    protected $_oldChangelogVersion;
+
+    /**
+     * Version string to use in Whups
+     *
+     * @var string
+     */
+    protected $_ticketVersion;
+
+    /**
+     * Version description to use in Whups
+     *
+     * @var string
+     */
+    protected $_ticketVersionDesc = '';
+
+    /**
+     * Directory name of unpacked tarball.
+     *
+     * @var string
+     */
+    protected $_directoryName;
+
+    /**
+     * Directory name of unpacked previous tarball.
+     *
+     * @var string
+     */
+    protected $_oldDirectoryName;
+
+    /**
+     * Filename of the tarball.
+     *
+     * @var string
+     */
+    protected $_tarballName;
+
+    /**
+     * MD5 sum of the tarball.
+     *
+     * @var string
+     */
+    protected $_tarballMD5;
+
+    /**
+     * Whether or not to create a patch file.
+     *
+     * @var boolean
+     */
+    protected $_makeDiff = false;
+
+    /**
+     * The list of binary diffs.
+     *
+     * @var array
+     */
+    protected $_binaryDiffs = array();
+
+    /**
+     * Whether or not we have an old version to compare against.
+     *
+     * @var boolean
+     */
+    protected $_oldVersion = false;
+
+    /**
+     * Filename of the gzip'ed patch file (without .gz extension).
+     *
+     * @var string
+     */
+    protected $_patchName;
+
+    /**
+     * MD5 sum of the patch file.
+     *
+     * @var string
+     */
+    protected $_patchMD5;
+
+    /**
+     * Whether or not this is a final release version.
+     *
+     * @var boolean
+     */
+    protected $_latest = true;
+
+    /**
+     * Load the configuration
+     */
+    public function __construct($options = array())
+    {
+        $this->_options = array_merge($this->_options, $options);
+        $cvsroot = getenv('CVSROOT');
+        if (empty($cvsroot)) {
+            putenv('CVSROOT=:ext:' . $this->_options['horde']['user'] . '@cvs.horde.org:/repository');
+        }
+        print 'CVSROOT ' . getenv('CVSROOT') . "\n";
+        if (!empty($this->_options['cvs']['cvs_rsh'])) {
+            putenv('CVS_RSH=' . $this->_options['cvs']['cvs_rsh']);
+        }
+        print 'CVS_RSH ' . getenv('CVS_RSH') . "\n";
+    }
+
+    /**
+     * Delete the directory given as an argument
+     */
+    public function deleteDirectory($directory)
+    {
+        print "Deleting directory $directory\n";
+        system("sudo rm -rf $directory");
+    }
+
+    /**
+     * tar and gzip the directory given as an argument
+     */
+    public function makeTarball()
+    {
+        print "Setting owner/group to 0/0\n";
+        system("sudo chown -R 0:0 $this->_directoryName");
+
+        print "Making tarball\n";
+        $this->_tarballName = $this->_directoryName . '.tar.gz';
+        if (file_exists($this->_tarballName)) {
+            unlink($this->_tarballName);
+        }
+        system("tar -zcf $this->_tarballName $this->_directoryName");
+        exec($this->_options['md5'] . ' ' . $this->_tarballName, $this->_tarballMD5);
+    }
+
+    /**
+     * Label all of the source here with the new label given as an argument
+     */
+    public function tagSource($directory = null, $version = null)
+    {
+        if (empty($directory)) {
+            $directory = $this->_directoryName;
+        }
+        if (empty($version)) {
+            $version = $this->_tagVersionString;
+        }
+        if (!$this->_options['nocommit']) {
+            print "Tagging source in $directory with tag $version\n";
+            system("cd $directory;cvs tag -F $version > /dev/null 2>&1");
+        } else {
+            print "NOT tagging source in $directory (would have been tag $version)\n";
+        }
+    }
+
+    /**
+     * Make a diff of the two directories given as arguments
+     */
+    public function diff()
+    {
+        $this->_patchName = 'patch-' . $this->_oldDirectoryName . str_replace($this->_options['module'], '', $this->_directoryName);
+        print "Making diff between $this->_oldDirectoryName and $this->_directoryName\n";
+        system("diff -uNr $this->_oldDirectoryName $this->_directoryName > $this->_patchName");
+
+        // Search for binary diffs
+        $this->_binaryDiffs = array();
+        $handle = fopen($this->_patchName, 'r');
+        if ($handle) {
+            while (!feof($handle)) {
+                // GNU diff reports binary diffs as the following:
+                // Binary files ./locale/de_DE/LC_MESSAGES/imp.mo and ../../horde/imp/locale/de_DE/LC_MESSAGES/imp.mo differ
+                if (preg_match("/^Binary files (.+) and (.+) differ$/i", rtrim(fgets($handle)), $matches)) {
+                    // [1] = oldname, [2] = newname
+                    $this->_binaryDiffs[] = ltrim(str_replace($this->_oldDirectoryName . '/', '', $matches[1]));
+                }
+            }
+            fclose($handle);
+        }
+        system("gzip -9f $this->_patchName");
+        exec($this->_options['md5'] . ' ' . $this->_patchName . '.gz', $this->_patchMD5);
+    }
+
+    /**
+     * Change the version file for the module in the directory specified to
+     * the version specified
+     */
+    public function updateVersionFile($directory, $version_string)
+    {
+        $module = $this->_options['module'];
+        $all_caps_module = strtoupper($module);
+        print "Updating version file for $module\n";
+
+        // construct the filenames
+        $filename_only = 'version.php';
+        $filename = $directory . '/lib/' . $filename_only;
+        $newfilename = $filename . '.new';
+
+        $oldfp = fopen($filename, 'r');
+        $newfp = fopen($newfilename, 'w');
+        while ($line = fgets($oldfp)) {
+            if (strstr($line, 'VERSION')) {
+                fwrite($newfp, "<?php define('{$all_caps_module}_VERSION', '$version_string') ?>\n");
+            } else {
+                fwrite($newfp, $line);
+            }
+        }
+        fclose($oldfp);
+        fclose($newfp);
+
+        system("mv -f $newfilename $filename");
+        if (!$this->_options['nocommit']) {
+            system("cd $directory/lib/; cvs commit -f -m \"Tarball script: building new $module release - $version_string\" $filename_only > /dev/null 2>&1");
+        }
+    }
+
+    /**
+     * Update the CHANGES file with the new version number
+     */
+    public function updateSentinel()
+    {
+        $module = $this->_options['module'];
+        $all_caps_module = strtoupper($module);
+        print "Updating CHANGES file for $module\n";
+
+        // construct the filenames
+        $filename_only = 'CHANGES';
+        $filename = $this->_directoryName . '/docs/' . $filename_only;
+        $newfilename = $filename . '.new';
+
+        $version = 'v' . substr($this->_newSourceVersionStringPlain, 0, strpos($this->_newSourceVersionString, '-'));
+
+        $oldfp = fopen($filename, 'r');
+        $newfp = fopen($newfilename, 'w');
+        fwrite($newfp, str_repeat('-', strlen($version)) . "\n$version\n" .
+               str_repeat('-', strlen($version)) . "\n\n\n\n\n");
+        while ($line = fgets($oldfp)) {
+            fwrite($newfp, $line);
+        }
+        fclose($oldfp);
+        fclose($newfp);
+
+        system("mv -f $newfilename $filename");
+        if (!$this->_options['nocommit']) {
+            system("cd {$this->_directoryName}/docs/; cvs commit -f -m \"Tarball script: building new $module release - {$this->_newSourceVersionString}\" $filename_only > /dev/null 2>&1");
+        }
+    }
+
+    /**
+     * get and save the revision number of the CHANGES file
+     */
+    public function saveChangelog($old = false, $directory = null)
+    {
+        if (empty($directory)) {
+            if ($old) {
+                $directory = './' . $this->_oldDirectoryName . '/docs';
+            } else {
+                $directory = './' . $this->_directoryName . '/docs';
+            }
+        }
+        if (!$old) {
+            include "$directory/RELEASE_NOTES";
+            if (strlen(htmlspecialchars($this->notes['fm']['changes'])) > 600) {
+                print "WARNING: freshmeat release notes are longer than 600 characters!\n";
+            }
+        }
+        exec("cd $directory; cvs status CHANGES", $output);
+        foreach ($output as $line) {
+            if (preg_match('/Repository revision:\s+([\d.]+)/', $line, $matches)) {
+                if ($old) {
+                    $this->_oldChangelogVersion = $matches[1];
+                } else {
+                    $this->_changelogVersion = $matches[1];
+                }
+                break;
+            }
+        }
+    }
+
+    /**
+     * work through the source directory given, cleaning things up by removing
+     * directories and files we don't want in the tarball
+     */
+    public function cleanDirectories($directory)
+    {
+        print "Cleaning source tree\n";
+        $directories = explode("\n", `find $directory -type d \\( -name CVS -o -name packaging -o -name framework \\) -print | sort -r`);
+        foreach ($directories as $dir) {
+            system("rm -rf $dir");
+        }
+        $cvsignores = explode("\n", `find $directory -name .cvsignore -print`);
+        foreach ($cvsignores as $file) {
+            if (!empty($file)) {
+                unlink($file);
+            }
+        }
+    }
+
+    /**
+     * Check out the tag we've been given to work with and move it to the
+     * directory name given
+     */
+    public function checkOutTag($mod_version, $directory, $module = null)
+    {
+        if (empty($module)) {
+            $module = $this->_options['module'];
+        }
+
+        if (@is_dir($module)) {
+            system("rm -rf $module");
+        }
+
+        // Use CVS to check the source out
+        if ($mod_version == 'HEAD') {
+            print "Checking out HEAD for $module\n";
+            $cmd = "cvs -q co -P $module > /dev/null";
+            system($cmd, $status);
+        } else {
+            print "Checking out tag $mod_version for $module\n";
+            $cmd = "cvs -q co -P -r$mod_version $module > /dev/null";
+            system($cmd, $status);
+        }
+        if ($status) {
+            die("\nThere was an error running the command\n$cmd\n");
+        }
+
+        // Move the source into the directory specified
+        print "Moving $module to $directory\n";
+        if (@is_dir($directory)) {
+            system("rm -rf $directory");
+        }
+        system("mv -f $module $directory");
+    }
+
+    /**
+     * Checkout and install framework
+     */
+    public function checkOutFramework($mod_version, $directory)
+    {
+        if ($this->_options['module'] == 'horde' &&
+            ($this->_options['branch'] == 'HEAD' ||
+             strstr($this->_options['branch'], 'FRAMEWORK'))) {
+            if ($this->_options['branch'] == 'HEAD') {
+                print "Checking out HEAD for framework\n";
+            } else {
+                print "Checking out tag $mod_version for framework\n";
+            }
+            $cmd = "cd $directory; cvs co -P -r$mod_version framework > /dev/null 2>&1; cd ..";
+            system($cmd, $status);
+            if ($status) {
+                die("\nThere was an error running the command\n$cmd\n");
+            }
+            print "Installing framework packages\n";
+            if (file_exists("./$directory/scripts/create-symlinks.php")) {
+                system("php ./$directory/scripts/create-symlinks.php --copy --src=./$directory/framework --dest=./$directory/lib");
+            } else {
+                system("horde-fw-symlinks.php --copy --src ./$directory/framework --dest ./$directory/lib");
+            }
+
+            print "Setting include path\n";
+            $filename = $directory . '/lib/core.php';
+            $newfilename = $filename . '.new';
+            $oldfp = fopen($filename, 'r');
+            $newfp = fopen($newfilename, 'w');
+            while ($line = fgets($oldfp)) {
+                fwrite($newfp, str_replace('// ini_set(\'include_path\'', 'ini_set(\'include_path\'', $line));
+            }
+            fclose($oldfp);
+            fclose($newfp);
+            system("mv -f $newfilename $filename");
+        }
+    }
+
+    /**
+     * Upload tarball to the FTP server
+     */
+    public function upload()
+    {
+        $module = $this->_options['module'];
+        $user = $this->_options['horde']['user'];
+        $identity = empty($this->_options['ssh']['identity']) ? '' : ' -i ' . $this->_options['ssh']['identity'];
+        $chmod = "chmod 664 /horde/ftp/pub/$module/$this->_tarballName;";
+        if ($this->_makeDiff) {
+            $chmod .= " chmod 664 /horde/ftp/pub/$module/patches/$this->_patchName.gz;";
+        }
+        if ($this->_latest &&
+            strpos($this->_options['branch'], 'RELENG') !== 0) {
+            $chmod .= " ln -sf $this->_tarballName /horde/ftp/pub/$module/$module-latest.tar.gz;";
+        }
+
+        if (!$this->_options['noftp']) {
+            print "Uploading $this->_tarballName to $user@ftp.horde.org:/horde/ftp/pub/$module/\n";
+            system("scp -P 35$identity $this->_tarballName $user@ftp.horde.org:/horde/ftp/pub/$module/");
+            if ($this->_makeDiff) {
+                print "Uploading $this->_patchName.gz to $user@ftp.horde.org:/horde/ftp/pub/$module/patches/\n";
+                system("scp -P 35$identity $this->_patchName.gz $user@ftp.horde.org:/horde/ftp/pub/$module/patches/");
+            }
+            print "Executing $chmod\n";
+            system("ssh -p 35 -l $user$identity ftp.horde.org '$chmod'");
+        } else {
+            print "NOT uploading $this->_tarballName to ftp.horde.org:/horde/ftp/pub/$module/\n";
+            if ($this->_makeDiff) {
+                print "NOT uploading $this->_patchName.gz to $user@ftp.horde.org:/horde/ftp/pub/$module/patches/\n";
+            }
+            print "NOT executing $chmod\n";
+        }
+    }
+
+    /**
+     * check if freshmeat announcement was successful.
+     */
+    protected function _fmVerify($fm)
+    {
+        if (is_a($fm, 'PEAR_Error')) {
+            print $fm->getMessage() . "\n";
+            return false;
+        } elseif (!is_array($fm)) {
+            var_dump($fm);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * announce release to mailing lists and freshmeat.
+     */
+    public function announce($doc_dir = null)
+    {
+        $module = $this->_options['module'];
+        if (!isset($this->notes)) {
+            print "NOT announcing release, RELEASE_NOTES missing.\n";
+            return;
+        }
+        if (!empty($this->_options['noannounce']) ||
+            !empty($this->_options['nofreshmeat'])) {
+            print "NOT announcing release on freshmeat.net\n";
+        } else {
+            print "Announcing release on freshmeat.net\n";
+        }
+
+        if (empty($this->_options['nofreshmeat'])) {
+            $fm = Horde_RPC::request(
+                'xmlrpc',
+                'http://freshmeat.net/xmlrpc/',
+                'login',
+                array('username' => $this->_options['fm']['user'],
+                      'password' => $this->_options['fm']['password']));
+        } else {
+            $fm = array('SID' => null);
+        }
+        if (empty($doc_dir)) {
+            $doc_dir = $module . '/docs';
+        }
+
+        $url_changelog = $this->_oldVersion
+            ? "http://cvs.horde.org/diff.php/$doc_dir/CHANGES?r1={$this->_oldChangelogVersion}&r2={$this->_changelogVersion}&ty=h"
+            : '';
+
+        if (is_a($fm, 'PEAR_Error')) {
+            print $fm->getMessage() . "\n";
+        } else {
+            $announcement = array('SID' => $fm['SID'],
+                                  'project_name' => $this->notes['fm']['project'],
+                                  'branch_name' => $this->notes['fm']['branch'],
+                                  'version' => $this->_sourceVersionString,
+                                  'changes' => htmlspecialchars($this->notes['fm']['changes']),
+                                  'release_focus' => (int)$this->notes['fm']['focus'],
+                                  'url_changelog' => $url_changelog,
+                                  'url_tgz' => "ftp://ftp.horde.org/pub/$module/{$this->_tarballName}");
+            if ($this->_fmVerify($fm)) {
+                if (!empty($this->_options['noannounce']) ||
+                    !empty($this->_options['nofreshmeat'])) {
+                    print "Announcement data:\n";
+                    print_r($announcement);
+                } else {
+                    $fm = Horde_RPC::request(
+                        'xmlrpc',
+                        'http://freshmeat.net/xmlrpc/',
+                        'publish_release',
+                        $announcement);
+                    $this->_fmVerify($fm);
+                }
+            }
+        }
+
+        $ml = (!empty($this->notes['list'])) ? $this->notes['list'] : $module;
+        if (substr($ml, 0, 6) == 'horde-') {
+            $ml = 'horde';
+        }
+
+        $to = "announce@lists.horde.org, vendor@lists.horde.org, $ml@lists.horde.org";
+        if (!$this->_latest) {
+            $to .= ', i18n@lists.horde.org';
+        }
+
+        if (!empty($this->_options['noannounce'])) {
+            print "NOT announcing release on $to\n";
+        } else {
+            print "Announcing release to $to\n";
+        }
+
+        // Building headers
+        $subject = $this->notes['name'] . ' ' . $this->_sourceVersionString;
+        if ($this->_latest) {
+            $subject .= ' (final)';
+        }
+        if ($this->notes['fm']['focus'] == 9) {
+            $subject = '[SECURITY] ' . $subject;
+        }
+        $headers = array('From' => $this->_options['ml']['from'],
+                         'To' => $to,
+                         'Subject' => $subject);
+
+        // Building message text
+        $body = $this->notes['ml']['changes'];
+        if ($this->_oldVersion) {
+            $body .= "\n\n" .
+                sprintf('The full list of changes (from version %s) can be viewed here:', $this->_oldSourceVersionString) .
+                "\n\n" .
+                $url_changelog;
+        }
+        $body .= "\n\n" .
+            sprintf('The %s %s distribution is available from the following locations:', $this->notes['name'], $this->_sourceVersionString) .
+            "\n\n" .
+            sprintf('    ftp://ftp.horde.org/pub/%s/%s', $module, $this->_tarballName) . "\n" .
+            sprintf('    http://ftp.horde.org/pub/%s/%s', $module, $this->_tarballName);
+        if ($this->_makeDiff) {
+            $body .= "\n\n" .
+                sprintf('Patches against version %s are available at:', $this->_oldSourceVersionString) .
+                "\n\n" .
+                sprintf('    ftp://ftp.horde.org/pub/%s/patches/%s.gz', $module, $this->_patchName) . "\n" .
+                sprintf('    http://ftp.horde.org/pub/%s/patches/%s.gz', $module, $this->_patchName);
+
+            if (!empty($this->_binaryDiffs)) {
+                $body .= "\n\n" .
+                    'NOTE: Patches do not contain differences between files containing binary data.' . "\n" .
+                    'These files will need to be updated via the distribution files:' . "\n\n    " .
+                    implode("\n    ", $this->_binaryDiffs);
+            }
+        }
+        $body .= "\n\n" .
+            'Or, for quicker access, download from your nearest mirror:' .
+            "\n\n" .
+            '    http://www.horde.org/mirrors.php' .
+            "\n\n" .
+            'MD5 sums for the packages are as follows:' .
+            "\n\n" .
+            '    ' . $this->_tarballMD5[0] . "\n" .
+            '    ' . $this->_patchMD5[0] .
+            "\n\n" .
+            'Have fun!' .
+            "\n\n" .
+            'The Horde Team.';
+
+        if (!empty($this->_options['noannounce'])) {
+            print "Message headers:\n";
+            print_r($headers);
+            print "Message body:\n$body\n";
+            return;
+        }
+
+        // Building and sending message
+        $mail = new Horde_Mime_Mail();
+        $mail->setBody($body, 'iso-8859-1', false);
+        $mail->addHeaders($headers);
+        $result = $mail->send($this->_options['mailer']['type'], $this->_options['mailer']['params']);
+        if (is_a($result, 'PEAR_Error')) {
+            print $result->getMessage() . "\n";
+        }
+    }
+
+    /**
+     * Do testing (development only)
+     */
+    public function test()
+    {
+        if (!$this->_options['test']) {
+            return;
+        }
+
+        print "options['version']={$this->_options['version']}\n";
+        print "options['oldversion']={$this->_options['oldversion']}\n";
+        print "options['module']={$this->_options['module']}\n";
+        print "options['branch']={$this->_options['branch']}\n";
+
+        $this->setVersionStrings();
+
+        print "hordeVersionString={$this->_hordeVersionString}\n";
+        print "oldHordeVersionString={$this->_oldHordeVersionString}\n";
+        print "makeDiff={$this->_makeDiff}\n";
+        print "oldVersion={$this->_oldVersion}\n";
+        print "directoryName={$this->_directoryName}\n";
+        if ($this->_oldVersion) {
+            print "oldDirectoryName={$this->_oldDirectoryName}\n";
+        }
+        print "tagVersionString={$this->_tagVersionString}\n";
+        if ($this->_oldVersion) {
+            print "oldTagVersionString={$this->_oldTagVersionString}\n";
+        }
+        print "sourceVersionString={$this->_sourceVersionString}\n";
+        if ($this->_oldVersion) {
+            print "oldSourceVersionString={$this->_oldSourceVersionString}\n";
+        }
+        print "newSourceVersionString={$this->_newSourceVersionString}\n";
+        print "newSourceVersionStringPlain={$this->_newSourceVersionStringPlain}\n";
+        print "ticketVersion={$this->_ticketVersion}\n";
+        print "ticketVersionDesc=MODULE{$this->_ticketVersionDesc}\n";
+        if ($this->_latest) {
+            print "This is a production release\n";
+        }
+        exit(0);
+    }
+
+    /**
+     * Add the new version to bugs.horde.org
+     */
+    public function addWhupsVersion()
+    {
+        if (!isset($this->notes)) {
+            print "\nNOT updating bugs.horde.org, RELEASE_NOTES missing.\n";
+            return;
+        }
+        $this->_ticketVersionDesc = $this->notes['name'] . $this->_ticketVersionDesc;
+
+        $params = array('url' => 'https://dev.horde.org/horde/rpc.php',
+                        'user' => $this->_options['horde']['user'],
+                        'pass' => $this->_options['horde']['pass']);
+        $whups = new Horde_Release_Whups($params);
+
+        if (!$this->_options['nowhups']) {
+            print "Adding new versions to bugs.horde.org: ";
+            /* Set the new version in the queue */
+            try {
+                $whups->addNewVersion($this->_options['module'], $this->_ticketVersion, $this->_ticketVersionDesc);
+                print "OK\n";
+            } catch (Horde_Exception $e) {
+                print "Failed:\n";
+                print $e->getMessage() . "\n";
+            }
+        } else {
+            print "NOT updating bugs.horde.org:\n";
+            print "New ticket version WOULD have been {$this->_ticketVersion}\n";
+            print "New ticket version description WOULD have been {$this->_ticketVersionDesc}\n";
+
+            /* Perform some sanity checks on bugs.horde.org */
+            try {
+                $queue = $whups->getQueueId($this->_options['module']);
+
+                if ($queue === false) {
+                    print "Was UNABLE to locate the queue id for {$this->_options['module']}\n";
+                } else {
+                    print "The queue id on bugs.horde.org is $queue \n";
+                }
+            } catch (Horde_Exception $e) {
+                print "Will be UNABLE to update bugs.horde.org:\n";
+                print $e->getMessage() . "\n";
+            }
+        }
+    }
+
+    /**
+     * Set the version strings to use given the arguments
+     */
+    public function setVersionStrings()
+    {
+        $ver = explode('.', $this->_options['version']);
+        if (preg_match('/(\d+)\-(.*)/', $ver[count($ver) - 1], $matches)) {
+            $ver[count($ver) - 1] = $matches[1];
+            $plus = $matches[2];
+        }
+        if (preg_match('/(H\d)-(\d+)/', $ver[0], $matches)) {
+            $ver[0] = $matches[2];
+            $this->_hordeVersionString = $matches[1];
+        }
+        if (count($ver) > 2 && $ver[count($ver) - 1] == '0') {
+            die("version {$this->_options['version']} should not have the trailing 3rd-level .0\n");
+        }
+
+        // check if --oldversion is empty or 0
+        if (!empty($this->_options['oldversion'])) {
+            $this->_oldVersion = true;
+        }
+        $oldver = explode('.', $this->_options['oldversion']);
+        if (preg_match('/(\d+)\-(.*)/', $oldver[count($oldver) - 1], $matches)) {
+            $oldver[count($oldver) - 1] = $matches[1];
+            $oldplus = $matches[2];
+        }
+        if (preg_match('/(H\d)-(\d+)/', $oldver[0], $matches)) {
+            $oldver[0] = $matches[2];
+            $this->_oldHordeVersionString = $matches[1];
+        }
+
+        // set the string to use as the tag name in CVS
+        $this->_tagVersionString = strtoupper($this->_options['module'] . '_' . preg_replace('/\W/', '_', implode('_', $ver)));
+        if (isset($plus)) {
+            $this->_tagVersionString .= '_' . $plus;
+        }
+
+        // create patches only if not a major version change
+        if ($this->_options['oldversion'] && $ver[0] == $oldver[0]) {
+            $this->_makeDiff = true;
+        }
+
+        // is this really a production release?
+        if (isset($plus) && !preg_match('/^pl\d/', $plus)) {
+            $this->_latest = false;
+        }
+
+        // set the string to insert into the source version file
+        $this->_sourceVersionString = implode('.', $ver);
+        if (isset($plus)) {
+            $this->_sourceVersionString .= '-' . $plus;
+        }
+
+        // set the string to be used for the directory to package from
+        $this->_directoryName = $this->_options['module'] . '-';
+        if (!empty($this->_hordeVersionString)) {
+            $this->_directoryName .= $this->_hordeVersionString . '-';
+        }
+        $this->_directoryName = strtolower($this->_directoryName . $this->_sourceVersionString);
+
+        if (!empty($this->_hordeVersionString)) {
+            $this->_sourceVersionString = $this->_hordeVersionString . ' (' . $this->_sourceVersionString . ')';
+        }
+
+        if ($this->_oldVersion) {
+            $this->_oldSourceVersionString = implode('.', $oldver);
+            if (isset($oldplus)) {
+                $this->_oldSourceVersionString .= '-' . $oldplus;
+            }
+            $this->_oldTagVersionString = strtoupper($this->_options['module'] . '_' . implode('_', $oldver));
+            if (isset($oldplus)) {
+                $this->_oldTagVersionString .= '_' . $oldplus;
+            }
+            $this->_oldDirectoryName = strtolower($this->_options['module'] . '-' . $this->_oldHordeVersionString . $this->_oldSourceVersionString);
+            $this->_oldDirectoryName = $this->_options['module'] . '-';
+            if (!empty($this->_oldHordeVersionString)) {
+                $this->_oldDirectoryName .= $this->_oldHordeVersionString . '-';
+            }
+            $this->_oldDirectoryName = strtolower($this->_oldDirectoryName . $this->_oldSourceVersionString);
+
+            if (!empty($this->_oldHordeVersionString)) {
+                $this->_oldSourceVersionString = $this->_oldHordeVersionString . ' (' . $this->_oldSourceVersionString . ')';
+            }
+        }
+
+        // Set string to use for updating ticketing system.
+        $this->_ticketVersion = implode('.', $ver);
+        if (!empty($plus)) {
+            $this->_ticketVersion .= '-' . $plus;
+        }
+
+        if (!empty($this->_hordeVersionString)) {
+            $this->_ticketVersionDesc .= ' ' . $this->_hordeVersionString;
+        }
+
+        // Account for the 'special' case of the horde module.
+        if ($this->_options['module'] == 'horde') {
+            $this->_ticketVersionDesc .= ' ' . implode('.', $ver);
+        } else {
+            $this->_ticketVersionDesc .= ' ' . '(' . implode('.', $ver) . ')';
+        }
+
+        // See if we have a 'Final', 'Alpha', or 'RC' to add.
+        if ($this->_latest) {
+            $this->_ticketVersionDesc .= ' Final';
+        } elseif (!empty($plus) &&
+                  preg_match('/^RC(\d+)/', $plus, $matches)) {
+            $this->_ticketVersionDesc .= ' Release Candidate ' . $matches[1];
+
+        } elseif (!empty($plus) && strtolower($plus) == 'alpha') {
+            $this->_ticketVersionDesc .= ' Alpha';
+        }
+
+        // set the name of the string to put into the source version file when
+        // done
+        if (!isset($plus)) {
+            while (count($ver) < 3) {
+                $ver[] = '0';
+            }
+            $ver[count($ver) - 1] += 1;
+        }
+        $this->_newSourceVersionString = implode('.', $ver) . '-cvs';
+        $this->_newSourceVersionStringPlain = $this->_newSourceVersionString;
+
+        if (!empty($this->_hordeVersionString)) {
+            $this->_newSourceVersionString = $this->_hordeVersionString .
+                ' (' . $this->_newSourceVersionString . ')';
+        }
+
+    }
+
+    /**
+     * Get all of the command-line arguments from the user
+     */
+    public function getArguments()
+    {
+        global $argv;
+
+        // Parse the command-line arguments
+        array_shift($argv);
+        foreach ($argv as $arg) {
+            // Check to see if they gave us a module
+            if (preg_match('/--module=(.*)/', $arg, $matches)) {
+                $this->_options['module'] = $matches[1];
+
+            // Check to see if they tell us the version of the tarball to make
+            } elseif (preg_match('/--version=(.*)/', $arg, $matches)) {
+                $this->_options['version']= $matches[1];
+
+            // Check to see if they tell us the last release version
+            } elseif (preg_match('/--oldversion=(.*)/', $arg, $matches)) {
+                $this->_options['oldversion']= $matches[1];
+
+            // Check to see if they tell us which branch to work with
+            } elseif (preg_match('/--branch=(.*)/', $arg, $matches)) {
+                $this->_options['branch']= $matches[1];
+
+            // Check to see if they tell us not to commit or tag
+            } elseif (strstr($arg, '--nocommit')) {
+                $this->_options['nocommit']= true;
+
+            // Check to see if they tell us not to upload
+            } elseif (strstr($arg, '--noftp')) {
+                $this->_options['noftp']= true;
+
+            // Check to see if they tell us not to announce
+            } elseif (strstr($arg, '--noannounce')) {
+                $this->_options['noannounce']= true;
+
+            // Check to see if they tell us not to announce
+            } elseif (strstr($arg, '--nofreshmeat')) {
+                $this->_options['nofreshmeat']= true;
+
+            // Check to see if they tell us not to add new ticket versions
+            } elseif (strstr($arg, '--noticketversion')) {
+                $this->_options['nowhups'] = true;
+
+            // Check to see if they tell us to do a dry run
+            } elseif (strstr($arg, '--dryrun')) {
+                $this->_options['nocommit'] = true;
+                $this->_options['noftp'] = true;
+                $this->_options['noannounce'] = true;
+                $this->_options['nowhups'] = true;
+                $this->_options['nofreshmeat']= true;
+
+            // Check to see if they tell us to test (for development only)
+            } elseif (strstr($arg, '--test')) {
+                $this->_options['test']= true;
+                // safety first
+                $this->_options['nocommit'] = true;
+                $this->_options['noftp'] = true;
+                $this->_options['noannounce'] = true;
+                $this->_options['nowhups'] = true;
+                $this->_options['nofreshmeat']= true;
+
+            // Check for help usage.
+            } elseif (strstr($arg, '--help')) {
+                $this->print_usage();
+                exit;
+
+            // We have no idea what this is
+            } else {
+                $this->print_usage('You have used unknown arguments: ' . $arg);
+                exit;
+            }
+        }
+    }
+
+    /**
+     * Check the command-line arguments and set some internal defaults
+     */
+    public function checkArguments()
+    {
+        // Make sure that we have a module defined
+        if (!isset($this->_options['module'])) {
+            $this->print_usage('You must define which module to package.');
+            exit;
+        }
+
+        // Let's make sure that there are valid version strings in here...
+        if (!isset($this->_options['version'])) {
+            $this->print_usage('You must define which version to package.');
+            exit;
+        }
+        if (!preg_match('/\d+\.\d+.*/', $this->_options['version'])) {
+            $this->print_usage('Incorrect version string.');
+            exit;
+        }
+        if (!isset($this->_options['oldversion'])) {
+            $this->print_usage('You must define last release\'s version.');
+            exit;
+        }
+        if (!preg_match('/\d+(\.\d+.*)?/', $this->_options['oldversion'])) {
+            $this->print_usage('Incorrect old version string.');
+            exit;
+        }
+
+        // Make sure we have a horde.org user
+        if (empty($this->_options['horde']['user'])) {
+            $this->print_usage('You must define a horde.org user.');
+            exit;
+        }
+
+        // If there is no branch defined, we're using the tip revisions.
+        // These releases are always developmental, and should use the HEAD "branch" name.
+        if (!isset($this->_options['branch'])) {
+            $this->_options['branch'] = 'HEAD';
+        }
+    }
+
+    /**
+     * Check the command-line arguments and set some internal defaults
+     */
+    public function checkSetSystem()
+    {
+        // Set umask
+        umask(022);
+    }
+
+    /**
+     * Show people how to use the damned thing
+     */
+    public function print_usage($message = null)
+    {
+        if (!is_null($message)) {
+            print "\n***  ERROR: $message  ***\n";
+        }
+
+        print <<<USAGE
+
+make-release.php: Horde release generator.
+
+   This script takes as arguments the module to make a release of, the
+   version of the release, and the branch:
+
+      horde-make-release.php --module=<name>
+                         --version=[Hn-]xx.yy[.zz[-<string>]]
+                         --oldversion=[Hn-]xx[.yy[.zz[-<string>]]]
+                         [--branch=<branchname>] [--nocommit] [--noftp]
+                         [--noannounce] [--nofreshmeat] [--noticketversion]
+                         [--test] [--dryrun] [--help]
+
+   If you omit the branch, it will implicitly work with the HEAD branch.
+   If you release a new major version use the --oldversion=0 option.
+   Use the --nocommit option to do a test build (without touching the CVS
+   repository).
+   Use the --noftp option to not upload any files on the FTP server.
+   Use the --noannounce option to not send any release announcements.
+   Use the --nofreshmeat option to not send any freshmeat announcements.
+   Use the --noticketversion option to not update the version information on
+   bugs.horde.org.
+   The --dryrun option is an alias for:
+     --nocommit --noftp --noannounce --nofreshmeat --noticketversion.
+   The --test option is for debugging purposes only.
+
+   EXAMPLES:
+
+   To make a new development release of Horde:
+      horde-make-release.php --module=horde --version=2.1-dev --oldversion=2.0
+
+   To make a new stable release of Turba:
+      horde-make-release.php --module=turba --version=H3-2.0.2 \
+        --oldversion=H3-2.0.1 --branch=FRAMEWORK_3
+
+   To make a new stable release of IMP 3:
+      horde-make-release.php --module=imp --version=3.0 --oldversion=2.3.7 \
+        --branch=RELENG_3
+
+   To make a brand new Alpha/Beta/RC release of Luxor:
+      horde-make-release.php --module=luxor --version=H3-1.0-ALPHA \
+        --oldversion=0
+
+USAGE;
+    }
+
+}
diff --git a/framework/Release/lib/Horde/Release/Whups.php b/framework/Release/lib/Horde/Release/Whups.php
new file mode 100644 (file)
index 0000000..27ec2fa
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+/**
+ * Class for interfacing with the tickets API.
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Core
+ */
+class Horde_Release_Whups
+{
+    /**
+     * Instance of XML_RPC_Client object
+     *
+     * @var XML_RPC_CLient
+     */
+    protected $_client;
+
+    /**
+     * Local copy of config params.
+     *
+     * @var array
+     */
+    protected $_params;
+
+    /**
+     * Constructor.
+     *
+     * @param array $params  TODO
+     */
+    public function __construct($params)
+    {
+        $this->_params = $params;
+    }
+
+    /**
+     * Add a new version to the current modules queue.
+     *
+     * @param string $module   The name of the module.
+     * @param string $version  The version string.
+     * @param string $desc     Descriptive text for this version.
+     *
+     * @throws Horde_Exception
+     */
+    public function addNewVersion($module, $version, $desc = '')
+    {
+        if ($module == 'horde') {
+            $module = 'horde base';
+        }
+
+        $id = $this->getQueueId($module);
+        if ($id === false) {
+            throw new Horde_Exception('Unable to locate requested queue');
+        }
+
+        $method = 'tickets.addVersion';
+        $params = array($id, $version, $desc);
+        $options = array('user' => $this->_params['user'],
+                         'pass' => $this->_params['pass']);
+
+        $res = Horde_RPC::request('jsonrpc', $this->_params['url'], $method, $params, $options);
+        if ($res instanceof PEAR_Error) {
+            throw new Horde_Exception($res);
+        }
+    }
+
+    /**
+     * Look up the queue id for the requested module name.
+     *
+     * @param string $module  TODO
+     *
+     * @return boolean  TODO
+     */
+    function getQueueId($module)
+    {
+        $queues = $this->_listQueues();
+
+        foreach ($queues as $id => $queue) {
+            if (strtolower($queue) == $module) {
+                return $id;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Perform a listQueue api call.
+     *
+     * @return string  TODO
+     * @throws Horde_Exception
+     */
+    protected function _listQueues()
+    {
+        $method = 'tickets.listQueues';
+        $result = Horde_RPC::request('jsonrpc', $this->_params['url'], $method,
+                                     null, array('user' => $this->_params['user'],
+                                                 'pass' => $this->_params['pass']));
+        if ($result instanceof PEAR_Error) {
+            throw new Horde_Exception($result);
+        }
+
+        return $result->result;
+    }
+
+}
diff --git a/framework/Release/package.xml b/framework/Release/package.xml
new file mode 100644 (file)
index 0000000..8e6b36d
--- /dev/null
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Release</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Release generation library</summary>
+ <description>These package provides the tools necessary to create the Horde distribution packages.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <developer>
+  <name>Michael Slusarz</name>
+  <user>slusarz</user>
+  <email>slusarz@horde.org</email>
+  <active>yes</active>
+ </developer>
+ <date>2009-07-08</date>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Initial release.
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="lib">
+    <dir name="Horde">
+     <file name="Release.php" role="php" />
+     <dir name="Release">
+      <file name="Whups.php" role="php" />
+     </dir> <!-- /lib/Horde/Release -->
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.5.4</min>
+   </pearinstaller>
+   <package>
+    <name>Core</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Mime</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Rpc</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+  <optional/>
+ </dependencies>
+ <phprelease>
+  <install>
+   <file name="lib/Horde/Release.php" as="Horde/Release.php" />
+   <file name="lib/Horde/Release/Whups.php" as="Horde/Release/Whups.php" />
+  </install>
+ </phprelease>
+ <changelog/>
+</package>