From 4bb52c73a8f50b963685d519bb531990fa89151b Mon Sep 17 00:00:00 2001 From: Michael M Slusarz Date: Wed, 8 Jul 2009 21:00:36 -0600 Subject: [PATCH] Horde core framework package import into Git from CVS HEAD 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. --- framework/Core/lib/Horde/Horde.php | 1939 ++++++++++++++++++++ framework/Core/lib/Horde/Horde/Config.php | 1548 ++++++++++++++++ framework/Core/lib/Horde/Horde/ErrorHandler.php | 202 ++ framework/Core/lib/Horde/Horde/Exception.php | 66 + framework/Core/lib/Horde/Horde/Help.php | 530 ++++++ framework/Core/lib/Horde/Horde/Menu.php | 313 ++++ framework/Core/lib/Horde/Horde/Registry.php | 1137 ++++++++++++ framework/Core/lib/Horde/Horde/Registry/Caller.php | 41 + framework/Core/lib/Horde/Horde/Release.php | 1067 +++++++++++ framework/Core/lib/Horde/Horde/Release/Whups.php | 109 ++ framework/Core/lib/Horde/Horde/Script/Files.php | 270 +++ framework/Core/package.xml | 160 ++ framework/Core/test/Horde/Framework/url.phpt | 290 +++ framework/Release/lib/Horde/Release.php | 1067 +++++++++++ framework/Release/lib/Horde/Release/Whups.php | 109 ++ framework/Release/package.xml | 83 + 16 files changed, 8931 insertions(+) create mode 100644 framework/Core/lib/Horde/Horde.php create mode 100644 framework/Core/lib/Horde/Horde/Config.php create mode 100644 framework/Core/lib/Horde/Horde/ErrorHandler.php create mode 100644 framework/Core/lib/Horde/Horde/Exception.php create mode 100644 framework/Core/lib/Horde/Horde/Help.php create mode 100644 framework/Core/lib/Horde/Horde/Menu.php create mode 100644 framework/Core/lib/Horde/Horde/Registry.php create mode 100644 framework/Core/lib/Horde/Horde/Registry/Caller.php create mode 100644 framework/Core/lib/Horde/Horde/Release.php create mode 100644 framework/Core/lib/Horde/Horde/Release/Whups.php create mode 100644 framework/Core/lib/Horde/Horde/Script/Files.php create mode 100644 framework/Core/package.xml create mode 100644 framework/Core/test/Horde/Framework/url.phpt create mode 100644 framework/Release/lib/Horde/Release.php create mode 100644 framework/Release/lib/Horde/Release/Whups.php create mode 100644 framework/Release/package.xml diff --git a/framework/Core/lib/Horde/Horde.php b/framework/Core/lib/Horde/Horde.php new file mode 100644 index 000000000..ff1f3ac3c --- /dev/null +++ b/framework/Core/lib/Horde/Horde.php @@ -0,0 +1,1939 @@ + + * @author Jon Parise + * @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: + *
+     * PEAR_LOG_EMERG
+     * PEAR_LOG_ALERT
+     * PEAR_LOG_CRIT
+     * PEAR_LOG_ERR
+     * PEAR_LOG_WARNING
+     * PEAR_LOG_NOTICE
+     * PEAR_LOG_INFO
+     * PEAR_LOG_DEBUG
+     * 
+ */ + 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 = '

' . _("A fatal error has occurred") . '

'; + 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']) . '
' . + 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']) . '
' . + 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 .= '

' . htmlspecialchars($message) . '

'; + } elseif (is_object($error) && method_exists($error, 'getMessage')) { + $errortext .= '

' . htmlspecialchars($error->getMessage()) . '

'; + } elseif (is_string($error)) { + $errortext .= '

' . htmlspecialchars($error) . '

'; + } + + if ($admin) { + $errortext .= '

' . sprintf(_("[line %d of %s]"), $line, $file) . '

