⚝
One Hat Cyber Team
⚝
Your IP:
216.73.216.72
Server IP:
41.128.143.86
Server:
Linux host.raqmix.cloud 6.8.0-1025-azure #30~22.04.1-Ubuntu SMP Wed Mar 12 15:28:20 UTC 2025 x86_64
Server Software:
Apache
PHP Version:
8.3.23
Buat File
|
Buat Folder
Eksekusi
Dir :
~
/
usr
/
share
/
psa-pear
/
pear
/
php
/
Horde
/
Edit File: Registry.php
<?php /** * Copyright 1999-2017 Horde LLC (http://www.horde.org/) * * See the enclosed file COPYING for license information (LGPL). If you * did not receive this file, see http://www.horde.org/licenses/lgpl21. * * @category Horde * @copyright 1999-2017 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Core */ /** * The registry provides a set of methods for communication between Horde * applications and keeping track of application configuration information. * * @author Chuck Hagenbuch <chuck@horde.org> * @author Jon Parise <jon@horde.org> * @author Anil Madhavapeddy <anil@recoil.org> * @author Michael Slusarz <slusarz@horde.org> * @category Horde * @copyright 1999-2017 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Core */ class Horde_Registry implements Horde_Shutdown_Task { /* Session flags. */ const SESSION_NONE = 1; const SESSION_READONLY = 2; /* Error codes for pushApp(). */ const AUTH_FAILURE = 1; const NOT_ACTIVE = 2; const PERMISSION_DENIED = 3; const HOOK_FATAL = 4; const INITCALLBACK_FATAL = 5; /* View types. */ const VIEW_BASIC = 1; const VIEW_DYNAMIC = 2; const VIEW_MINIMAL = 3; const VIEW_SMARTMOBILE = 4; /* Session keys. */ const REGISTRY_CACHE = 'registry_cache'; /** * Hash storing information on each registry-aware application. * * @var array */ public $applications = array(); /** * Original authentication exception. Set if 'fallback' auth is used, and * authentication fails. * * @since 2.11.0 * @todo Fix this up for H6 (framework needs to do better job of * supporting bootstrapping before authentication). * * @var Exception */ public $authException; /** * A flag that is set once the basic horde application has been * minimally configured. * * @var boolean */ public $hordeInit = false; /** * The application that called appInit(). * * @var string */ public $initialApp; /** * NLS configuration. * * @var Horde_Registry_Nlsconfig */ public $nlsconfig; /** * The current virtual host configuration file. * * @since 2.12.0 * * @var string */ public $vhost = null; /** * The list of APIs. * * @var array */ protected $_apiList = array(); /** * Stack of in-use applications. * * @var array */ protected $_appStack = array(); /** * The list of applications initialized during this access. * * @var array */ protected $_appsInit = array(); /** * The arguments that have been passed when instantiating the registry. * * @var array */ protected $_args = array(); /** * Internal cached data. * * @var array */ protected $_cache = array( 'auth' => null, 'cfile' => array(), 'conf' => array(), 'existing' => array(), 'ob' => array() ); /** * Interfaces list. * * @var array */ protected $_interfaces = array(); /** * The last modified time of the newest modified registry file. * * @var integer */ protected $_regmtime; /** * Application bootstrap initialization. * Solves chicken-and-egg problem - need a way to init Horde environment * from application without an active Horde_Registry object. * * Page compression will be started (if configured). * * Global variables defined: * <pre> * - $browser: Horde_Browser object * - $cli: Horde_Cli object (if 'cli' is true) * - $conf: Configuration array * - $injector: Horde_Injector object * - $language: Language * - $notification: Horde_Notification object * - $page_output: Horde_PageOutput object * - $prefs: Horde_Prefs object * - $registry: Horde_Registry object * - $session: Horde_Session object * </pre> * * @param string $app The application to initialize. * @param array $args Optional arguments: * <pre> * - admin: (boolean) Require authenticated user to be an admin? * DEFAULT: false * - authentication: (string) The type of authentication to use: * - none: Do not authenticate * - fallback: Attempt to authenticate; if failure, then don't auth * (@since 2.11.0). * - [DEFAULT]: Authenticate; on no auth redirect to login screen * - cli: (boolean) Initialize a CLI interface. Setting this to true * implicitly sets 'authentication' to 'none' and 'admin' and * 'nocompress' to true. * DEFAULT: false * - nocompress: (boolean) If set, the page will not be compressed. * DEFAULT: false * - nologintasks: (boolean) If set, don't perform logintasks (never * performed if authentication is 'none'). * DEFAULT: false * - nonotificationinit: (boolean) If set, don't initialize the * application handlers for the notification * system (@since 2.12.0). * - permission: (array) The permission required by the user to access * the page. The first element (REQUIRED) is the permission * name. The second element (OPTION; defaults to SHOW) is * the permission level. * - session_cache_limiter: (string) Use this value for the session * cache limiter. * DEFAULT: Uses the value in the config. * - session_control: (string) Special session control limitations: * - netscape: TODO; start read/write session * - none: Do not start a session * - readonly: Start session readonly * - [DEFAULT] - Start read/write session * - test: (boolean) Is this the test script? If so, we relax several * sanity checks and don't load things from the cache. * DEFAULT: false * - timezone: (boolean) Set the time zone? * DEFAULT: false * - user_admin: (boolean) Set authentication to an admin user? * DEFAULT: false * </pre> * * @return Horde_Registry_Application The application object. * @throws Horde_Exception */ public static function appInit($app, array $args = array()) { if (isset($GLOBALS['registry'])) { return $GLOBALS['registry']->getApiInstance($app, 'application'); } $args = array_merge(array( 'admin' => false, 'authentication' => null, 'cli' => null, 'nocompress' => false, 'nologintasks' => false, 'nonotificationinit' => false, 'permission' => false, 'session_cache_limiter' => null, 'session_control' => null, 'timezone' => false, 'user_admin' => null, ), $args); /* CLI initialization. */ if ($args['cli']) { /* Make sure no one runs from the web. */ if (!Horde_Cli::runningFromCLI()) { throw new Horde_Exception(Horde_Core_Translation::t("Script must be run from the command line")); } /* Load the CLI environment - make sure there's no time limit, * init some variables, etc. */ $GLOBALS['cli'] = Horde_Cli::init(); $args['nocompress'] = true; $args['authentication'] = 'none'; } // For 'fallback' authentication, try authentication first. if ($args['authentication'] === 'fallback') { $fallback_auth = true; $args['authentication'] = null; } else { $fallback_auth = false; } // Registry. $s_ctrl = 0; switch ($args['session_control']) { case 'netscape': // Chicken/egg: Browser object doesn't exist yet. // Can't use Horde_Core_Browser since it depends on registry to be // configured. $browser = new Horde_Browser(); if ($browser->isBrowser('mozilla')) { $args['session_cache_limiter'] = 'private, must-revalidate'; } break; case 'none': $s_ctrl = self::SESSION_NONE; break; case 'readonly': $s_ctrl = self::SESSION_READONLY; break; } $classname = __CLASS__; $registry = $GLOBALS['registry'] = new $classname($s_ctrl, $args); $registry->initialApp = $app; $appob = $registry->getApiInstance($app, 'application'); $appob->initParams = $args; do { try { $registry->pushApp($app, array( 'check_perms' => ($args['authentication'] != 'none'), 'logintasks' => !$args['nologintasks'], 'notransparent' => !empty($args['notransparent']) )); if ($args['admin'] && !$registry->isAdmin()) { throw new Horde_Exception(Horde_Core_Translation::t("Not an admin")); } $e = null; } catch (Horde_Exception_PushApp $e) { if ($fallback_auth) { $registry->authException = $e; $registry->setAuthenticationSetting('none'); $args['authentication'] = 'none'; $fallback_auth = false; continue; } } break; } while (true); if (!is_null($e)) { $appob->appInitFailure($e); switch ($e->getCode()) { case self::AUTH_FAILURE: $failure = new Horde_Exception_AuthenticationFailure($e->getMessage()); $failure->application = $app; throw $failure; case self::NOT_ACTIVE: /* Try redirect to Horde if an app is not active. */ if (!$args['cli'] && $app != 'horde') { $GLOBALS['notification']->push($e, 'horde.error'); Horde::url($registry->getInitialPage('horde'))->redirect(); } /* Shouldn't reach here, but fall back to permission denied * error if we can't even access Horde. */ // Fall-through case self::PERMISSION_DENIED: $failure = new Horde_Exception_AuthenticationFailure($e->getMessage(), Horde_Auth::REASON_MESSAGE); $failure->application = $app; throw $failure; } throw $e; } if ($args['timezone']) { $registry->setTimeZone(); } if (!$args['nocompress']) { $GLOBALS['page_output']->startCompression(); } if ($args['user_admin']) { if (empty($GLOBALS['conf']['auth']['admins'])) { throw new Horde_Exception(Horde_Core_Translation::t("Admin authentication requested, but no admin users defined in configuration.")); } $registry->setAuth( reset($GLOBALS['conf']['auth']['admins']), array(), array('no_convert' => true) ); } if ($args['permission']) { $admin_opts = array( 'permission' => $args['permission'][0], 'permlevel' => (isset($args['permission'][1]) ? $args['permission'][1] : Horde_Perms::SHOW) ); if (!$registry->isAdmin($admin_opts)) { throw new Horde_Exception_PermissionDenied(Horde_Core_Translation::t("Permission denied.")); } } return $appob; } /** * Create a new Horde_Registry instance. * * @param integer $session_flags Any session flags. * @param array $args See appInit(). * * @throws Horde_Exception */ public function __construct($session_flags = 0, array $args = array()) { /* Set a valid timezone. */ date_default_timezone_set( ini_get('date.timezone') ?: getenv('TZ') ?: 'UTC' ); /* Save arguments. */ $this->_args = $args; /* Define factories. By default, uses the 'create' method in the given * classname (string). If other function needed, define as the second * element in an array. */ $factories = array( 'Horde_ActiveSyncBackend' => 'Horde_Core_Factory_ActiveSyncBackend', 'Horde_ActiveSyncServer' => 'Horde_Core_Factory_ActiveSyncServer', 'Horde_ActiveSyncState' => 'Horde_Core_Factory_ActiveSyncState', 'Horde_Alarm' => 'Horde_Core_Factory_Alarm', 'Horde_Browser' => 'Horde_Core_Factory_Browser', 'Horde_Cache' => 'Horde_Core_Factory_Cache', 'Horde_Controller_Request' => 'Horde_Core_Factory_Request', 'Horde_Controller_RequestConfiguration' => array( 'Horde_Core_Controller_RequestMapper', 'getRequestConfiguration', ), 'Horde_Core_Auth_Signup' => 'Horde_Core_Factory_AuthSignup', 'Horde_Core_CssCache' => 'Horde_Core_Factory_CssCache', 'Horde_Core_JavascriptCache' => 'Horde_Core_Factory_JavascriptCache', 'Horde_Core_Perms' => 'Horde_Core_Factory_PermsCore', 'Horde_Dav_Server' => 'Horde_Core_Factory_DavServer', 'Horde_Dav_Storage' => 'Horde_Core_Factory_DavStorage', 'Horde_Db_Adapter' => 'Horde_Core_Factory_DbBase', 'Horde_Editor' => 'Horde_Core_Factory_Editor', 'Horde_ElasticSearch_Client' => 'Horde_Core_Factory_ElasticSearch', 'Horde_Group' => 'Horde_Core_Factory_Group', 'Horde_HashTable' => 'Horde_Core_Factory_HashTable', 'Horde_History' => 'Horde_Core_Factory_History', 'Horde_Kolab_Server_Composite' => 'Horde_Core_Factory_KolabServer', 'Horde_Kolab_Session' => 'Horde_Core_Factory_KolabSession', 'Horde_Kolab_Storage' => 'Horde_Core_Factory_KolabStorage', 'Horde_Lock' => 'Horde_Core_Factory_Lock', 'Horde_Log_Logger' => 'Horde_Core_Factory_Logger', 'Horde_Mail' => 'Horde_Core_Factory_MailBase', 'Horde_Memcache' => 'Horde_Core_Factory_Memcache', 'Horde_Nosql_Adapter' => 'Horde_Core_Factory_NosqlBase', 'Horde_Notification' => 'Horde_Core_Factory_Notification', 'Horde_Perms' => 'Horde_Core_Factory_Perms', 'Horde_Queue_Storage' => 'Horde_Core_Factory_QueueStorage', 'Horde_Routes_Mapper' => 'Horde_Core_Factory_Mapper', 'Horde_Routes_Matcher' => 'Horde_Core_Factory_Matcher', 'Horde_Secret' => 'Horde_Core_Factory_Secret', 'Horde_Secret_Cbc' => 'Horde_Core_Factory_Secret_Cbc', 'Horde_Service_Facebook' => 'Horde_Core_Factory_Facebook', 'Horde_Service_Twitter' => 'Horde_Core_Factory_Twitter', 'Horde_Service_UrlShortener' => 'Horde_Core_Factory_UrlShortener', 'Horde_SessionHandler' => 'Horde_Core_Factory_SessionHandler', 'Horde_Template' => 'Horde_Core_Factory_Template', 'Horde_Timezone' => 'Horde_Core_Factory_Timezone', 'Horde_Token' => 'Horde_Core_Factory_Token', 'Horde_Variables' => 'Horde_Core_Factory_Variables', 'Horde_View' => 'Horde_Core_Factory_View', 'Horde_View_Base' => 'Horde_Core_Factory_View', 'Horde_Weather' => 'Horde_Core_Factory_Weather', 'Net_DNS2_Resolver' => 'Horde_Core_Factory_Dns', 'Text_LanguageDetect' => 'Horde_Core_Factory_LanguageDetect', ); /* Define implementations. */ $implementations = array( 'Horde_Controller_ResponseWriter' => 'Horde_Controller_ResponseWriter_Web' ); /* Setup injector. */ $GLOBALS['injector'] = $injector = new Horde_Injector(new Horde_Injector_TopLevel()); foreach ($factories as $key => $val) { if (is_string($val)) { $val = array($val, 'create'); } $injector->bindFactory($key, $val[0], $val[1]); } foreach ($implementations as $key => $val) { $injector->bindImplementation($key, $val); } $GLOBALS['registry'] = $this; $injector->setInstance(__CLASS__, $this); /* Setup autoloader instance. */ $injector->setInstance('Horde_Autoloader', $GLOBALS['__autoloader']); /* Import and global Horde's configuration values. */ $this->importConfig('horde'); $conf = $GLOBALS['conf']; /* Set the umask according to config settings. */ if (isset($conf['umask'])) { umask($conf['umask']); } /* 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, but only if not running from the CLI */ if (!isset($GLOBALS['cli'])) { set_time_limit($conf['max_exec_time']); } /* The basic framework is up and loaded, so set the init flag. */ $this->hordeInit = true; /* Initial Horde-wide settings. */ /* Initialize browser object. */ $GLOBALS['browser'] = $injector->getInstance('Horde_Browser'); /* Get modified time of registry files. */ $regfiles = array( HORDE_BASE . '/config/registry.php', HORDE_BASE . '/config/registry.d' ); if (file_exists(HORDE_BASE . '/config/registry.local.php')) { $regfiles[] = HORDE_BASE . '/config/registry.local.php'; } if (!empty($conf['vhosts'])) { $vhost = HORDE_BASE . '/config/registry-' . $conf['server']['name'] . '.php'; if (file_exists($vhost)) { $regfiles[] = $this->vhost = $vhost; } } $this->_regmtime = max(array_map('filemtime', $regfiles)); /* Start a session. */ if ($session_flags & self::SESSION_NONE) { /* Never start a session if the session flags include SESSION_NONE. */ $GLOBALS['session'] = $session = new Horde_Session_Null(); $session->setup(true, $args['session_cache_limiter']); } elseif ((PHP_SAPI === 'cli') || (empty($_SERVER['SERVER_NAME']) && ((PHP_SAPI === 'cgi') || (PHP_SAPI === 'cgi-fcgi')))) { $GLOBALS['session'] = $session = new Horde_Session(); $session->setup(false, $args['session_cache_limiter']); } else { $GLOBALS['session'] = $session = new Horde_Session(); $session->setup(true, $args['session_cache_limiter']); if ($session_flags & self::SESSION_READONLY) { /* Close the session immediately so no changes can be made but values are still available. */ $session->close(); } } $injector->setInstance('Horde_Session', $session); /* Always need to load applications information. */ $this->_loadApplications(); /* Stop system if Horde is inactive. */ if ($this->applications['horde']['status'] == 'inactive') { throw new Horde_Exception(Horde_Core_Translation::t("This system is currently deactivated.")); } /* Initialize language configuration object. */ $this->nlsconfig = new Horde_Registry_Nlsconfig(); /* Initialize the localization routines and variables. */ $this->setLanguageEnvironment(null, 'horde'); /* Initialize global page output object. */ $GLOBALS['page_output'] = $injector->getInstance('Horde_PageOutput'); /* Initialize notification object. Always attach status listener by * default. */ $nclass = null; switch ($this->getView()) { case self::VIEW_DYNAMIC: $nclass = 'Horde_Core_Notification_Listener_DynamicStatus'; break; case self::VIEW_SMARTMOBILE: $nclass = 'Horde_Core_Notification_Listener_SmartmobileStatus'; break; } $GLOBALS['notification'] = $injector->getInstance('Horde_Notification'); if (empty($args['nonotificationinit'])) { $GLOBALS['notification']->attachAllAppHandlers(); } $GLOBALS['notification']->attach('status', null, $nclass); Horde_Shutdown::add($this); } /** * (Re)set the authentication parameter. Useful for requests, such as Rpc * requests where we actually don't perform authentication until later in * the request, but still need Horde bootstrapped early in the request. Also * clears the local app/api cache since applications will probably already * have been initialized during Notification polling. * * @see appInit() * * @param string $authentication The authentication setting. */ public function setAuthenticationSetting($authentication) { $this->_args['authentication'] = $authentication; $this->_cache['cfile'] = $this->_cache['ob'] = array(); $this->_cache['isauth'] = array(); $this->_appsInit = array(); while ($this->popApp()); } /** * Events to do on shutdown. */ public function shutdown() { /* 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. */ Horde::log('Max memory usage: ' . memory_get_peak_usage(true) . ' bytes', 'DEBUG'); } /** * A property call to the registry object will return a Caller object. */ public function __get($api) { if (in_array($api, $this->listAPIs())) { return new Horde_Registry_Caller($this, $api); } throw new Horde_Exception('The API "' . $api . '" is not defined in the Horde Registry.'); } /** * Clone should never be called on this object. If it is, die. * * @throws Horde_Exception */ public function __clone() { throw new LogicException('Registry objects should never be cloned.'); } /** * serialize() should never be called on this object. If it is, die. * * @throws Horde_Exception */ public function __sleep() { throw new LogicException('Registry objects should never be serialized.'); } /** * Rebuild the registry configuration. */ public function rebuild() { global $session; $app = $this->getApp(); $this->applications = $this->_apiList = $this->_cache['conf'] = $this->_cache['ob'] = $this->_interfaces = array(); $session->remove('horde', 'nls/'); $session->remove('horde', 'registry/'); $session->remove('horde', self::REGISTRY_CACHE); $this->_loadApplications(); $this->importConfig('horde'); $this->importConfig($app); } /** * Load application information from registry config files. */ protected function _loadApplications() { global $cli, $injector; if (!empty($this->_interfaces)) { return; } /* First, try to load from cache. */ if (!isset($cli) && !$this->isTest()) { if (Horde_Util::extensionExists('apc')) { $cstorage = 'Horde_Cache_Storage_Apc'; } elseif (Horde_Util::extensionExists('xcache')) { $cstorage = 'Horde_Cache_Storage_Xcache'; } else { $cstorage = 'Horde_Cache_Storage_File'; } $cache = new Horde_Cache( new $cstorage(array( 'no_gc' => true, 'prefix' => 'horde_registry_cache_' )), array( 'lifetime' => 0, 'logger' => $injector->getInstance('Horde_Log_Logger') ) ); if (($cid = $this->_cacheId()) && ($cdata = $cache->get($cid, 0))) { try { list($this->applications, $this->_interfaces) = $injector->getInstance('Horde_Pack')->unpack($cdata); return; } catch (Horde_Pack_Exception $e) {} } } $config = new Horde_Registry_Registryconfig($this); $this->applications = $config->applications; $this->_interfaces = $config->interfaces; if (!isset($cache)) { return; } /* Need to determine hash of generated data, since it is possible that * there is dynamic data in the config files. This only needs to * be done once per session. */ $packed_data = $injector->getInstance('Horde_Pack')->pack(array( $this->applications, $this->_interfaces )); $cid = $this->_cacheId($packed_data); if (!$cache->exists($cid, 0)) { $cache->set($cid, $packed_data); } } /** * Get the cache ID for the registry information. * * @param string $hash If set, hash this value and use as the hash of the * registry. If false, uses session stored value. */ protected function _cacheId($hash = null) { global $session; if (!is_null($hash)) { $hash = hash('md5', $hash); $session->set('horde', self::REGISTRY_CACHE, $hash); } elseif (!($hash = $session->get('horde', self::REGISTRY_CACHE))) { return false; } /* Generate cache ID. */ return implode('|', array( gethostname() ?: php_uname(), __FILE__, $this->_regmtime, $hash )); } /** * Load an application's API object. * * @param string $app The application to load. * * @return Horde_Registry_Api The API object, or null if not available. */ protected function _loadApi($app) { if (isset($this->_cache['ob'][$app]['api'])) { return $this->_cache['ob'][$app]['api']; } $api = null; $status = array('active', 'notoolbar', 'hidden'); $status[] = $this->isAdmin() ? 'admin' : 'noadmin'; if (in_array($this->applications[$app]['status'], $status)) { try { $api = $this->getApiInstance($app, 'api'); } catch (Horde_Exception $e) { Horde::log($e, 'DEBUG'); } } $this->_cache['ob'][$app]['api'] = $api; return $api; } /** * Retrieve an API object. * * @param string $app The application to load. * @param string $type Either 'application' or 'api'. * * @return Horde_Registry_Api|Horde_Registry_Application The API object. * @throws Horde_Exception */ public function getApiInstance($app, $type) { if (isset($this->_cache['ob'][$app][$type])) { return $this->_cache['ob'][$app][$type]; } $path = $this->get('fileroot', $app) . '/lib'; /* 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. */ if (!isset($this->_cache['ob'][$app])) { $autoloader = $GLOBALS['injector']->getInstance('Horde_Autoloader'); $app_mappers = array( 'Controller' => 'controllers', 'Helper' => 'helpers', 'SettingsExporter' => 'settings' ); $applicationMapper = new Horde_Autoloader_ClassPathMapper_Application($this->get('fileroot', $app) . '/app'); foreach ($app_mappers as $key => $val) { $applicationMapper->addMapping($key, $val); } $autoloader->addClassPathMapper($applicationMapper); /* Skip horde, since this was already setup in core.php. */ if ($app != 'horde') { $autoloader->addClassPathMapper( new Horde_Autoloader_ClassPathMapper_PrefixString($app, $path) ); } } $cname = Horde_String::ucfirst($type); /* Can't autoload here, since the application may not have been * initialized yet. */ $classname = Horde_String::ucfirst($app) . '_' . $cname; $path = $path . '/' . $cname . '.php'; if (file_exists($path)) { include_once $path; } else { $classname = __CLASS__ . '_' . $cname; } if (!class_exists($classname, false)) { throw new Horde_Exception("$app does not have an API"); } $this->_cache['ob'][$app][$type] = ($type == 'application') ? new $classname($app) : new $classname(); return $this->_cache['ob'][$app][$type]; } /** * 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 Return hash with app names as keys and config * parameters as values? * @param integer $perms The permission level to check for in the list. * If null, skips permission check. * * @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 = Horde_Perms::SHOW) { if (is_null($filter)) { $filter = array('notoolbar', 'active'); } if (!$this->isAdmin() && in_array('active', $filter) && !in_array('noadmin', $filter)) { $filter[] = 'noadmin'; } $apps = array(); foreach ($this->applications as $app => $params) { if (in_array($params['status'], $filter)) { /* Topbar apps can only be displayed if the parent app is * active. */ if (($params['status'] == 'topbar') && $this->isInactive($params['app'])) { continue; } if ((is_null($perms) || $this->hasPermission($app, $perms))) { $apps[$app] = $params; } } } return $assoc ? $apps : array_keys($apps); } /** * Return a list of all applications, ignoring permissions. * * @return array List of all apps registered with Horde. */ public function listAllApps() { // Default to all installed (but possibly not configured) applications. return $this->listApps(array( 'active', 'admin', 'noadmin', 'hidden', 'inactive', 'notoolbar' ), false, null); } /** * Is the given application inactive? * * @param string $app The application to check. * * @return boolean True if inactive. */ public function isInactive($app) { return (!isset($this->applications[$app]) || ($this->applications[$app]['status'] == 'inactive') || (($this->applications[$app]['status'] == 'admin') && !$this->isAdmin()) || (($this->applications[$app]['status'] == 'noadmin') && $this->currentProcessAuth() && $this->isAdmin())); } /** * Returns all available registry APIs. * * @return array The API list. */ public function listAPIs() { if (empty($this->_apiList) && !empty($this->_interfaces)) { $apis = array(); foreach (array_keys($this->_interfaces) as $interface) { list($api,) = explode('/', $interface, 2); $apis[$api] = true; } $this->_apiList = array_keys($apis); } return $this->_apiList; } /** * 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. If null, returns all methods. * * @return array The method list. */ public function listMethods($api = null) { $methods = array(); 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 (($api_ob = $this->_loadApi($app)) && (is_null($api) || ($method == $api))) { foreach ($api_ob->methods() as $service) { $methods[$method . '/' . $service] = true; } } } } } return array_keys($methods); } /** * 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->_interfaces[$interface]) ? $this->_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) { return $this->_doHasSearch($method, $app, 'methods'); } /** * Determine if a link has been registered with the registry. * * @since 2.12.0 * * @param string $method The full name of the link method to check for. * @param string $app Only check this application. * * @return mixed The application implementing $method if we have it, * false if the link method doesn't exist. */ public function hasLink($method, $app = null) { return $this->_doHasSearch($method, $app, 'links'); } /** * Do the has*() search. * * @see hasMethod * @see hasLink * * @param string $func The API function to call to get the list of * elements to search. Either 'methods' or 'links'. * * @return mixed The application implementing $method, false if it * doesn't exist; */ protected function _doHasSearch($method, $app, $func) { if (is_null($app)) { if (($lookup = $this->_methodLookup($method)) === false) { return false; } list($app, $call) = $lookup; } else { $call = $method; } if ($api_ob = $this->_loadApi($app)) { switch ($func) { case 'links': $links = $api_ob->links(); return isset($links[$call]) ? $app : false; case 'methods': return in_array($call, $api_ob->methods()) ? $app : false; } } return 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". * * @param string $method The method to call. * @param array $args Arguments to the method. * * @return mixed Return from method call. * @throws Horde_Exception */ public function call($method, $args = array()) { if (($lookup = $this->_methodLookup($method)) === false) { throw new Horde_Exception('The method "' . $method . '" is not defined in the Horde Registry.'); } return $this->callByPackage($lookup[0], $lookup[1], $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. * @param array $options Additional options: * - noperms: (boolean) If true, don't check the perms. * * @return mixed Return from application call. * @throws Horde_Exception_PushApp */ public function callByPackage($app, $call, array $args = array(), array $options = 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)) { throw new Horde_Exception(sprintf('The method "%s" is not defined in the API for %s.', $call, $app)); } /* Load the API now. */ $methods = ($api_ob = $this->_loadApi($app)) ? $api_ob->methods() : array(); /* Make sure that the function actually exists. */ if (!in_array($call, $methods)) { throw new Horde_Exception('The function implementing ' . $call . ' is not defined in ' . $app . '\'s API.'); } /* 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, array( 'check_perms' => !in_array($call, $api_ob->noPerms()) && empty($options['noperms']) && $this->currentProcessAuth() )); try { $result = call_user_func_array(array($api_ob, $call), $args); if ($result instanceof PEAR_Error) { $result = new Horde_Exception_Wrapped($result); } } catch (Horde_Exception $e) { $result = $e; } /* If we changed application context in the course of this * call, undo that change now. */ if ($pushed === true) { $this->popApp(); } if ($result instanceof Horde_Exception) { throw $result; } return $result; } /** * Call a private Horde application method. * * @param string $app The application name. * @param string $call The method to call. * @param array $options Additional options: * - args: (array) Additional parameters to pass to the method. * - check_missing: (boolean) If true, throws an Exception if method * does not exist. Otherwise, will return null. * - noperms: (boolean) If true, don't check the perms. * * @return mixed Various. * * @throws Horde_Exception Application methods should throw this if there * is a fatal error. * @throws Horde_Exception_PushApp */ public function callAppMethod($app, $call, array $options = array()) { /* Load the API now. */ try { $api = $this->getApiInstance($app, 'application'); } catch (Horde_Exception $e) { if (empty($options['check_missing'])) { return null; } throw $e; } if (!method_exists($api, $call)) { if (empty($options['check_missing'])) { return null; } throw new Horde_Exception('Method does not exist.'); } /* 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, array( 'check_perms' => empty($options['noperms']) && $this->currentProcessAuth() )); try { $result = call_user_func_array(array($api, $call), empty($options['args']) ? array() : $options['args']); } catch (Horde_Exception $e) { $result = $e; } /* If we changed application context in the course of this * call, undo that change now. */ if ($pushed === true) { $this->popApp(); } if ($result instanceof Exception) { throw $e; } return $result; } /** * Returns the link corresponding to the default package that provides the * functionality requested by the $method parameter. * * @param string $method The method to link to, consisting of * "packagetype/methodname". * @param array $args Arguments to the method. * @param mixed $extra Extra, non-standard arguments to the method. * * @return string The link for that method. * @throws Horde_Exception */ public function link($method, $args = array(), $extra = '') { if (($lookup = $this->_methodLookup($method)) === false) { throw new Horde_Exception('The link "' . $method . '" is not defined in the Horde Registry.'); } return $this->linkByPackage($lookup[0], $lookup[1], $args, $extra); } /** * Returns the link 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 string The link for that method. * @throws Horde_Exception */ public function linkByPackage($app, $call, $args = array(), $extra = '') { $links = ($api_ob = $this->_loadApi($app)) ? $api_ob->links() : array(); /* Make sure the link is defined. */ if (!isset($links[$call])) { throw new Horde_Exception('The link "' . $call . '" is not defined in ' . $app . '\'s API.'); } /* Initial link value. */ $link = $links[$call]; /* Fill in html-encoded arguments. */ foreach ($args as $key => $val) { $link = str_replace('%' . $key . '%', htmlentities($val), $link); } $link = $this->applicationWebPath($link, $app); /* 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; } /** * Do a lookup of method name -> app call. * * @param string $method The method name. * * @return mixed An array containing the app and method call, or false * if not found. */ protected function _methodLookup($method) { list($interface, $call) = explode('/', $method, 2); if (!empty($this->_interfaces[$method])) { return array($this->_interfaces[$method], $call); } elseif (!empty($this->_interfaces[$interface])) { return array($this->_interfaces[$interface], $call); } return false; } /** * 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 string The application file path. * @throws Horde_Exception */ public function applicationFilePath($path, $app = null) { if (is_null($app)) { $app = $this->getApp(); } if (!isset($this->applications[$app])) { throw new Horde_Exception(sprintf(Horde_Core_Translation::t("\"%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 string The application web path. */ public function applicationWebPath($path, $app = null) { return str_replace('%application%', $this->get('webroot', $app), $path); } /** * TODO * * @param string $type The type of link. * <pre> * The following must be defined in Horde's menu config, or else they * won't be displayed in the menu: * 'help', 'problem', 'logout', 'login', 'prefs' * </pre> * * @return boolean True if the link is to be shown. */ public function showService($type) { global $conf; if (!in_array($type, array('help', 'problem', 'logout', 'login', 'prefs'))) { return true; } if (empty($conf['menu']['links'][$type])) { return false; } switch ($conf['menu']['links'][$type]) { case 'all': return true; case 'authenticated': return (bool)$this->getAuth(); default: case 'never': return false; } } /** * Returns the URL to access a Horde service. * * @param string $type The service to display: * - ajax: AJAX endpoint. * - cache: Cached data output. * - download: Download link. * - emailconfirm: E-mail confirmation page. * - go: URL redirection utility. * - help: Help page. * - imple: Imple endpoint. * - login: Login page. * - logintasks: Logintasks page. * - logout: Logout page. * - pixel: Pixel generation page. * - portal: Main portal page. * - prefs: Preferences UI. * - problem: Problem reporting page. * @param string $app The name of the current Horde application. * @param boolean $full Return a full url? @since 2.4.0 * * @return Horde_Url The link. * @throws Horde_Exception */ public function getServiceLink($type, $app = null, $full = false) { $opts = array('app' => 'horde'); switch ($type) { case 'ajax': if (is_null($app)) { $app = 'horde'; } return Horde::url('services/ajax.php/' . $app . '/', $full, $opts) ->add('token', $GLOBALS['session']->getToken()); case 'cache': $opts['append_session'] = -1; return Horde::url('services/cache.php', $full, $opts); case 'download': return Horde::url('services/download/', $full, $opts) ->add('app', $app); case 'emailconfirm': return Horde::url('services/confirm.php', $full, $opts); case 'go': return Horde::url('services/go.php', $full, $opts); case 'help': return Horde::url('services/help/', $full, $opts) ->add('module', $app); case 'imple': return Horde::url('services/imple.php', $full, $opts); case 'login': return Horde::url('login.php', $full, $opts); case 'logintasks': return Horde::url('services/logintasks.php', $full, $opts) ->add('app', $app); case 'logout': return $this->getLogoutUrl(array( 'reason' => Horde_Auth::REASON_LOGOUT )); case 'pixel': return Horde::url('services/images/pixel.php', $full, $opts); case 'prefs': if (!in_array($GLOBALS['conf']['prefs']['driver'], array('', 'none'))) { $url = Horde::url('services/prefs.php', $full, $opts); if (!is_null($app)) { $url->add('app', $app); } return $url; } break; case 'portal': return ($this->getView() == Horde_Registry::VIEW_SMARTMOBILE) ? Horde::url('services/portal/smartmobile.php', $full, $opts) : Horde::url('services/portal/', $full, $opts); break; case 'problem': return Horde::url('services/problem.php', $full, $opts) ->add( 'return_url', Horde_Util::getFormData( 'location', Horde::signUrl(Horde::selfUrl(true, true, true)) ) ); case 'sidebar': return Horde::url('services/sidebar.php', $full, $opts); case 'twitter': return Horde::url('services/twitter/', true); } throw new BadFunctionCallException('Invalid service requested: ' . print_r(debug_backtrace(false), true)); } /** * 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 array $options Additional options: * - check_perms: (boolean) Make sure that the current user has * permissions to the application being loaded. Should * ONLY be disabled by system scripts (cron jobs, etc.) * and scripts that handle login. * DEFAULT: true * - logintasks: (boolean) Perform login tasks? Only performed if * 'check_perms' is also true. System tasks are always * peformed if the user is authorized. * DEFAULT: false * - notransparent: (boolean) Do not attempt transparent authentication. * DEFAULT: false * * @return boolean Whether or not the _appStack was modified. * @throws Horde_Exception_PushApp */ public function pushApp($app, array $options = array()) { global $injector, $notification, $language, $session; if ($app == $this->getApp()) { return false; } /* Bail out if application is not present or inactive. */ if ($this->isInactive($app)) { throw new Horde_Exception_PushApp( sprintf( Horde_Core_Translation::t("%s is not activated."), $this->applications[$app]['name'] ), self::NOT_ACTIVE, $app ); } $checkPerms = ((!isset($options['check_perms']) || !empty($options['check_perms'])) && $this->currentProcessAuth()); /* 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) { $error = $error_log = null; $error_app = $this->applications[$app]['name']; $error_type = self::AUTH_FAILURE; if (($auth = $this->getAuth()) && !$this->checkExistingAuth()) { $error = '%s is not authorized %s(Remote host: %s)'; $error_app = ''; } if (!$error && !$this->hasPermission($app, Horde_Perms::READ, array('notransparent' => !empty($options['notransparent'])))) { $error = '%s is not authorized for %s (Host: %s).'; if ($this->isAuthenticated(array('app' => $app))) { $error_log = '%s does not have READ permission for %s (Host: %s)'; $error_type = self::PERMISSION_DENIED; } } if ($error) { $auth = $auth ? 'User ' . $auth : 'Guest user'; $remote = $this->remoteHost(); if ($error_log) { Horde::log( sprintf($error_log, $auth, $error_app, $remote->host), 'DEBUG' ); } throw new Horde_Exception_PushApp( sprintf($error, $auth, $error_app, $remote->host), $error_type, $app ); } } /* Push application on the stack. */ $this->_appStack[] = $app; /* 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, they have to be loaded after the * configuration, because they rely on configuration settings. So try * with the current language, and reset the language later. */ $this->setLanguageEnvironment($language, $app); /* Load config and prefs. */ $this->importConfig('horde'); $this->importConfig($app); $this->loadPrefs($app); /* Reset language, since now we can grab language from prefs. */ if (!$checkPerms && (count($this->_appStack) == 1)) { $this->setLanguageEnvironment(null, $app); } /* Run authenticated hooks, if necessary. */ $hooks = $injector->getInstance('Horde_Core_Hooks'); if ($session->get('horde', 'auth_app_init/' . $app)) { try { $error = self::INITCALLBACK_FATAL; $this->callAppMethod($app, 'authenticated'); $error = self::HOOK_FATAL; $hooks->callHook('appauthenticated', $app); } catch (Exception $e) { $this->_pushAppError($e, $error); } $session->remove('horde', 'auth_app_init/' . $app); unset($this->_appsInit[$app]); } /* Initialize application. */ if (!isset($this->_appsInit[$app])) { $notification->addAppHandler($app); try { $error = self::INITCALLBACK_FATAL; $this->callAppMethod($app, 'init'); $error = self::HOOK_FATAL; $hooks->callHook('pushapp', $app); } catch (Exception $e) { $this->_pushAppError($e, $error); } $this->_appsInit[$app] = true; } /* Do login tasks. */ if ($checkPerms && !empty($options['logintasks']) && ($tasks = $injector->getInstance('Horde_Core_Factory_LoginTasks')->create($app))) { $tasks->runTasks(); } return true; } /** * Process Exceptions thrown when pushing app on stack. * * @param Exception $e The thrown Exception. * @param integer $error The pushApp() error type. * * @throws Horde_Exception_PushApp */ protected function _pushAppError(Exception $e, $error) { if ($e instanceof Horde_Exception_HookNotSet) { return; } /* Hook errors are already logged. */ if ($error == self::INITCALLBACK_FATAL) { Horde::log($e); } $app = $this->getApp(); $this->applications[$app]['status'] = 'inactive'; $this->popApp(); throw new Horde_Exception_PushApp($e, $error, $app); } /** * 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. * @throws Horde_Exception */ 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) { /* Load config and prefs. */ $this->importConfig('horde'); $this->importConfig($app); $this->loadPrefs($app); $this->setTextdomain( $app, $this->get('fileroot', $app) . '/locale' ); } 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. * @param array $params Additional options: * - notransparent: (boolean) Do not attempt transparent authentication. * DEFAULT: false * * @return boolean Whether access is allowed. */ public function hasPermission($app, $perms = Horde_Perms::READ, array $params = array()) { /* Always do isAuthenticated() check first. You can be an admin, but * application auth != Horde admin auth. And there can *never* be * non-SHOW access to an application that requires authentication. */ if (!$this->isAuthenticated(array('app' => $app, 'notransparent' => !empty($params['notransparent']))) && $GLOBALS['injector']->getInstance('Horde_Core_Factory_Auth')->create($app)->requireAuth() && ($perms != Horde_Perms::SHOW)) { return false; } /* Otherwise, allow access for admins, for apps that do not have any * explicit permissions, or for apps that allow the given permission. */ return $this->isAdmin() || ($GLOBALS['injector']->getInstance('Horde_Perms')->exists($app) ? $GLOBALS['injector']->getInstance('Horde_Perms')->hasPermission($app, $this->getAuth(), $perms) : (bool)$this->getAuth()); } /** * Reads the configuration values for the given application and imports * them into the global $conf variable. * * @param string $app The application name. */ public function importConfig($app) { /* Make sure Horde is always loaded. */ if (!isset($this->_cache['conf']['horde'])) { $this->_cache['conf']['horde'] = new Horde_Registry_Hordeconfig(array('app' => 'horde')); } if (!isset($this->_cache['conf'][$app])) { $this->_cache['conf'][$app] = new Horde_Registry_Hordeconfig_Merged(array( 'aconfig' => new Horde_Registry_Hordeconfig(array('app' => $app)), 'hconfig' => $this->_cache['conf']['horde'] )); } $GLOBALS['conf'] = $this->_cache['conf'][$app]->toArray(); } /** * Loads the preferences for the current user for the current application * and imports them into the global $prefs variable. * $app will be the active application after calling this function. * * @param string $app The name of the application. * * @throws Horde_Exception */ public function loadPrefs($app = null) { global $injector, $prefs; if (strlen($app)) { $this->pushApp($app); } elseif (($app = $this->getApp()) === false) { $app = 'horde'; } $user = $this->getAuth(); if ($user) { if (isset($prefs) && ($prefs->getUser() == $user)) { $prefs->changeScope($app); return; } $opts = array( 'user' => $user ); } else { /* If there is no logged in user, return an empty Horde_Prefs * object with just default preferences. */ $opts = array( 'driver' => 'Horde_Prefs_Storage_Null' ); } $prefs = $injector->getInstance('Horde_Core_Factory_Prefs')->create($app, $opts); } /** * Load a configuration file from a Horde application's config directory. * This call is cached (a config file is only loaded once, regardless of * the $vars value). * * @since 2.12.0 * * @param string $conf_file Configuration file name. * @param mixed $vars List of config variables to load. * @param string $app Application. * * @return Horde_Registry_Loadconfig The config object. * @throws Horde_Exception */ public function loadConfigFile($conf_file, $vars = null, $app = null) { if (is_null($app)) { $app = $this->getApp(); } if (!isset($this->_cache['cfile'][$app][$conf_file])) { $this->_cache['cfile'][$app][$conf_file] = new Horde_Registry_Loadconfig( $app, $conf_file, $vars ); } return $this->_cache['cfile'][$app][$conf_file]; } /** * 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 { switch ($parameter) { case 'icon': $pval = Horde_Themes::img($app . '.png', $app); if ((string)$pval == '') { $pval = Horde_Themes::img('app-unknown.png', 'horde'); } break; case 'initial_page': $pval = null; break; default: $pval = isset($this->applications['horde'][$parameter]) ? $this->applications['horde'][$parameter] : null; break; } } return ($parameter == 'name') ? (strlen($pval) ? _($pval) : '') : $pval; } /** * Return the version string for a given application. * * @param string $app The application to get the value for. * @param boolean $number Return the raw version number, suitable for * comparison purposes. * * @return string The version string for the application. */ public function getVersion($app = null, $number = false) { if (empty($app)) { $app = $this->getApp(); } try { $api = $this->getApiInstance($app, 'application'); } catch (Horde_Exception $e) { return 'unknown'; } return $number ? preg_replace('/H\d \((.*)\)/', '$1', $api->version) : $api->version; } /** * Does the application have the queried feature? * * @param string $id Feature ID. * @param string $app The application to check (defaults to current app). * * @return boolean True if the application has the feature. */ public function hasFeature($id, $app = null) { if (empty($app)) { $app = $this->getApp(); } try { $api = $this->getApiInstance($app, 'application'); } catch (Horde_Exception $e) { return false; } return !empty($api->features[$id]); } /** * Does the given application have the queried view? * * @param integer $view The view type (VIEW_* constant). * @param string $app The application to check (defaults to current * app). * * @return boolean True if the view is available in the application. */ public function hasView($view, $app = null) { switch ($view) { case self::VIEW_BASIC: // For now, consider all apps to have BASIC view. return true; case self::VIEW_DYNAMIC: return $this->hasFeature('dynamicView', $app); case self::VIEW_MINIMAL: return $this->hasFeature('minimalView', $app); case self::VIEW_SMARTMOBILE: return $this->hasFeature('smartmobileView', $app); } } /** * Set current view. * * @param integer $view The view type. */ public function setView($view = self::VIEW_BASIC) { $GLOBALS['session']->set('horde', 'view', $view); } /** * Get current view. * * @return integer The view type. */ public function getView() { global $session; return $session->exists('horde', 'view') ? $session->get('horde', 'view') : self::VIEW_BASIC; } /** * Returns a list of available drivers for a library that are available * in an application. * * @param string $app The application name. * @param string $prefix The library prefix. * * @return array The list of available class names. */ public function getAppDrivers($app, $prefix) { $classes = array(); $fileprefix = strtr($prefix, '_', '/'); $fileroot = $this->get('fileroot', $app); if (!is_null($fileroot)) { try { $pushed = $this->pushApp($app); } catch (Horde_Exception $e) { if ($e->getCode() == Horde_Registry::AUTH_FAILURE) { return array(); } throw $e; } if (is_dir($fileroot . '/lib/' . $fileprefix)) { try { $di = new DirectoryIterator($fileroot . '/lib/' . $fileprefix); foreach ($di as $val) { if (!$val->isDir() && !$di->isDot()) { $class = $app . '_' . $prefix . '_' . basename($val, '.php'); if (class_exists($class)) { $classes[] = $class; } } } } catch (UnexpectedValueException $e) {} } if ($pushed) { $this->popApp(); } } return $classes; } /** * 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 initial page of the application. * @throws Horde_Exception */ public function getInitialPage($app = null) { try { if (($url = $this->callAppMethod($app, 'getInitialPage')) !== null) { return $url; } } catch (Horde_Exception $e) {} if (($webroot = $this->get('webroot', $app)) !== null) { return $webroot . '/' . strval($this->get('initial_page', $app)); } throw new Horde_Exception(sprintf( Horde_Core_Translation::t("\"%s\" is not configured in the Horde Registry."), is_null($app) ? $this->getApp() : $app )); } /** * Clears any authentication tokens in the current session. * * @param boolean $destroy Destroy the session? */ public function clearAuth($destroy = true) { global $session; /* Do application logout tasks. */ /* @todo: Replace with exclusively registered logout tasks. */ foreach ($this->getAuthApps() as $app) { try { $this->callAppMethod($app, 'logout'); } catch (Horde_Exception $e) {} } /* Do registered logout tasks. */ $logout = new Horde_Registry_Logout(); $logout->run(); // @suspicious shouldn't this be 'auth/' $session->remove('horde', 'auth'); $session->remove('horde', 'auth_app/'); $this->_cache['auth'] = null; $this->_cache['existing'] = $this->_cache['isauth'] = array(); if ($destroy) { $session->destroy(); } } /** * Clears authentication tokens for a given application in the current * session. * * @return boolean If false, did not remove authentication token because * the application is in control of Horde's auth. */ public function clearAuthApp($app) { global $session; if ($session->get('horde', 'auth/credentials') == $app) { return false; } if ($this->isAuthenticated(array('app' => $app, 'notransparent' => true))) { $this->callAppMethod($app, 'logout'); $session->remove($app); $session->remove('horde', 'auth_app/' . $app); $session->remove('horde', 'auth_app_init/' . $app); } unset( $this->_cache['existing'][$app], $this->_cache['isauth'][$app] ); return true; } /** * Is a user an administrator? * * @param array $options Options: * - permission: (string) Allow users with this permission admin access * in the current context. * - permlevel: (integer) The level of permissions to check for. * Defaults to Horde_Perms::EDIT. * - user: (string) The user to check. * Defaults to self::getAuth(). * * @return boolean Whether or not this is an admin user. */ public function isAdmin(array $options = array()) { $user = isset($options['user']) ? $options['user'] : $this->getAuth(); if ($user && @is_array($GLOBALS['conf']['auth']['admins']) && in_array($user, $GLOBALS['conf']['auth']['admins'])) { return true; } return isset($options['permission']) ? $GLOBALS['injector']->getInstance('Horde_Perms')->hasPermission($options['permission'], $user, isset($options['permlevel']) ? $options['permlevel'] : Horde_Perms::EDIT) : false; } /** * Checks if there is a session with valid auth information. If there * isn't, but the configured Auth driver supports transparent * authentication, then we try that. * * @param array $opts Additional options: * - app: (string) Check authentication for this app. * DEFAULT: Checks horde-wide authentication. * - notransparent: (boolean) Do not attempt transparent authentication. * DEFAULT: false * * @return boolean Whether or not the user is authenticated. */ public function isAuthenticated(array $opts = array()) { global $injector, $session; $app = empty($opts['app']) ? 'horde' : $opts['app']; $transparent = intval(empty($opts['notransparent'])); if (isset($this->_cache['isauth'][$app][$transparent])) { return $this->_cache['isauth'][$app][$transparent]; } /* Check for cached authentication results. */ if ($this->getAuth() && (($app == 'horde') || $session->exists('horde', 'auth_app/' . $app)) && $this->checkExistingAuth($app)) { $res = true; } elseif ($transparent) { try { $res = $injector->getInstance('Horde_Core_Factory_Auth')->create($app)->transparent(); } catch (Horde_Exception $e) { Horde::log($e); $res = false; } } else { $res = false; } $this->_cache['isauth'][$app][$transparent] = $res; return $res; } /** * Checks whether this process required authentication. * * @since 2.11.0 * * @return boolean True if the current process required authentication. */ public function currentProcessAuth() { return ($this->_args['authentication'] !== 'none'); } /** * Returns a URL to the login screen, adding the necessary logout * parameters. * * If no reason/msg is passed in, uses the current global authentication * error message. * * @param array $options Additional options: * - app: (string) Authenticate to this application * DEFAULT: Horde * - msg: (string) If reason is Horde_Auth::REASON_MESSAGE, the message * to display to the user. * DEFAULT: None * - params: (array) Additional params to add to the URL (not allowed: * 'app', 'horde_logout_token', 'msg', 'reason', 'url'). * DEFAULT: None * - reason: (integer) The reason for logout * DEFAULT: None * * @return Horde_Url The formatted URL. */ public function getLogoutUrl(array $options = array()) { if (!isset($options['reason'])) { // TODO: This only returns the error for Horde-wide // authentication, not for application auth. $options['reason'] = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Auth')->create()->getError(); } $params = array(); if (!in_array($options['reason'], array(Horde_Auth::REASON_LOGOUT, Horde_Auth::REASON_MESSAGE))) { $params['url'] = Horde::signUrl(Horde::selfUrl(true, true, true)); } if (empty($options['app']) || ($options['app'] == 'horde') || ($options['reason'] == Horde_Auth::REASON_LOGOUT)) { $params['horde_logout_token'] = $GLOBALS['session']->getToken(); } if (isset($options['app'])) { $params['app'] = $options['app']; } if ($options['reason']) { $params['logout_reason'] = $options['reason']; if ($options['reason'] == Horde_Auth::REASON_MESSAGE) { $params['logout_msg'] = empty($options['msg']) ? $GLOBALS['injector']->getInstance('Horde_Core_Factory_Auth')->create()->getError(true) : $options['msg']; } } return $this->getServiceLink('login', 'horde')->add($params)->setRaw(true); } /** * Returns a URL to be used for downloading data. * * @param string $filename The filename of the download data. * @param array $params Additional URL parameters needed. * * @return Horde_Url The download URL. This URL should be used as-is, * since the filename MUST be the last parameter added * to the URL. */ public function downloadUrl($filename, array $params = array()) { $url = $this->getServiceLink('download', $this->getApp()) /* Add parameters. */ ->add($params); if (strlen($filename)) { /* Add the filename to the end of the URL. Although not necessary * for many browsers, this should allow every browser to download * correctly. */ $url->add('fn', '/' . $filename); } return $url; } /** * Converts an authentication username to a unique Horde username. * * @param string $userId The username to convert. * @param boolean $toHorde If true, convert to a Horde username. If * false, convert to the auth username. * * @return string The converted username. * @throws Horde_Exception */ public function convertUsername($userId, $toHorde) { try { return $GLOBALS['injector']->getInstance('Horde_Core_Hooks')-> callHook('authusername', 'horde', array($userId, $toHorde)); } catch (Horde_Exception_HookNotSet $e) { return $userId; } } /** * Returns the currently logged in user, if there is one. * * @param string $format The return format, defaults to the unique Horde * ID. Alternative formats: * - bare: (string) Horde ID without any domain information. * EXAMPLE: foo@example.com would be returned as 'foo'. * - domain: (string) Domain of the Horde ID. * EXAMPLE: foo@example.com would be returned as 'example.com'. * - original: (string) The username used to originally login to Horde. * * @return mixed The user ID or false if no user is logged in. */ public function getAuth($format = null) { global $session; if (is_null($format) && !is_null($this->_cache['auth'])) { return $this->_cache['auth']; } if (!isset($session)) { return false; } if ($format == 'original') { return $session->exists('horde', 'auth/authId') ? $session->get('horde', 'auth/authId') : false; } $user = $session->get('horde', 'auth/userId'); if (is_null($user)) { return false; } switch ($format) { case 'bare': return (($pos = strpos($user, '@')) === false) ? $user : substr($user, 0, $pos); case 'domain': return (($pos = strpos($user, '@')) === false) ? false : substr($user, $pos + 1); default: /* Specifically cache this result, since it generally is called * many times in a page. */ $this->_cache['auth'] = $user; return $user; } } /** * Return whether the authentication backend requested a password change. * * @return boolean Whether the backend requested a password change. */ public function passwordChangeRequested() { return (bool)$GLOBALS['session']->get('horde', 'auth/change'); } /** * Returns the requested credential for the currently logged in user, if * present. * * @param string $credential The credential to retrieve. * @param string $app The app to query. Defaults to Horde. * * @return mixed The requested credential, all credentials if $credential * is null, or false if no user is logged in. */ public function getAuthCredential($credential = null, $app = null) { if (!$this->getAuth()) { return false; } $credentials = $this->_getAuthCredentials($app); return is_null($credential) ? $credentials : ((is_array($credentials) && isset($credentials[$credential])) ? $credentials[$credential] : false); } /** * Sets the requested credential for the currently logged in user. * * @param mixed $credential The credential to set. If an array, * overwrites the current credentials array. * @param string $value The value to set the credential to. If * $credential is an array, this value is * ignored. * @param string $app The app to update. Defaults to Horde. */ public function setAuthCredential($credential, $value = null, $app = null) { global $session; if (!$this->getAuth()) { return; } if (is_array($credential)) { $credentials = $credential; } else { if (($credentials = $this->_getAuthCredentials($app)) === false) { return; } if (!is_array($credentials)) { $credentials = array(); } $credentials[$credential] = $value; } $entry = $credentials; if (($base_app = $session->get('horde', 'auth/credentials')) && ($session->get('horde', 'auth_app/' . $base_app) == $entry)) { $entry = true; } if (is_null($app)) { $app = $base_app; } /* The auth_app key contains application-specific authentication. * Session subkeys are the app names, values are an array containing * credentials. If the value is true, application does not require any * specific credentials. */ $session->set('horde', 'auth_app/' . $app, $entry, $session::ENCRYPT); $session->set('horde', 'auth_app_init/' . $app, true); unset( $this->_cache['existing'][$app], $this->_cache['isauth'][$app] ); } /** * Get the list of credentials for a given app. * * @param string $app The application name. * * @return mixed True, false, or the credential list. */ protected function _getAuthCredentials($app) { global $session; $base_app = $session->get('horde', 'auth/credentials'); if (is_null($base_app)) { return false; } if (is_null($app)) { $app = $base_app; } if (!$session->exists('horde', 'auth_app/' . $app)) { return ($base_app != $app) ? $this->_getAuthCredentials($base_app) : false; } return $session->get('horde', 'auth_app/' . $app); } /** * Returns information about the remote host. * * @since 2.17.0 * * @return object An object with the following properties: * <pre> * - addr: (string) Remote IP address. * - host: (string) Remote hostname (if resolvable; otherwise, this value * is identical to 'addr'). * - proxy: (boolean) True if this user is connecting through a proxy. * </pre> */ public function remoteHost() { global $injector; $out = new stdClass; $dns = $injector->getInstance('Net_DNS2_Resolver'); $old_error = error_reporting(0); if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $remote_path = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); $out->addr = $remote_path[0]; $out->proxy = true; } else { $out->addr = $_SERVER['REMOTE_ADDR']; if (!empty($_SERVER['REMOTE_HOST'])) { $out->host = $_SERVER['REMOTE_HOST']; } $out->proxy = false; } if ($dns && !isset($out->host)) { $out->host = $out->addr; try { if ($response = $dns->query($out->addr, 'PTR')) { foreach ($response->answer as $val) { if (isset($val->ptrdname)) { $out->host = $val->ptrdname; break; } } } } catch (Net_DNS2_Exception $e) {} } elseif (!isset($out->host)) { $out->host = gethostbyaddr($out->addr); } error_reporting($old_error); return $out; } /** * Sets data in the session saying that authorization has succeeded, * note which userId was authorized, and note when the login took place. * * If a user name hook was defined in the configuration, it gets applied * to the $userId at this point. * * @param string $authId The userId that has been authorized. * @param array $credentials The credentials of the user. * @param array $options Additional options: * - app: (string) The app to set authentication credentials for. * DEFAULT: 'horde' * - change: (boolean) Whether to request that the user change their * password. * DEFAULT: No * - language: (string) The preferred language. * DEFAULT: null * - no_convert: (boolean) Don't convert the user name with the * authusername hook. * DEFAULT: false */ public function setAuth($authId, $credentials, array $options = array()) { global $browser, $injector, $session; $app = empty($options['app']) ? 'horde' : $options['app']; if ($this->getAuth() == $authId) { /* Store app credentials - base Horde session already exists. */ $this->setAuthCredential($credentials, null, $app); return; } /* Initial authentication to Horde. */ $session->set('horde', 'auth/authId', $authId); $session->set('horde', 'auth/browser', $browser->getAgentString()); if (!empty($options['change'])) { $session->set('horde', 'auth/change', 1); } $session->set('horde', 'auth/credentials', $app); $remote = $this->remoteHost(); $session->set('horde', 'auth/remoteAddr', $remote->addr); $session->set('horde', 'auth/timestamp', time()); $username = trim($authId); if (!empty($GLOBALS['conf']['auth']['lowercase'])) { $username = Horde_String::lower($username); } if (empty($options['no_convert'])) { $username = $this->convertUsername($username, true); } $session->set('horde', 'auth/userId', $username); $this->_cache['auth'] = null; $this->_cache['existing'] = $this->_cache['isauth'] = array(); $this->setAuthCredential($credentials, null, $app); /* Reload preferences for the new user. */ unset($GLOBALS['prefs']); $this->loadPrefs($this->getApp()); $this->setLanguageEnvironment(isset($options['language']) ? $this->preferredLang($options['language']) : null, $app); } /** * Check existing auth for triggers that might invalidate it. * * @param string $app Check authentication for this app too. * * @return boolean Is existing auth valid? */ public function checkExistingAuth($app = 'horde') { global $browser, $conf, $injector, $session; if (!empty($this->_cache['existing'][$app])) { return true; } /* Tasks that only need to run once. */ if (empty($this->_cache['existing'])) { if (!empty($conf['auth']['checkip']) && ($remoteaddr = $session->get('horde', 'auth/remoteAddr')) && ($remoteob = $this->remoteHost()) && ($remoteaddr != $remoteob->addr)) { $injector->getInstance('Horde_Core_Factory_Auth')->create() ->setError(Horde_Core_Auth_Application::REASON_SESSIONIP); return false; } if (!empty($conf['auth']['checkbrowser']) && ($session->get('horde', 'auth/browser') != $browser->getAgentString())) { $injector->getInstance('Horde_Core_Factory_Auth')->create() ->setError(Horde_Core_Auth_Application::REASON_BROWSER); return false; } if (!empty($conf['session']['max_time']) && (($conf['session']['max_time'] + $session->begin) < time())) { $injector->getInstance('Horde_Core_Factory_Auth')->create() ->setError(Horde_Core_Auth_Application::REASON_SESSIONMAXTIME); return false; } } foreach (array_unique(array('horde', $app)) as $val) { if (!isset($this->_cache['existing'][$val])) { $auth = $injector->getInstance('Horde_Core_Factory_Auth')->create($val); if (!$auth->validateAuth()) { if (!$auth->getError()) { $auth->setError(Horde_Auth::REASON_SESSION); } return false; } $this->_cache['existing'][$val] = true; } } return true; } /** * Removes a user from the authentication backend and calls all * applications' removeUserData API methods. * * @param string $userId The userId to delete. * * @throws Horde_Exception */ public function removeUser($userId) { $GLOBALS['injector'] ->getInstance('Horde_Core_Factory_Auth') ->create() ->removeUser($userId); $this->removeUserData($userId); } /** * Removes user's application data. * * @param string $user The user ID to delete. * @param string $app If set, only removes data from this application. * By default, removes data from all apps. * * @throws Horde_Exception */ public function removeUserData($user, $app = null) { if (!$this->isAdmin() && ($user != $this->getAuth())) { throw new Horde_Exception(Horde_Core_Translation::t("You are not allowed to remove user data.")); } $applist = empty($app) ? $this->listApps( array('notoolbar', 'hidden', 'active', 'admin', 'noadmin') ) : array($app); $errApps = array(); if (!empty($applist)) { $prefs_ob = $GLOBALS['injector'] ->getInstance('Horde_Core_Factory_Prefs') ->create('horde', array('user' => $user)); // Remove all preference at once, if possible. if (empty($app)) { try { $prefs_ob->removeAll(); } catch (Horde_Exception $e) { Horde::log($e); } } } foreach ($applist as $item) { try { $this->callAppMethod($item, 'removeUserData', array( 'args' => array($user) )); } catch (Exception $e) { Horde::log($e); $errApps[] = $item; } if (empty($app)) { continue; } try { $prefs_ob->retrieve($item); $prefs_ob->remove(); } catch (Horde_Exception $e) { Horde::log($e); $errApps[] = $item; } } if (count($errApps)) { throw new Horde_Exception(sprintf(Horde_Core_Translation::t("The following applications encountered errors removing user data: %s"), implode(', ', array_unique($errApps)))); } } /** * Returns authentication metadata information. * * @since 2.12.0 * * @return array Authentication metadata: * - authId: (string) The username used during the original auth. * - browser: (string) The remote browser string. * - change: (boolean) Is a password change requested? * - credentials: (string) The 'auth_app' entry that contains the Horde * credentials. * - remoteAddr: (string) The remote IP address of the user. * - timestamp: (integer) The login time. * - userId: (string) The unique Horde username. */ public function getAuthInfo() { global $session; return $session->get('horde', 'auth/', $session::TYPE_ARRAY); } /** * Returns the list of applications currently authenticated to. * * @since 2.12.0 * * @return array List of authenticated applications. */ public function getAuthApps() { global $session; return array_keys( $session->get('horde', 'auth_app/', $session::TYPE_ARRAY) ); } /* NLS functions. */ /** * Returns the charset for the current language. * * @return string The character set that should be used with the current * locale settings. */ public function getLanguageCharset() { return ($charset = $this->nlsconfig->curr_charset) ? $charset : 'ISO-8859-1'; } /** * Returns the charset to use for outgoing emails. * * @return string The preferred charset for outgoing mails based on * the user's preferences and the current language. */ public function getEmailCharset() { if (isset($GLOBALS['prefs']) && ($charset = $GLOBALS['prefs']->getValue('sending_charset'))) { return $charset; } return ($charset = $this->nlsconfig->curr_emails) ? $charset : $this->getLanguageCharset(); } /** * Selects the most preferred language for the current client session. * * @param string $lang Force to use this language. * * @return string The selected language abbreviation. */ public function preferredLang($lang = null) { /* Check if we have a language set in the session */ if ($GLOBALS['session']->exists('horde', 'language')) { return basename($GLOBALS['session']->get('horde', 'language')); } /* If language pref exists, we should use that. */ if (isset($GLOBALS['prefs']) && ($language = $GLOBALS['prefs']->getValue('language'))) { return basename($language); } /* Check if the user selected a language from the login screen */ if (!empty($lang) && $this->nlsconfig->validLang($lang)) { return basename($lang); } /* Try browser-accepted languages. */ if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { /* The browser supplies a list, so return the first valid one. */ $browser_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']); foreach ($browser_langs as $lang) { /* Strip quality value for language */ if (($pos = strpos($lang, ';')) !== false) { $lang = substr($lang, 0, $pos); } $lang = $this->_mapLang(trim($lang)); if ($this->nlsconfig->validLang($lang)) { return basename($lang); } /* In case there's no full match, save our best guess. Try * ll_LL, followed by just ll. */ if (!isset($partial_lang)) { $ll_LL = Horde_String::lower(substr($lang, 0, 2)) . '_' . Horde_String::upper(substr($lang, 0, 2)); if ($this->nlsconfig->validLang($ll_LL)) { $partial_lang = $ll_LL; } else { $ll = $this->_mapLang(substr($lang, 0, 2)); if ($this->nlsconfig->validLang($ll)) { $partial_lang = $ll; } } } } if (isset($partial_lang)) { return basename($partial_lang); } } /* Use site-wide default, if one is defined */ return $this->nlsconfig->curr_default ? basename($this->nlsconfig->curr_default) /* No dice auto-detecting, default to US English. */ : 'en_US'; } /** * Sets the language. * * @param string $lang The language abbreviation. * * @return string The current language (since 2.5.0). * * @throws Horde_Exception */ public function setLanguage($lang = null) { if (empty($lang) || !$this->nlsconfig->validLang($lang)) { $lang = $this->preferredLang(); } $GLOBALS['session']->set('horde', 'language', $lang); $changed = false; if (isset($GLOBALS['language'])) { if ($GLOBALS['language'] == $lang) { return $lang; } $changed = true; } $GLOBALS['language'] = $lang; $lang_charset = $lang . '.UTF-8'; if (setlocale(LC_ALL, $lang_charset)) { putenv('LC_ALL=' . $lang_charset); putenv('LANG=' . $lang_charset); putenv('LANGUAGE=' . $lang_charset); } else { $changed = false; } if ($changed) { $this->rebuild(); $this->_cache['cfile'] = array(); foreach ($this->listApps() as $app) { if ($this->isAuthenticated(array('app' => $app, 'notransparent' => true))) { $this->callAppMethod($app, 'changeLanguage'); } } } return $lang; } /** * Sets the language and reloads the whole NLS environment. * * When setting the language, the gettext catalogs have to be reloaded * too, charsets have to be updated etc. This method takes care of all * this. * * @param string $lang The new language. * @param string $app The application for reloading the gettext catalog. * Uses current application if null. */ public function setLanguageEnvironment($lang = null, $app = null) { if (empty($app)) { $app = $this->getApp(); } $old_lang = $this->setLanguage($lang); $this->setTextdomain( $app, $this->get('fileroot', $app) . '/locale' ); if ($old_lang == $GLOBALS['language']) { return; } $GLOBALS['session']->remove('horde', 'nls/'); } /** * Sets the gettext domain. * * @param string $app The application name. * @param string $directory The directory where the application's * LC_MESSAGES directory resides. */ public function setTextdomain($app, $directory) { bindtextdomain($app, $directory); textdomain($app); /* The existence of this function depends on the platform. */ if (function_exists('bind_textdomain_codeset')) { bind_textdomain_codeset($app, 'UTF-8'); } } /** * Sets the current timezone, if available. */ public function setTimeZone() { $tz = $GLOBALS['prefs']->getValue('timezone'); if (!empty($tz)) { @date_default_timezone_set($tz); } } /** * Maps languages with common two-letter codes (such as nl) to the full * locale code (in this case, nl_NL). Returns the language unmodified if * it isn't an alias. * * @param string $language The language code to map. * * @return string The mapped language code. */ protected function _mapLang($language) { // Translate the $language to get broader matches. // (eg. de-DE should match de_DE) $trans_lang = str_replace('-', '_', $language); $lang_parts = explode('_', $trans_lang); $trans_lang = Horde_String::lower($lang_parts[0]); if (isset($lang_parts[1])) { $trans_lang .= '_' . Horde_String::upper($lang_parts[1]); } return empty($this->nlsconfig->aliases[$trans_lang]) ? $trans_lang : $this->nlsconfig->aliases[$trans_lang]; } /** * Is the registry in 'test' mode? * * @since 2.12.0 * * @return boolean True if in testing mode. */ public function isTest() { return !empty($this->_args['test']); } }
Simpan