* Copyright 1999-2005 Jon Parise * Copyright 1999-2005 Anil Madhavapeddy * * 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 * @author Jon Parise * @author Anil Madhavapeddy * @since Horde 1.3 * @package Horde_Framework */ class Registry { /** * Hash storing all of the known services and callbacks. * * @var array $_apiCache */ var $_apiCache = array(); /** * Hash storing all known data types. * * @var array $_typeCache */ var $_typeCache = array(); /** * Hash storing all of the registered interfaces that applications * provide. * * @var array $_interfaces */ var $_interfaces = array(); /** * Hash storing information on each registry-aware application. * * @var array $applications */ var $applications = array(); /** * Stack of in-use applications. * * @var array $_appStack */ var $_appStack = array(); /** * Quick pointer to the current application. * * @var $_currentApp */ var $_currentApp = null; /** * Cache of $prefs objects * * @var array $_prefsCache */ var $_prefsCache = array(); /** * Cache of application configurations. * * @var array $_confCache */ var $_confCache = array(); /** * Returns a reference to the global Registry object, only * creating it if it doesn't already exist. * * This method must be invoked as: $registry = &Registry::singleton() * * @param optional integer $session_flags Any session flags. * * @return object Registry The Horde Registry instance. */ function &singleton($session_flags = 0) { static $registry; if (!isset($registry)) { $registry = new Registry($session_flags); } return $registry; } /** * Create a new registry instance. Should never be called except * by &Registry::singleton(). * * @param optional integer $session_flags Any session flags. * * @access private */ function Registry($session_flags = 0) { /* Import and global Horde's configuration values. */ $this->importConfig('horde'); /* Start a session. */ if ($session_flags & HORDE_SESSION_NONE) { /* Never start a session if the session flags include HORDE_SESSION_NONE. */ $_SESSION = array(); } else { Horde::setupSessionHandler(); @session_start(); if ($session_flags & HORDE_SESSION_READONLY) { /* Close the session immediately so no changes can be made but values are still available. */ @session_write_close(); } } /* Read the registry configuration file. */ require_once HORDE_BASE . '/config/registry.php'; /* Initialize the localization routines and variables. */ # NLS::setLang(); # NLS::setTextdomain('horde', HORDE_BASE . '/locale', NLS::getCharset()); # String::setDefaultCharset(NLS::getCharset()); /* Stop system if Horde is inactive. */ if ($this->applications['horde']['status'] == 'inactive') { Horde::fatal(_("This system is currently deactivated."), __FILE__, __LINE__); } /* Scan for all APIs provided by each app, and set other * common defaults like templates and graphics. */ $appList = array_keys($this->applications); foreach ($appList as $appName) { $app = &$this->applications[$appName]; if (($app['status'] == 'heading') || ($app['status'] == 'inactive') || ($app['status'] == 'admin' && !Auth::isAdmin())) { continue; } if (isset($app['provides'])) { if (is_array($app['provides'])) { foreach ($app['provides'] as $interface) { $this->_interfaces[$interface] = $appName; } } else { $this->_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'; } } # /* Create the global Perms object. */ # $GLOBALS['perms'] = &Perms::singleton(); # /* Attach javascript notification listener. */ # $notification = &Notification::singleton(); # $notification->attach('javascript'); /* Register access key logger for translators. */ if (@$GLOBALS['conf']['log_accesskeys']) { register_shutdown_function(create_function('', 'Horde::getAccessKey(null, null, true);')); } } /** * Return a list of the installed and registered applications. * * @since Horde 2.2 * * @access public * * @param array $filter (optional) An array of the statuses that * should be returned. Defaults to non-hidden. * @param boolean $assoc (optional) Associative array with app names * as keys. * @param integer $permission (optional) The permission level to check * for in the list. Defaults to PERMS_SHOW. * * @return array List of apps registered with Horde. If no * applications are defined returns an empty array. */ function listApps($filter = null, $assoc = false, $permission = PERMS_SHOW) { $apps = array(); if (is_null($filter)) { $filter = array('notoolbar', 'active'); } foreach ($this->applications as $app => $params) { if (in_array($params['status'], $filter) && (defined('AUTH_HANDLER') || $this->hasPermission($app, $permission))) { $assoc ? $apps[$app] = $app : $apps[] = $app; } } return $apps; } /** * Returns all available registry APIs. * * @access public * * @return array The API list. */ function listAPIs() { $apis = array(); foreach (array_keys($this->_interfaces) as $interface) { @list($api, ) = explode('/', $interface); $apis[] = $api; } return array_unique($apis); } /** * Returns all of the available registry methods, or alternately * only those for a specified API. * * @access public * * @param optional string $api Defines the API for which the methods * shall be returned. * * @return array The method list. */ function listMethods($api = null) { $methods = array(); $this->_fillAPICache(); 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); } } else { $provides = array(); } foreach ($provides as $method) { if (strpos($method, '/') !== false) { if (is_null($api) || (substr($method, 0, strlen($api)) == $api)) { $methods[] = $method; } } elseif (is_null($api) || ($method == $api)) { if (isset($this->_apiCache[$app])) { foreach (array_keys($this->_apiCache[$app]) as $service) { $methods[] = $method . '/' . $service; } } } } } return array_unique($methods); } /** * Returns all of the available registry data types. * * @access public * * @return array The data type list. */ function listTypes() { $this->_fillAPICache(); return $this->_typeCache; } /** * Returns a method's signature. * * @access public * * @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. */ function getSignature($method) { if (!($app = $this->hasMethod($method))) { return; } $this->_fillAPICache(); @list(, $function) = explode('/', $method); if (isset($this->_apiCache[$app][$function]['type']) && isset($this->_apiCache[$app][$function]['args'])) { return array($this->_apiCache[$app][$function]['args'], $this->_apiCache[$app][$function]['type']); } } /** * Determine if an interface is implemented by an active * application. * * @access public * * @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. */ function hasInterface($interface) { return !empty($this->_interfaces[$interface]) ? $this->_interfaces[$interface] : false; } /** * Determine if a method has been registered with the registry. * * @access public * * @param string $method The full name of the method to check for. * @param string $app (optional) Only check this application. * * @return mixed The application implementing $method if we have it, * false if the method doesn't exist. */ function hasMethod($method, $app = null) { if (is_null($app)) { @list($interface, $call) = explode('/', $method); if (!empty($this->_interfaces[$method])) { $app = $this->_interfaces[$method]; } elseif (!empty($this->_interfaces[$interface])) { $app = $this->_interfaces[$interface]; } else { return false; } } else { $call = $method; } $this->_fillAPICache(); return !empty($this->_apiCache[$app][$call]) ? $app : false; } /** * 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". * * @access public * * @param string $method The method to call. * @param optional array $args Arguments to the method. * * @return TODO * Returns PEAR_Error on error. */ function call($method, $args = array()) { @list($interface, $call) = explode('/', $method); if (!empty($this->_interfaces[$method])) { $app = $this->_interfaces[$method]; } elseif (!empty($this->_interfaces[$interface])) { $app = $this->_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. * * @access public * * @param string $app The application being called. * @param string $call The method to call. * @param optional array $args Arguments to the method. * * @return TODO * Returns PEAR_Error on error. */ 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)); } /* Make sure that the function actually exists. */ $function = '_' . $app . '_' . $call; if (!function_exists($function)) { return PEAR::raiseError('The function implementing ' . $call . ' (' . $function . ') is not defined in ' . $app . '\'s API.'); } $checkPerms = isset($this->_apiCache[$app][$call]['checkperms']) ? $this->_apiCache[$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". * * @access public * * @param string $method The method to link to. * @param optional array $args Arguments to the method. * @param optional mixed $extra Extra, non-standard arguments to the * method. * * @return TODO * Returns PEAR_Error on error. */ function link($method, $args = array(), $extra = '') { @list($interface, $call) = explode('/', $method); if (!empty($this->_interfaces[$method])) { $app = $this->_interfaces[$method]; } elseif (!empty($this->_interfaces[$interface])) { $app = $this->_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. * * @access public * * @param string $app The application being called. * @param string $call The method to link to. * @param optional array $args Arguments to the method. * @param optional mixed $extra Extra, non-standard arguments to the * method. * * @return TODO * Returns PEAR_Error on error. */ 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. */ if (empty($this->_apiCache[$app][$call]['link'])) { return PEAR::raiseError('The link ' . $call . ' is not defined in ' . $app . '\'s API.'); } /* Initial link value. */ $link = $this->_apiCache[$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('|' . 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. * * @access public * * @param string $path The application string. * @param optional string $app The application being called. * * @return TODO * Returns PEAR_Error on error. */ function applicationFilePath($path, $app = null) { if (is_null($app)) { $app = $this->_currentApp; } 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. * * @access public * * @param string $path The application string. * @param optional string $app The application being called. * * @return TODO * Returns PEAR_Error on error. */ function applicationWebPath($path, $app = null) { if (!isset($app)) { $app = $this->_currentApp; } 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. * * @access public * * @param string $app The name of the application to push. * @param boolean $checkPerms (optional) 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. */ function pushApp($app, $checkPerms = true) { if ($app == $this->_currentApp) { 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' && !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('User %s does not have READ permission for %s', Auth::getAuth(), $app), __FILE__, __LINE__, PEAR_LOG_DEBUG); return PEAR::raiseError(sprintf(_("User %s is not authorised for %s."), Auth::getAuth(), $this->applications[$app]['name']), 'permission_denied'); } /* 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($this->_prefsCache[$app]) && isset($this->_prefsCache[$app]->_prefs['language'])) { $language = $this->_prefsCache[$app]->getValue('language'); } NLS::setLang($language); NLS::setTextdomain($app, $this->applications[$app]['fileroot'] . '/locale', NLS::getCharset()); String::setDefaultCharset(NLS::getCharset()); /* Once we know everything succeeded and is in a consistent * state again, push the new application onto the stack. */ array_push($this->_appStack, $app); $this->_currentApp = $app; return true; } /** * Remove the current app from the application stack, setting the * current app to whichever app was current before this one took * over. * * @access public * * @return string The name of the application that was popped. */ 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. */ $this->_currentApp = count($this->_appStack) ? end($this->_appStack) : null; if ($this->_currentApp) { $this->importConfig($this->_currentApp); $this->loadPrefs($this->_currentApp); #$language = $GLOBALS['prefs']->getValue('language'); #if (isset($language)) { # NLS::setLang($language); #} NLS::setTextdomain($this->_currentApp, $this->applications[$this->_currentApp]['fileroot'] . '/locale', NLS::getCharset()); String::setDefaultCharset(NLS::getCharset()); } return $previous; } /** * Return the current application - the app at the top of the * application stack. * * @access public * * @return string The current application. */ function getApp() { return $this->_currentApp; } /** * Check permissions on an application. * * @access public * * @return boolean Whether or not access is allowed. */ function hasPermission($app, $permission = PERMS_READ) { return true; #return Auth::isAdmin() || ($GLOBALS['perms']->exists($app) ? # $GLOBALS['perms']->hasPermission($app, Auth::getAuth(), $permission) : # (bool)Auth::getAuth()); } /** * Reads the configuration values for the given application and * imports them into the global $conf variable. * * @access public * * @param string $app The name of the application. * * @return boolean True on success, PEAR_Error on error. */ function importConfig($app) { /* Don't make config files global $registry themselves. */ global $registry; /* Cache config values so that we don't re-read files on every * popApp() call. */ if (!isset($this->_confCache[$app])) { if (!isset($this->_confCache['horde'])) { $conf = array(); ob_start(); $success = include HORDE_BASE . '/config/conf.php'; $errors = ob_get_contents(); ob_end_clean(); if (!empty($errors)) { return PEAR::raiseError(sprintf('Failed to import Horde configuration: %s', strip_tags($errors))); } if (!$success) { return PEAR::raiseError('Failed to import Horde configuration.'); } /* Initial Horde-wide settings. */ /* Set the error reporting level in accordance with * the config settings. */ error_reporting($conf['debug_level']); /* Set the maximum execution time in accordance with * the config settings. */ @set_time_limit($conf['max_exec_time']); /* Set the umask according to config settings. */ if (isset($conf['umask'])) { umask($conf['umask']); } } else { $conf = $this->_confCache['horde']; } if ($app !== 'horde') { $success = @include $this->applications[$app]['fileroot'] . '/config/conf.php'; if (!$success) { return PEAR::raiseError('Failed to import application configuration for ' . $app); } } $this->_confCache[$app] = &$conf; } $GLOBALS['conf'] = &$this->_confCache[$app]; return true; } /** * Loads the preferences for the current user for the current * application and imports them into the global $prefs variable. * * @access public * * @param string $app The name of the application. */ function loadPrefs($app = null) { return array(); static $prefs_default = false; require_once 'Horde/Prefs.php'; if ($app === null) { $app = $this->_currentApp; } /* If there is no logged in user, return an empty Prefs:: * object with just default preferences. */ # if (!Auth::getAuth()) { # $prefs = &Prefs::factory('none', $app, '', '', null, false); # $prefs->retrieve(); # $this->_prefsCache[$app] = &$prefs; # $GLOBALS['prefs'] = &$this->_prefsCache[$app]; # $prefs_default = true; # return; # } /* Cache prefs objects so that we don't re-load them on every * popApp() call. */ # if (!isset($this->_prefsCache[$app]) || # !empty($prefs_default)) { # $prefs = &Prefs::factory($GLOBALS['conf']['prefs']['driver'], $app, # Auth::getAuth(), Auth::getCredential('password')); # $prefs->retrieve(); # $this->_prefsCache[$app] = &$prefs; # } $GLOBALS['prefs'] = &$this->_prefsCache[$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. * * @access public * * @param string $app (optional) The application to unload prefrences for. * If null, ALL preferences are reset. */ function unloadPrefs($app = null) { if ($app === null) { $this->_prefsCache = array(); } elseif (isset($this->_prefsCache[$app])) { unset($this->_prefsCache[$app]); } else { return; } if ($this->_currentApp) { $this->loadPrefs(); } } /** * Return the requested configuration parameter for the specified * application. If no application is specified, the value of * $this->_currentApp (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. * * @access public * * @param string $parameter The configuration value to retrieve. * @param optional string $app The application to get the value for. * * @return string The requested parameter, or null if it is not set. */ function get($parameter, $app = null) { if (is_null($app)) { $app = $this->_currentApp; } if (isset($this->applications[$app][$parameter])) { $pval = $this->applications[$app][$parameter]; } else { if ($parameter == 'icon') { $pval = $this->_getIcon($app); } else { $pval = isset($this->applications['horde'][$parameter]) ? $this->applications['horde'][$parameter] : null; } } if ($parameter == 'name') { return _($pval); } else { return $pval; } } /** * Function to work out an application's graphics URI, taking into * account any themes directories that may be set up. * * @access public * * @param optional string $app The application for which to get the * image directory. If blank will default * to current application. * * @return string The image directory uri path. */ function getImageDir($app = null) { if (empty($app)) { $app = $this->_currentApp; } static $img_dir = array(); if (isset($img_dir[$app])) { return $img_dir[$app]; } /* This is the default location for the graphics. */ $img_dir[$app] = $this->get('themesuri', $app) . '/graphics'; /* Figure out if this is going to be overridden by any theme * settings. */ if (isset($GLOBALS['prefs']) && ($theme = $GLOBALS['prefs']->getValue('theme')) && (@include $this->get('themesfs', 'horde') . '/' . $theme . '/info.php') && isset($theme_icons) && in_array($app, $theme_icons)) { $img_dir[$app] = $this->get('themesuri', $app) . '/' . $theme . '/graphics'; } return $img_dir[$app]; } /** * Returns the path to an application's icon, respecting whether the * current theme has its own icons. * * @access private * * @param string $app The application for which to get the icon. */ function _getIcon($app) { return $this->getImageDir($app) . '/' . $app . '.png'; } /** * Query the initial page for an application - the webroot, if * there is no initial_page set, and the initial_page, if it is * set. * * @access public * * @param optional string $app The name of the application. * * @return string URL pointing to the inital page of the application. * Returns PEAR_Error on error. */ function getInitialPage($app = null) { if (is_null($app)) { $app = $this->_currentApp; } if (!isset($this->applications[$app])) { return PEAR::raiseError(sprintf(_("'%s' is not configured in the Horde Registry."), $app)); } return $this->applications[$app]['webroot'] . '/' . (isset($this->applications[$app]['initial_page']) ? $this->applications[$app]['initial_page'] : ''); } /** * Fills the registry's API cache with the available services. * * @access private */ function _fillAPICache() { if (!empty($this->_apiCache)) { return; } $status = array('active', 'notoolbar', 'hidden'); # if (Auth::isAdmin()) { # $status[] = 'admin'; # } $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; } if (!isset($_services)) { $this->_apiCache[$app] = array(); } else { $this->_apiCache[$app] = $_services; } if (isset($_types)) { foreach ($_types as $type => $params) { /* Prefix non-Horde types with the application * name. */ $prefix = $app == 'horde' ? '' : "${app}_"; $this->_typeCache[$prefix . $type] = $params; } } } } }