'; + if (is_object($error)) { + $errortext .= '

' . _("Details:") . '

'; + $errortext .= '

' . _("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.") . '

'; + if (extension_loaded('xdebug')) { + $errortext .= '
' . print_r($error, true); + } else { + $errortext .= '

' . htmlspecialchars(print_r($error, true)) . '

'; + } + } + } elseif ($log) { + $errortext .= '

' . _("Details have been logged for the administrator.") . '

'; + } + + // 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('
', '

', '

', '

', '

', '

', '

'), "\n", $errortext)); + } else { + echo <<< HTML + +Horde :: Fatal Error +$errortext + +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 "&". + * + * @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('/(=?.*?)&(.*?=)/', + '/(=?.*?)&(.*?=)/'), + '$1&$2', $url); + } elseif (preg_match('/=.*&.*=/', $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 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 &'s in the URL. */ + $url = preg_replace(array('/(=?.*?)&(.*?=)/', '/(=?.*?)&(.*?=)/'), '$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 + * tag. + * @param boolean $escape Whether to escape special characters in the + * title attribute. + * + * @return string The full tag. + */ + static public function link($url = '', $title = '', $class = '', + $target = '', $onclick = '', $title2 = '', + $accesskey = '', $attributes = array(), + $escape = true) + { + if (!empty($title2)) { + $title = $title2; + } + + $ret = ' $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 + * tag. + * + * @return string The full 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 = '<pre>' . preg_replace(array('/\n/', '/((?))/em', '/

/', '/
/'), array('', 'str_repeat(" ", strlen("$1"))', '<br /> <br />', '<br />'), nl2br(htmlspecialchars(htmlspecialchars($title, ENT_QUOTES, $charset), ENT_QUOTES, $charset))) . '</pre>'; + 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
Title 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) . ''; + } + + /** + * 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 = ' $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 = ' $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 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 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 .= '' . "\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 . '(' + . strtoupper($accessKey) . '' . ')'; + } else { + return str_replace('_' . $accessKey, '' . $accessKey . '', $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('', + $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 index 000000000..14b5ba253 --- /dev/null +++ b/framework/Core/lib/Horde/Horde/Config.php @@ -0,0 +1,1548 @@ + + * @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 = "_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 tag. + * Subnodes will be parsed and added to both the Horde defaults and the + * Custom configuration parts. + * + * @param string $ctx The context of the tag. + * @param DomNode $node The DomNode representation of the + * 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 tag. + * Subnodes will be parsed and added to both the Horde defaults and the + * Custom configuration parts. + * + * @param string $ctx The context of the tag. + * @param DomNode $node The DomNode representation of the + * 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 tag. + * + * The keys contain the actual enum values while the values contain their + * corresponding descriptions. + * + * @param DomNode $node The DomNode representation of the + * 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 + * tag. + * + * @param DomNode &$node The DomNode representation of the + * 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 + * tag as used inside of enum configurations. + * + * @param DomNode $node The DomNode representation of the + * 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 + * @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 index 000000000..c70669470 --- /dev/null +++ b/framework/Core/lib/Horde/Horde/ErrorHandler.php @@ -0,0 +1,202 @@ + '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\[function\.[\d\w-_]+\]%", '', $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 '

' . htmlspecialchars($error['file']) . ':' . htmlspecialchars($error['line']) . ': ' . htmlspecialchars($error['str']) . '

'; + } + } + } + +} diff --git a/framework/Core/lib/Horde/Horde/Exception.php b/framework/Core/lib/Horde/Horde/Exception.php new file mode 100644 index 000000000..375485eaf --- /dev/null +++ b/framework/Core/lib/Horde/Horde/Exception.php @@ -0,0 +1,66 @@ +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 index 000000000..cc1a1ea25 --- /dev/null +++ b/framework/Core/lib/Horde/Horde/Help.php @@ -0,0 +1,530 @@ + + * @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 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 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 ' '; + } + + 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 <b> 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 <i> 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 <pre> 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 index 000000000..5a66ce835 --- /dev/null +++ b/framework/Core/lib/Horde/Horde/Menu.php @@ -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\"> </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 index 000000000..b2d81d6d0 --- /dev/null +++ b/framework/Core/lib/Horde/Horde/Registry.php @@ -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 index 000000000..661b2bbc1 --- /dev/null +++ b/framework/Core/lib/Horde/Horde/Registry/Caller.php @@ -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 index 000000000..f93b2989d --- /dev/null +++ b/framework/Core/lib/Horde/Horde/Release.php @@ -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 index 000000000..27ec2fa91 --- /dev/null +++ b/framework/Core/lib/Horde/Horde/Release/Whups.php @@ -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 index 000000000..c9c06cba8 --- /dev/null +++ b/framework/Core/lib/Horde/Horde/Script/Files.php @@ -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 index 000000000..050a4f3b2 --- /dev/null +++ b/framework/Core/package.xml @@ -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 index 000000000..6a9e0b676 --- /dev/null +++ b/framework/Core/test/Horde/Framework/url.phpt @@ -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&bar=2', + 'test.php?foo=1&bar=2&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&PHPSESSID= +test.php?foo=1 +test.php?foo=1&PHPSESSID= +test.php?foo=1 +test.php?foo=1&PHPSESSID= +test.php?foo=1 +test.php?foo=1&PHPSESSID= +test.php?foo=1 +test.php?foo=1&PHPSESSID= +test.php?foo=1 +test.php?foo=1&PHPSESSID= +test.php?foo=1 +test.php?foo=1&PHPSESSID= +test.php?foo=1 +test.php?foo=1&PHPSESSID= +test.php?foo=1 +test.php?foo=1&PHPSESSID= +test.php?foo=1 +test.php?foo=1&PHPSESSID= +test.php?foo=1 +test.php?foo=1&PHPSESSID= +test.php?foo=1 +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: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&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +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: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&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +test.php?foo=1&bar=2&PHPSESSID= +test.php?foo=1&bar=2 +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: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&bar=2&baz=3 +test.php?foo=1&bar=2&baz=3&PHPSESSID= +test.php?foo=1&bar=2&baz=3 +test.php?foo=1&bar=2&baz=3&PHPSESSID= +test.php?foo=1&bar=2&baz=3 +test.php?foo=1&bar=2&baz=3&PHPSESSID= +test.php?foo=1&bar=2&baz=3 +test.php?foo=1&bar=2&baz=3&PHPSESSID= +test.php?foo=1&bar=2&baz=3 +test.php?foo=1&bar=2&baz=3&PHPSESSID= +test.php?foo=1&bar=2&baz=3 +test.php?foo=1&bar=2&baz=3&PHPSESSID= +test.php?foo=1&bar=2&baz=3 +test.php?foo=1&bar=2&baz=3&PHPSESSID= +test.php?foo=1&bar=2&baz=3 +test.php?foo=1&bar=2&baz=3&PHPSESSID= +test.php?foo=1&bar=2&baz=3 +test.php?foo=1&bar=2&baz=3&PHPSESSID= +test.php?foo=1&bar=2&baz=3 +test.php?foo=1&bar=2&baz=3&PHPSESSID= +test.php?foo=1&bar=2&baz=3 +test.php?foo=1&bar=2&baz=3&PHPSESSID= +test.php?foo=1&bar=2&baz=3 +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: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 index 000000000..f93b2989d --- /dev/null +++ b/framework/Release/lib/Horde/Release.php @@ -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 index 000000000..27ec2fa91 --- /dev/null +++ b/framework/Release/lib/Horde/Release/Whups.php @@ -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 index 000000000..8e6b36d8e --- /dev/null +++ b/framework/Release/package.xml @@ -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> -- 2.11.0