1<?php
2/**
3 * Copyright 1999-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (LGPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
7 *
8 * @category  Horde
9 * @copyright 1999-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
11 * @package   Core
12 */
13
14/**
15 * The registry provides a set of methods for communication between Horde
16 * applications and keeping track of application configuration information.
17 *
18 * @author    Chuck Hagenbuch <chuck@horde.org>
19 * @author    Jon Parise <jon@horde.org>
20 * @author    Anil Madhavapeddy <anil@recoil.org>
21 * @author    Michael Slusarz <slusarz@horde.org>
22 * @category  Horde
23 * @copyright 1999-2017 Horde LLC
24 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
25 * @package   Core
26 */
27class Horde_Registry implements Horde_Shutdown_Task
28{
29    /* Session flags. */
30    const SESSION_NONE = 1;
31    const SESSION_READONLY = 2;
32
33    /* Error codes for pushApp(). */
34    const AUTH_FAILURE = 1;
35    const NOT_ACTIVE = 2;
36    const PERMISSION_DENIED = 3;
37    const HOOK_FATAL = 4;
38    const INITCALLBACK_FATAL = 5;
39
40    /* View types. */
41    const VIEW_BASIC = 1;
42    const VIEW_DYNAMIC = 2;
43    const VIEW_MINIMAL = 3;
44    const VIEW_SMARTMOBILE = 4;
45
46    /* Session keys. */
47    const REGISTRY_CACHE = 'registry_cache';
48
49    /**
50     * Hash storing information on each registry-aware application.
51     *
52     * @var array
53     */
54    public $applications = array();
55
56    /**
57     * Original authentication exception. Set if 'fallback' auth is used, and
58     * authentication fails.
59     *
60     * @since 2.11.0
61     * @todo Fix this up for H6 (framework needs to do better job of
62     *       supporting bootstrapping before authentication).
63     *
64     * @var Exception
65     */
66    public $authException;
67
68    /**
69     * A flag that is set once the basic horde application has been
70     * minimally configured.
71     *
72     * @var boolean
73     */
74    public $hordeInit = false;
75
76    /**
77     * The application that called appInit().
78     *
79     * @var string
80     */
81    public $initialApp;
82
83    /**
84     * NLS configuration.
85     *
86     * @var Horde_Registry_Nlsconfig
87     */
88    public $nlsconfig;
89
90    /**
91     * The current virtual host configuration file.
92     *
93     * @since 2.12.0
94     *
95     * @var string
96     */
97    public $vhost = null;
98
99    /**
100     * The list of APIs.
101     *
102     * @var array
103     */
104    protected $_apiList = array();
105
106    /**
107     * Stack of in-use applications.
108     *
109     * @var array
110     */
111    protected $_appStack = array();
112
113    /**
114     * The list of applications initialized during this access.
115     *
116     * @var array
117     */
118    protected $_appsInit = array();
119
120    /**
121     * The arguments that have been passed when instantiating the registry.
122     *
123     * @var array
124     */
125    protected $_args = array();
126
127    /**
128     * Internal cached data.
129     *
130     * @var array
131     */
132    protected $_cache = array(
133        'auth' => null,
134        'cfile' => array(),
135        'conf' => array(),
136        'existing' => array(),
137        'ob' => array()
138    );
139
140    /**
141     * Interfaces list.
142     *
143     * @var array
144     */
145    protected $_interfaces = array();
146
147    /**
148     * The last modified time of the newest modified registry file.
149     *
150     * @var integer
151     */
152    protected $_regmtime;
153
154    /**
155     * Application bootstrap initialization.
156     * Solves chicken-and-egg problem - need a way to init Horde environment
157     * from application without an active Horde_Registry object.
158     *
159     * Page compression will be started (if configured).
160     *
161     * Global variables defined:
162     * <pre>
163     *   - $browser: Horde_Browser object
164     *   - $cli: Horde_Cli object (if 'cli' is true)
165     *   - $conf: Configuration array
166     *   - $injector: Horde_Injector object
167     *   - $language: Language
168     *   - $notification: Horde_Notification object
169     *   - $page_output: Horde_PageOutput object
170     *   - $prefs: Horde_Prefs object
171     *   - $registry: Horde_Registry object
172     *   - $session: Horde_Session object
173     * </pre>
174     *
175     * @param string $app  The application to initialize.
176     * @param array $args  Optional arguments:
177     * <pre>
178     *   - admin: (boolean) Require authenticated user to be an admin?
179     *            DEFAULT: false
180     *   - authentication: (string) The type of authentication to use:
181     *     - none: Do not authenticate
182     *     - fallback: Attempt to authenticate; if failure, then don't auth
183     *                 (@since 2.11.0).
184     *     - [DEFAULT]: Authenticate; on no auth redirect to login screen
185     *   - cli: (boolean) Initialize a CLI interface. Setting this to true
186     *          implicitly sets 'authentication' to 'none' and 'admin' and
187     *          'nocompress' to true.
188     *          DEFAULT: false
189     *   - nocompress: (boolean) If set, the page will not be compressed.
190     *                 DEFAULT: false
191     *   - nologintasks: (boolean) If set, don't perform logintasks (never
192     *                   performed if authentication is 'none').
193     *                   DEFAULT: false
194     *   - nonotificationinit: (boolean) If set, don't initialize the
195     *                         application handlers for the notification
196     *                         system (@since 2.12.0).
197     *   - permission: (array) The permission required by the user to access
198     *                 the page. The first element (REQUIRED) is the permission
199     *                 name. The second element (OPTION; defaults to SHOW) is
200     *                 the permission level.
201     *   - session_cache_limiter: (string) Use this value for the session
202     *                            cache limiter.
203     *                            DEFAULT: Uses the value in the config.
204     *   - session_control: (string) Special session control limitations:
205     *     - netscape: TODO; start read/write session
206     *     - none: Do not start a session
207     *     - readonly: Start session readonly
208     *     - [DEFAULT] - Start read/write session
209     *   - test: (boolean) Is this the test script? If so, we relax several
210     *           sanity checks and don't load things from the cache.
211     *           DEFAULT: false
212     *   - timezone: (boolean) Set the time zone?
213     *               DEFAULT: false
214     *   - user_admin: (boolean) Set authentication to an admin user?
215     *                 DEFAULT: false
216     * </pre>
217     *
218     * @return Horde_Registry_Application  The application object.
219     * @throws Horde_Exception
220     */
221    public static function appInit($app, array $args = array())
222    {
223        if (isset($GLOBALS['registry'])) {
224            return $GLOBALS['registry']->getApiInstance($app, 'application');
225        }
226
227        $args = array_merge(array(
228            'admin' => false,
229            'authentication' => null,
230            'cli' => null,
231            'nocompress' => false,
232            'nologintasks' => false,
233            'nonotificationinit' => false,
234            'permission' => false,
235            'session_cache_limiter' => null,
236            'session_control' => null,
237            'timezone' => false,
238            'user_admin' => null,
239        ), $args);
240
241        /* CLI initialization. */
242        if ($args['cli']) {
243            /* Make sure no one runs from the web. */
244            if (!Horde_Cli::runningFromCLI()) {
245                throw new Horde_Exception(Horde_Core_Translation::t("Script must be run from the command line"));
246            }
247
248            /* Load the CLI environment - make sure there's no time limit,
249             * init some variables, etc. */
250            $GLOBALS['cli'] = Horde_Cli::init();
251
252            $args['nocompress'] = true;
253            $args['authentication'] = 'none';
254        }
255
256        // For 'fallback' authentication, try authentication first.
257        if ($args['authentication'] === 'fallback') {
258            $fallback_auth = true;
259            $args['authentication'] = null;
260        } else {
261            $fallback_auth = false;
262        }
263
264        // Registry.
265        $s_ctrl = 0;
266        switch ($args['session_control']) {
267        case 'netscape':
268            // Chicken/egg: Browser object doesn't exist yet.
269            // Can't use Horde_Core_Browser since it depends on registry to be
270            // configured.
271            $browser = new Horde_Browser();
272            if ($browser->isBrowser('mozilla')) {
273                $args['session_cache_limiter'] = 'private, must-revalidate';
274            }
275            break;
276
277        case 'none':
278            $s_ctrl = self::SESSION_NONE;
279            break;
280
281        case 'readonly':
282            $s_ctrl = self::SESSION_READONLY;
283            break;
284        }
285
286        $classname = __CLASS__;
287        $registry = $GLOBALS['registry'] = new $classname($s_ctrl, $args);
288        $registry->initialApp = $app;
289
290        $appob = $registry->getApiInstance($app, 'application');
291        $appob->initParams = $args;
292
293        do {
294            try {
295                $registry->pushApp($app, array(
296                    'check_perms' => ($args['authentication'] != 'none'),
297                    'logintasks' => !$args['nologintasks'],
298                    'notransparent' => !empty($args['notransparent'])
299                ));
300
301                if ($args['admin'] && !$registry->isAdmin()) {
302                    throw new Horde_Exception(Horde_Core_Translation::t("Not an admin"));
303                }
304
305                $e = null;
306            } catch (Horde_Exception_PushApp $e) {
307                if ($fallback_auth) {
308                    $registry->authException = $e;
309                    $registry->setAuthenticationSetting('none');
310                    $args['authentication'] = 'none';
311                    $fallback_auth = false;
312                    continue;
313                }
314            }
315
316            break;
317        } while (true);
318
319        if (!is_null($e)) {
320            $appob->appInitFailure($e);
321
322            switch ($e->getCode()) {
323            case self::AUTH_FAILURE:
324                $failure = new Horde_Exception_AuthenticationFailure($e->getMessage());
325                $failure->application = $app;
326                throw $failure;
327
328            case self::NOT_ACTIVE:
329                /* Try redirect to Horde if an app is not active. */
330                if (!$args['cli'] && $app != 'horde') {
331                    $GLOBALS['notification']->push($e, 'horde.error');
332                    Horde::url($registry->getInitialPage('horde'))->redirect();
333                }
334
335                /* Shouldn't reach here, but fall back to permission denied
336                 * error if we can't even access Horde. */
337                // Fall-through
338
339            case self::PERMISSION_DENIED:
340                $failure = new Horde_Exception_AuthenticationFailure($e->getMessage(), Horde_Auth::REASON_MESSAGE);
341                $failure->application = $app;
342                throw $failure;
343            }
344
345            throw $e;
346        }
347
348        if ($args['timezone']) {
349            $registry->setTimeZone();
350        }
351
352        if (!$args['nocompress']) {
353            $GLOBALS['page_output']->startCompression();
354        }
355
356        if ($args['user_admin']) {
357            if (empty($GLOBALS['conf']['auth']['admins'])) {
358                throw new Horde_Exception(Horde_Core_Translation::t("Admin authentication requested, but no admin users defined in configuration."));
359            }
360            $registry->setAuth(
361                reset($GLOBALS['conf']['auth']['admins']),
362                array(),
363                array('no_convert' => true)
364            );
365        }
366
367        if ($args['permission']) {
368            $admin_opts = array(
369                'permission' => $args['permission'][0],
370                'permlevel' => (isset($args['permission'][1]) ? $args['permission'][1] : Horde_Perms::SHOW)
371            );
372            if (!$registry->isAdmin($admin_opts)) {
373                throw new Horde_Exception_PermissionDenied(Horde_Core_Translation::t("Permission denied."));
374            }
375        }
376
377        return $appob;
378    }
379
380    /**
381     * Create a new Horde_Registry instance.
382     *
383     * @param integer $session_flags  Any session flags.
384     * @param array $args             See appInit().
385     *
386     * @throws Horde_Exception
387     */
388    public function __construct($session_flags = 0, array $args = array())
389    {
390        /* Set a valid timezone. */
391        date_default_timezone_set(
392            ini_get('date.timezone') ?: getenv('TZ') ?: 'UTC'
393        );
394
395        /* Save arguments. */
396        $this->_args = $args;
397
398        /* Define factories. By default, uses the 'create' method in the given
399         * classname (string). If other function needed, define as the second
400         * element in an array. */
401        $factories = array(
402            'Horde_ActiveSyncBackend' => 'Horde_Core_Factory_ActiveSyncBackend',
403            'Horde_ActiveSyncServer' => 'Horde_Core_Factory_ActiveSyncServer',
404            'Horde_ActiveSyncState' => 'Horde_Core_Factory_ActiveSyncState',
405            'Horde_Alarm' => 'Horde_Core_Factory_Alarm',
406            'Horde_Browser' => 'Horde_Core_Factory_Browser',
407            'Horde_Cache' => 'Horde_Core_Factory_Cache',
408            'Horde_Controller_Request' => 'Horde_Core_Factory_Request',
409            'Horde_Controller_RequestConfiguration' => array(
410                'Horde_Core_Controller_RequestMapper',
411                'getRequestConfiguration',
412            ),
413            'Horde_Core_Auth_Signup' => 'Horde_Core_Factory_AuthSignup',
414            'Horde_Core_CssCache' => 'Horde_Core_Factory_CssCache',
415            'Horde_Core_JavascriptCache' => 'Horde_Core_Factory_JavascriptCache',
416            'Horde_Core_Perms' => 'Horde_Core_Factory_PermsCore',
417            'Horde_Dav_Server' => 'Horde_Core_Factory_DavServer',
418            'Horde_Dav_Storage' => 'Horde_Core_Factory_DavStorage',
419            'Horde_Db_Adapter' => 'Horde_Core_Factory_DbBase',
420            'Horde_Editor' => 'Horde_Core_Factory_Editor',
421            'Horde_ElasticSearch_Client' => 'Horde_Core_Factory_ElasticSearch',
422            'Horde_Group' => 'Horde_Core_Factory_Group',
423            'Horde_HashTable' => 'Horde_Core_Factory_HashTable',
424            'Horde_History' => 'Horde_Core_Factory_History',
425            'Horde_Kolab_Server_Composite' => 'Horde_Core_Factory_KolabServer',
426            'Horde_Kolab_Session' => 'Horde_Core_Factory_KolabSession',
427            'Horde_Kolab_Storage' => 'Horde_Core_Factory_KolabStorage',
428            'Horde_Lock' => 'Horde_Core_Factory_Lock',
429            'Horde_Log_Logger' => 'Horde_Core_Factory_Logger',
430            'Horde_Mail' => 'Horde_Core_Factory_MailBase',
431            'Horde_Memcache' => 'Horde_Core_Factory_Memcache',
432            'Horde_Nosql_Adapter' => 'Horde_Core_Factory_NosqlBase',
433            'Horde_Notification' => 'Horde_Core_Factory_Notification',
434            'Horde_Perms' => 'Horde_Core_Factory_Perms',
435            'Horde_Queue_Storage' => 'Horde_Core_Factory_QueueStorage',
436            'Horde_Routes_Mapper' => 'Horde_Core_Factory_Mapper',
437            'Horde_Routes_Matcher' => 'Horde_Core_Factory_Matcher',
438            'Horde_Secret' => 'Horde_Core_Factory_Secret',
439            'Horde_Secret_Cbc' => 'Horde_Core_Factory_Secret_Cbc',
440            'Horde_Service_Facebook' => 'Horde_Core_Factory_Facebook',
441            'Horde_Service_Twitter' => 'Horde_Core_Factory_Twitter',
442            'Horde_Service_UrlShortener' => 'Horde_Core_Factory_UrlShortener',
443            'Horde_SessionHandler' => 'Horde_Core_Factory_SessionHandler',
444            'Horde_Template' => 'Horde_Core_Factory_Template',
445            'Horde_Timezone' => 'Horde_Core_Factory_Timezone',
446            'Horde_Token' => 'Horde_Core_Factory_Token',
447            'Horde_Variables' => 'Horde_Core_Factory_Variables',
448            'Horde_View' => 'Horde_Core_Factory_View',
449            'Horde_View_Base' => 'Horde_Core_Factory_View',
450            'Horde_Weather' => 'Horde_Core_Factory_Weather',
451            'Net_DNS2_Resolver' => 'Horde_Core_Factory_Dns',
452            'Text_LanguageDetect' => 'Horde_Core_Factory_LanguageDetect',
453        );
454
455        /* Define implementations. */
456        $implementations = array(
457            'Horde_Controller_ResponseWriter' => 'Horde_Controller_ResponseWriter_Web'
458        );
459
460        /* Setup injector. */
461        $GLOBALS['injector'] = $injector = new Horde_Injector(new Horde_Injector_TopLevel());
462
463        foreach ($factories as $key => $val) {
464            if (is_string($val)) {
465                $val = array($val, 'create');
466            }
467            $injector->bindFactory($key, $val[0], $val[1]);
468        }
469        foreach ($implementations as $key => $val) {
470            $injector->bindImplementation($key, $val);
471        }
472
473        $GLOBALS['registry'] = $this;
474        $injector->setInstance(__CLASS__, $this);
475
476        /* Setup autoloader instance. */
477        $injector->setInstance('Horde_Autoloader', $GLOBALS['__autoloader']);
478
479        /* Import and global Horde's configuration values. */
480        $this->importConfig('horde');
481        $conf = $GLOBALS['conf'];
482
483        /* Set the umask according to config settings. */
484        if (isset($conf['umask'])) {
485            umask($conf['umask']);
486        }
487
488        /* Set the error reporting level in accordance with the config
489         * settings. */
490        error_reporting($conf['debug_level']);
491
492        /* Set the maximum execution time in accordance with the config
493         * settings, but only if not running from the CLI */
494        if (!isset($GLOBALS['cli'])) {
495            set_time_limit($conf['max_exec_time']);
496        }
497
498        /* The basic framework is up and loaded, so set the init flag. */
499        $this->hordeInit = true;
500
501        /* Initial Horde-wide settings. */
502
503        /* Initialize browser object. */
504        $GLOBALS['browser'] = $injector->getInstance('Horde_Browser');
505
506        /* Get modified time of registry files. */
507        $regfiles = array(
508            HORDE_BASE . '/config/registry.php',
509            HORDE_BASE . '/config/registry.d'
510        );
511        if (file_exists(HORDE_BASE . '/config/registry.local.php')) {
512            $regfiles[] = HORDE_BASE . '/config/registry.local.php';
513        }
514        if (!empty($conf['vhosts'])) {
515            $vhost = HORDE_BASE . '/config/registry-' . $conf['server']['name'] . '.php';
516            if (file_exists($vhost)) {
517                $regfiles[] = $this->vhost = $vhost;
518            }
519        }
520        $this->_regmtime = max(array_map('filemtime', $regfiles));
521
522        /* Start a session. */
523        if ($session_flags & self::SESSION_NONE) {
524            /* Never start a session if the session flags include
525               SESSION_NONE. */
526            $GLOBALS['session'] = $session = new Horde_Session_Null();
527            $session->setup(true, $args['session_cache_limiter']);
528        } elseif ((PHP_SAPI === 'cli') ||
529                  (empty($_SERVER['SERVER_NAME']) &&
530                   ((PHP_SAPI === 'cgi') || (PHP_SAPI === 'cgi-fcgi')))) {
531            $GLOBALS['session'] = $session = new Horde_Session();
532            $session->setup(false, $args['session_cache_limiter']);
533        } else {
534            $GLOBALS['session'] = $session = new Horde_Session();
535            $session->setup(true, $args['session_cache_limiter']);
536            if ($session_flags & self::SESSION_READONLY) {
537                /* Close the session immediately so no changes can be made but
538                   values are still available. */
539                $session->close();
540            }
541        }
542        $injector->setInstance('Horde_Session', $session);
543
544        /* Always need to load applications information. */
545        $this->_loadApplications();
546
547        /* Stop system if Horde is inactive. */
548        if ($this->applications['horde']['status'] == 'inactive') {
549            throw new Horde_Exception(Horde_Core_Translation::t("This system is currently deactivated."));
550        }
551
552        /* Initialize language configuration object. */
553        $this->nlsconfig = new Horde_Registry_Nlsconfig();
554
555        /* Initialize the localization routines and variables. */
556        $this->setLanguageEnvironment(null, 'horde');
557
558        /* Initialize global page output object. */
559        $GLOBALS['page_output'] = $injector->getInstance('Horde_PageOutput');
560
561        /* Initialize notification object. Always attach status listener by
562         * default. */
563        $nclass = null;
564        switch ($this->getView()) {
565        case self::VIEW_DYNAMIC:
566            $nclass = 'Horde_Core_Notification_Listener_DynamicStatus';
567            break;
568
569        case self::VIEW_SMARTMOBILE:
570            $nclass = 'Horde_Core_Notification_Listener_SmartmobileStatus';
571            break;
572        }
573        $GLOBALS['notification'] = $injector->getInstance('Horde_Notification');
574        if (empty($args['nonotificationinit'])) {
575            $GLOBALS['notification']->attachAllAppHandlers();
576        }
577        $GLOBALS['notification']->attach('status', null, $nclass);
578
579        Horde_Shutdown::add($this);
580    }
581
582    /**
583     * (Re)set the authentication parameter. Useful for requests, such as Rpc
584     * requests where we actually don't perform authentication until later in
585     * the request, but still need Horde bootstrapped early in the request. Also
586     * clears the local app/api cache since applications will probably already
587     * have been initialized during Notification polling.
588     *
589     * @see appInit()
590     *
591     * @param string $authentication  The authentication setting.
592     */
593    public function setAuthenticationSetting($authentication)
594    {
595        $this->_args['authentication'] = $authentication;
596        $this->_cache['cfile'] = $this->_cache['ob'] = array();
597        $this->_cache['isauth'] = array();
598        $this->_appsInit = array();
599        while ($this->popApp());
600    }
601
602    /**
603     * Events to do on shutdown.
604     */
605    public function shutdown()
606    {
607        /* Register access key logger for translators. */
608        if (!empty($GLOBALS['conf']['log_accesskeys'])) {
609            Horde::getAccessKey(null, null, true);
610        }
611
612        /* Register memory tracker if logging in debug mode. */
613        Horde::log('Max memory usage: ' . memory_get_peak_usage(true) . ' bytes', 'DEBUG');
614    }
615
616    /**
617     * A property call to the registry object will return a Caller object.
618     */
619    public function __get($api)
620    {
621        if (in_array($api, $this->listAPIs())) {
622            return new Horde_Registry_Caller($this, $api);
623        }
624        throw new Horde_Exception('The API "' . $api . '" is not defined in the Horde Registry.');
625    }
626
627    /**
628     * Clone should never be called on this object. If it is, die.
629     *
630     * @throws Horde_Exception
631     */
632    public function __clone()
633    {
634        throw new LogicException('Registry objects should never be cloned.');
635    }
636
637    /**
638     * serialize() should never be called on this object. If it is, die.
639     *
640     * @throws Horde_Exception
641     */
642    public function __sleep()
643    {
644        throw new LogicException('Registry objects should never be serialized.');
645    }
646
647    /**
648     * Rebuild the registry configuration.
649     */
650    public function rebuild()
651    {
652        global $session;
653
654        $app = $this->getApp();
655
656        $this->applications = $this->_apiList = $this->_cache['conf'] = $this->_cache['ob'] = $this->_interfaces = array();
657
658        $session->remove('horde', 'nls/');
659        $session->remove('horde', 'registry/');
660        $session->remove('horde', self::REGISTRY_CACHE);
661
662        $this->_loadApplications();
663
664        $this->importConfig('horde');
665        $this->importConfig($app);
666    }
667
668    /**
669     * Load application information from registry config files.
670     */
671    protected function _loadApplications()
672    {
673        global $cli, $injector;
674
675        if (!empty($this->_interfaces)) {
676            return;
677        }
678
679        /* First, try to load from cache. */
680        if (!isset($cli) && !$this->isTest()) {
681            if (Horde_Util::extensionExists('apc')) {
682                $cstorage = 'Horde_Cache_Storage_Apc';
683            } elseif (Horde_Util::extensionExists('xcache')) {
684                $cstorage = 'Horde_Cache_Storage_Xcache';
685            } else {
686                $cstorage = 'Horde_Cache_Storage_File';
687            }
688
689            $cache = new Horde_Cache(
690                new $cstorage(array(
691                    'no_gc' => true,
692                    'prefix' => 'horde_registry_cache_'
693                )),
694                array(
695                    'lifetime' => 0,
696                    'logger' => $injector->getInstance('Horde_Log_Logger')
697                )
698            );
699
700            if (($cid = $this->_cacheId()) &&
701                ($cdata = $cache->get($cid, 0))) {
702                try {
703                    list($this->applications, $this->_interfaces) =
704                        $injector->getInstance('Horde_Pack')->unpack($cdata);
705                    return;
706                } catch (Horde_Pack_Exception $e) {}
707            }
708        }
709
710        $config = new Horde_Registry_Registryconfig($this);
711        $this->applications = $config->applications;
712        $this->_interfaces = $config->interfaces;
713
714        if (!isset($cache)) {
715            return;
716        }
717
718        /* Need to determine hash of generated data, since it is possible that
719         * there is dynamic data in the config files. This only needs to
720         * be done once per session. */
721        $packed_data = $injector->getInstance('Horde_Pack')->pack(array(
722            $this->applications,
723            $this->_interfaces
724        ));
725        $cid = $this->_cacheId($packed_data);
726
727        if (!$cache->exists($cid, 0)) {
728            $cache->set($cid, $packed_data);
729        }
730    }
731
732    /**
733     * Get the cache ID for the registry information.
734     *
735     * @param string $hash  If set, hash this value and use as the hash of the
736     *                      registry. If false, uses session stored value.
737     */
738    protected function _cacheId($hash = null)
739    {
740        global $session;
741
742        if (!is_null($hash)) {
743            $hash = hash('md5', $hash);
744            $session->set('horde', self::REGISTRY_CACHE, $hash);
745        } elseif (!($hash = $session->get('horde', self::REGISTRY_CACHE))) {
746            return false;
747        }
748
749        /* Generate cache ID. */
750        return implode('|', array(
751            gethostname() ?: php_uname(),
752            __FILE__,
753            $this->_regmtime,
754            $hash
755        ));
756    }
757
758    /**
759     * Load an application's API object.
760     *
761     * @param string $app  The application to load.
762     *
763     * @return Horde_Registry_Api  The API object, or null if not available.
764     */
765    protected function _loadApi($app)
766    {
767        if (isset($this->_cache['ob'][$app]['api'])) {
768            return $this->_cache['ob'][$app]['api'];
769        }
770
771        $api = null;
772        $status = array('active', 'notoolbar', 'hidden');
773        $status[] = $this->isAdmin()
774            ? 'admin'
775            : 'noadmin';
776
777        if (in_array($this->applications[$app]['status'], $status)) {
778            try {
779                $api = $this->getApiInstance($app, 'api');
780            } catch (Horde_Exception $e) {
781                Horde::log($e, 'DEBUG');
782            }
783        }
784
785        $this->_cache['ob'][$app]['api'] = $api;
786
787        return $api;
788    }
789
790    /**
791     * Retrieve an API object.
792     *
793     * @param string $app   The application to load.
794     * @param string $type  Either 'application' or 'api'.
795     *
796     * @return Horde_Registry_Api|Horde_Registry_Application  The API object.
797     * @throws Horde_Exception
798     */
799    public function getApiInstance($app, $type)
800    {
801        if (isset($this->_cache['ob'][$app][$type])) {
802            return $this->_cache['ob'][$app][$type];
803        }
804
805        $path = $this->get('fileroot', $app) . '/lib';
806
807        /* Set up autoload paths for the current application. This needs to
808         * be done here because it is possible to try to load app-specific
809         * libraries from other applications. */
810        if (!isset($this->_cache['ob'][$app])) {
811            $autoloader = $GLOBALS['injector']->getInstance('Horde_Autoloader');
812
813            $app_mappers = array(
814                'Controller' =>  'controllers',
815                'Helper' => 'helpers',
816                'SettingsExporter' => 'settings'
817            );
818            $applicationMapper = new Horde_Autoloader_ClassPathMapper_Application($this->get('fileroot', $app) . '/app');
819            foreach ($app_mappers as $key => $val) {
820                $applicationMapper->addMapping($key, $val);
821            }
822            $autoloader->addClassPathMapper($applicationMapper);
823
824            /* Skip horde, since this was already setup in core.php. */
825            if ($app != 'horde') {
826                $autoloader->addClassPathMapper(
827                    new Horde_Autoloader_ClassPathMapper_PrefixString($app, $path)
828                );
829            }
830        }
831
832        $cname = Horde_String::ucfirst($type);
833
834        /* Can't autoload here, since the application may not have been
835         * initialized yet. */
836        $classname = Horde_String::ucfirst($app) . '_' . $cname;
837        $path = $path . '/' . $cname . '.php';
838        if (file_exists($path)) {
839            include_once $path;
840        } else {
841            $classname = __CLASS__ . '_' . $cname;
842        }
843
844        if (!class_exists($classname, false)) {
845            throw new Horde_Exception("$app does not have an API");
846        }
847
848        $this->_cache['ob'][$app][$type] = ($type == 'application')
849            ? new $classname($app)
850            : new $classname();
851
852        return $this->_cache['ob'][$app][$type];
853    }
854
855    /**
856     * Return a list of the installed and registered applications.
857     *
858     * @param array $filter   An array of the statuses that should be
859     *                        returned. Defaults to non-hidden.
860     * @param boolean $assoc  Return hash with app names as keys and config
861     *                        parameters as values?
862     * @param integer $perms  The permission level to check for in the list.
863     *                        If null, skips permission check.
864     *
865     * @return array  List of apps registered with Horde. If no
866     *                applications are defined returns an empty array.
867     */
868    public function listApps($filter = null, $assoc = false,
869                             $perms = Horde_Perms::SHOW)
870    {
871        if (is_null($filter)) {
872            $filter = array('notoolbar', 'active');
873        }
874        if (!$this->isAdmin() &&
875            in_array('active', $filter) &&
876            !in_array('noadmin', $filter)) {
877            $filter[] = 'noadmin';
878        }
879
880        $apps = array();
881        foreach ($this->applications as $app => $params) {
882            if (in_array($params['status'], $filter)) {
883                /* Topbar apps can only be displayed if the parent app is
884                 * active. */
885                if (($params['status'] == 'topbar') &&
886                    $this->isInactive($params['app'])) {
887                        continue;
888                }
889
890                if ((is_null($perms) || $this->hasPermission($app, $perms))) {
891                    $apps[$app] = $params;
892                }
893            }
894        }
895
896        return $assoc ? $apps : array_keys($apps);
897    }
898
899    /**
900     * Return a list of all applications, ignoring permissions.
901     *
902     * @return array  List of all apps registered with Horde.
903     */
904    public function listAllApps()
905    {
906        // Default to all installed (but possibly not configured) applications.
907        return $this->listApps(array(
908            'active', 'admin', 'noadmin', 'hidden', 'inactive', 'notoolbar'
909        ), false, null);
910    }
911
912    /**
913     * Is the given application inactive?
914     *
915     * @param string $app  The application to check.
916     *
917     * @return boolean  True if inactive.
918     */
919    public function isInactive($app)
920    {
921        return (!isset($this->applications[$app]) ||
922                ($this->applications[$app]['status'] == 'inactive') ||
923                (($this->applications[$app]['status'] == 'admin') &&
924                 !$this->isAdmin()) ||
925                (($this->applications[$app]['status'] == 'noadmin') &&
926                 $this->currentProcessAuth() &&
927                 $this->isAdmin()));
928    }
929
930    /**
931     * Returns all available registry APIs.
932     *
933     * @return array  The API list.
934     */
935    public function listAPIs()
936    {
937        if (empty($this->_apiList) && !empty($this->_interfaces)) {
938            $apis = array();
939
940            foreach (array_keys($this->_interfaces) as $interface) {
941                list($api,) = explode('/', $interface, 2);
942                $apis[$api] = true;
943            }
944
945            $this->_apiList = array_keys($apis);
946        }
947
948        return $this->_apiList;
949    }
950
951    /**
952     * Returns all of the available registry methods, or alternately
953     * only those for a specified API.
954     *
955     * @param string $api  Defines the API for which the methods shall be
956     *                     returned. If null, returns all methods.
957     *
958     * @return array  The method list.
959     */
960    public function listMethods($api = null)
961    {
962        $methods = array();
963
964        foreach (array_keys($this->applications) as $app) {
965            if (isset($this->applications[$app]['provides'])) {
966                $provides = $this->applications[$app]['provides'];
967                if (!is_array($provides)) {
968                    $provides = array($provides);
969                }
970
971                foreach ($provides as $method) {
972                    if (strpos($method, '/') !== false) {
973                        if (is_null($api) ||
974                            (substr($method, 0, strlen($api)) == $api)) {
975                            $methods[$method] = true;
976                        }
977                    } elseif (($api_ob = $this->_loadApi($app)) &&
978                              (is_null($api) || ($method == $api))) {
979                        foreach ($api_ob->methods() as $service) {
980                            $methods[$method . '/' . $service] = true;
981                        }
982                    }
983                }
984            }
985        }
986
987        return array_keys($methods);
988    }
989
990    /**
991     * Determine if an interface is implemented by an active application.
992     *
993     * @param string $interface  The interface to check for.
994     *
995     * @return mixed  The application implementing $interface if we have it,
996     *                false if the interface is not implemented.
997     */
998    public function hasInterface($interface)
999    {
1000        return !empty($this->_interfaces[$interface])
1001            ? $this->_interfaces[$interface]
1002            : false;
1003    }
1004
1005    /**
1006     * Determine if a method has been registered with the registry.
1007     *
1008     * @param string $method  The full name of the method to check for.
1009     * @param string $app     Only check this application.
1010     *
1011     * @return mixed  The application implementing $method if we have it,
1012     *                false if the method doesn't exist.
1013     */
1014    public function hasMethod($method, $app = null)
1015    {
1016        return $this->_doHasSearch($method, $app, 'methods');
1017    }
1018
1019    /**
1020     * Determine if a link has been registered with the registry.
1021     *
1022     * @since 2.12.0
1023     *
1024     * @param string $method  The full name of the link method to check for.
1025     * @param string $app     Only check this application.
1026     *
1027     * @return mixed  The application implementing $method if we have it,
1028     *                false if the link method doesn't exist.
1029     */
1030    public function hasLink($method, $app = null)
1031    {
1032        return $this->_doHasSearch($method, $app, 'links');
1033    }
1034
1035    /**
1036     * Do the has*() search.
1037     *
1038     * @see hasMethod
1039     * @see hasLink
1040     *
1041     * @param string $func  The API function to call to get the list of
1042     *                      elements to search. Either 'methods' or 'links'.
1043     *
1044     * @return mixed  The application implementing $method, false if it
1045     *                doesn't exist;
1046     */
1047    protected function _doHasSearch($method, $app, $func)
1048    {
1049        if (is_null($app)) {
1050            if (($lookup = $this->_methodLookup($method)) === false) {
1051                return false;
1052            }
1053            list($app, $call) = $lookup;
1054        } else {
1055            $call = $method;
1056        }
1057
1058        if ($api_ob = $this->_loadApi($app)) {
1059            switch ($func) {
1060            case 'links':
1061                $links = $api_ob->links();
1062                return isset($links[$call]) ? $app : false;
1063
1064            case 'methods':
1065                return in_array($call, $api_ob->methods()) ? $app : false;
1066            }
1067        }
1068
1069        return false;
1070    }
1071
1072    /**
1073     * Return the hook corresponding to the default package that provides the
1074     * functionality requested by the $method parameter.
1075     * $method is a string consisting of "packagetype/methodname".
1076     *
1077     * @param string $method  The method to call.
1078     * @param array $args     Arguments to the method.
1079     *
1080     * @return mixed  Return from method call.
1081     * @throws Horde_Exception
1082     */
1083    public function call($method, $args = array())
1084    {
1085        if (($lookup = $this->_methodLookup($method)) === false) {
1086            throw new Horde_Exception('The method "' . $method . '" is not defined in the Horde Registry.');
1087        }
1088
1089        return $this->callByPackage($lookup[0], $lookup[1], $args);
1090    }
1091
1092    /**
1093     * Output the hook corresponding to the specific package named.
1094     *
1095     * @param string $app     The application being called.
1096     * @param string $call    The method to call.
1097     * @param array $args     Arguments to the method.
1098     * @param array $options  Additional options:
1099     *   - noperms: (boolean) If true, don't check the perms.
1100     *
1101     * @return mixed  Return from application call.
1102     * @throws Horde_Exception_PushApp
1103     */
1104    public function callByPackage($app, $call, array $args = array(),
1105                                  array $options = array())
1106    {
1107        /* Note: calling hasMethod() makes sure that we've cached
1108         * $app's services and included the API file, so we don't try
1109         * to do it again explicitly in this method. */
1110        if (!$this->hasMethod($call, $app)) {
1111            throw new Horde_Exception(sprintf('The method "%s" is not defined in the API for %s.', $call, $app));
1112        }
1113
1114        /* Load the API now. */
1115        $methods = ($api_ob = $this->_loadApi($app))
1116            ? $api_ob->methods()
1117            : array();
1118
1119        /* Make sure that the function actually exists. */
1120        if (!in_array($call, $methods)) {
1121            throw new Horde_Exception('The function implementing ' . $call . ' is not defined in ' . $app . '\'s API.');
1122        }
1123
1124        /* Switch application contexts now, if necessary, before
1125         * including any files which might do it for us. Return an
1126         * error immediately if pushApp() fails. */
1127        $pushed = $this->pushApp($app, array(
1128            'check_perms' => !in_array($call, $api_ob->noPerms()) && empty($options['noperms']) && $this->currentProcessAuth()
1129        ));
1130
1131        try {
1132            $result = call_user_func_array(array($api_ob, $call), $args);
1133            if ($result instanceof PEAR_Error) {
1134                $result = new Horde_Exception_Wrapped($result);
1135            }
1136        } catch (Horde_Exception $e) {
1137            $result = $e;
1138        }
1139
1140        /* If we changed application context in the course of this
1141         * call, undo that change now. */
1142        if ($pushed === true) {
1143            $this->popApp();
1144        }
1145
1146        if ($result instanceof Horde_Exception) {
1147            throw $result;
1148        }
1149
1150        return $result;
1151    }
1152
1153    /**
1154     * Call a private Horde application method.
1155     *
1156     * @param string $app     The application name.
1157     * @param string $call    The method to call.
1158     * @param array $options  Additional options:
1159     *   - args: (array) Additional parameters to pass to the method.
1160     *   - check_missing: (boolean) If true, throws an Exception if method
1161     *                    does not exist. Otherwise, will return null.
1162     *   - noperms: (boolean) If true, don't check the perms.
1163     *
1164     * @return mixed  Various.
1165     *
1166     * @throws Horde_Exception  Application methods should throw this if there
1167     *                          is a fatal error.
1168     * @throws Horde_Exception_PushApp
1169     */
1170    public function callAppMethod($app, $call, array $options = array())
1171    {
1172        /* Load the API now. */
1173        try {
1174            $api = $this->getApiInstance($app, 'application');
1175        } catch (Horde_Exception $e) {
1176            if (empty($options['check_missing'])) {
1177                return null;
1178            }
1179            throw $e;
1180        }
1181
1182        if (!method_exists($api, $call)) {
1183            if (empty($options['check_missing'])) {
1184                return null;
1185            }
1186            throw new Horde_Exception('Method does not exist.');
1187        }
1188
1189        /* Switch application contexts now, if necessary, before
1190         * including any files which might do it for us. Return an
1191         * error immediately if pushApp() fails. */
1192        $pushed = $this->pushApp($app, array(
1193            'check_perms' => empty($options['noperms']) && $this->currentProcessAuth()
1194        ));
1195
1196        try {
1197            $result = call_user_func_array(array($api, $call), empty($options['args']) ? array() : $options['args']);
1198        } catch (Horde_Exception $e) {
1199            $result = $e;
1200        }
1201
1202        /* If we changed application context in the course of this
1203         * call, undo that change now. */
1204        if ($pushed === true) {
1205            $this->popApp();
1206        }
1207
1208        if ($result instanceof Exception) {
1209            throw $e;
1210        }
1211
1212        return $result;
1213    }
1214
1215    /**
1216     * Returns the link corresponding to the default package that provides the
1217     * functionality requested by the $method parameter.
1218     *
1219     * @param string $method  The method to link to, consisting of
1220     *                        "packagetype/methodname".
1221     * @param array $args     Arguments to the method.
1222     * @param mixed $extra    Extra, non-standard arguments to the method.
1223     *
1224     * @return string  The link for that method.
1225     * @throws Horde_Exception
1226     */
1227    public function link($method, $args = array(), $extra = '')
1228    {
1229        if (($lookup = $this->_methodLookup($method)) === false) {
1230            throw new Horde_Exception('The link "' . $method . '" is not defined in the Horde Registry.');
1231        }
1232
1233        return $this->linkByPackage($lookup[0], $lookup[1], $args, $extra);
1234    }
1235
1236    /**
1237     * Returns the link corresponding to the specific package named.
1238     *
1239     * @param string $app   The application being called.
1240     * @param string $call  The method to link to.
1241     * @param array $args   Arguments to the method.
1242     * @param mixed $extra  Extra, non-standard arguments to the method.
1243     *
1244     * @return string  The link for that method.
1245     * @throws Horde_Exception
1246     */
1247    public function linkByPackage($app, $call, $args = array(), $extra = '')
1248    {
1249        $links = ($api_ob = $this->_loadApi($app))
1250            ? $api_ob->links()
1251            : array();
1252
1253        /* Make sure the link is defined. */
1254        if (!isset($links[$call])) {
1255            throw new Horde_Exception('The link "' . $call . '" is not defined in ' . $app . '\'s API.');
1256        }
1257
1258        /* Initial link value. */
1259        $link = $links[$call];
1260
1261        /* Fill in html-encoded arguments. */
1262        foreach ($args as $key => $val) {
1263            $link = str_replace('%' . $key . '%', htmlentities($val), $link);
1264        }
1265
1266        $link = $this->applicationWebPath($link, $app);
1267
1268        /* Replace htmlencoded arguments that haven't been specified with
1269           an empty string (this is where the default would be substituted
1270           in a stricter registry implementation). */
1271        $link = preg_replace('|%.+%|U', '', $link);
1272
1273        /* Fill in urlencoded arguments. */
1274        foreach ($args as $key => $val) {
1275            $link = str_replace('|' . Horde_String::lower($key) . '|', urlencode($val), $link);
1276        }
1277
1278        /* Append any extra, non-standard arguments. */
1279        if (is_array($extra)) {
1280            $extra_args = '';
1281            foreach ($extra as $key => $val) {
1282                $extra_args .= '&' . urlencode($key) . '=' . urlencode($val);
1283            }
1284        } else {
1285            $extra_args = $extra;
1286        }
1287        $link = str_replace('|extra|', $extra_args, $link);
1288
1289        /* Replace html-encoded arguments that haven't been specified with
1290           an empty string (this is where the default would be substituted
1291           in a stricter registry implementation). */
1292        $link = preg_replace('|\|.+\||U', '', $link);
1293
1294        return $link;
1295    }
1296
1297    /**
1298     * Do a lookup of method name -> app call.
1299     *
1300     * @param string $method  The method name.
1301     *
1302     * @return mixed  An array containing the app and method call, or false
1303     *                if not found.
1304     */
1305    protected function _methodLookup($method)
1306    {
1307        list($interface, $call) = explode('/', $method, 2);
1308        if (!empty($this->_interfaces[$method])) {
1309            return array($this->_interfaces[$method], $call);
1310        } elseif (!empty($this->_interfaces[$interface])) {
1311            return array($this->_interfaces[$interface], $call);
1312        }
1313
1314        return false;
1315    }
1316
1317    /**
1318     * Replace any %application% strings with the filesystem path to the
1319     * application.
1320     *
1321     * @param string $path  The application string.
1322     * @param string $app   The application being called.
1323     *
1324     * @return string  The application file path.
1325     * @throws Horde_Exception
1326     */
1327    public function applicationFilePath($path, $app = null)
1328    {
1329        if (is_null($app)) {
1330            $app = $this->getApp();
1331        }
1332
1333        if (!isset($this->applications[$app])) {
1334            throw new Horde_Exception(sprintf(Horde_Core_Translation::t("\"%s\" is not configured in the Horde Registry."), $app));
1335        }
1336
1337        return str_replace('%application%', $this->applications[$app]['fileroot'], $path);
1338    }
1339
1340    /**
1341     * Replace any %application% strings with the web path to the application.
1342     *
1343     * @param string $path  The application string.
1344     * @param string $app   The application being called.
1345     *
1346     * @return string  The application web path.
1347     */
1348    public function applicationWebPath($path, $app = null)
1349    {
1350        return str_replace('%application%', $this->get('webroot', $app), $path);
1351    }
1352
1353    /**
1354     * TODO
1355     *
1356     * @param string $type       The type of link.
1357     * <pre>
1358     * The following must be defined in Horde's menu config, or else they
1359     * won't be displayed in the menu:
1360     * 'help', 'problem', 'logout', 'login', 'prefs'
1361     * </pre>
1362     *
1363     * @return boolean  True if the link is to be shown.
1364     */
1365    public function showService($type)
1366    {
1367        global $conf;
1368
1369        if (!in_array($type, array('help', 'problem', 'logout', 'login', 'prefs'))) {
1370            return true;
1371        }
1372
1373        if (empty($conf['menu']['links'][$type])) {
1374            return false;
1375        }
1376
1377        switch ($conf['menu']['links'][$type]) {
1378        case 'all':
1379            return true;
1380
1381        case 'authenticated':
1382            return (bool)$this->getAuth();
1383
1384        default:
1385        case 'never':
1386            return false;
1387        }
1388    }
1389
1390    /**
1391     * Returns the URL to access a Horde service.
1392     *
1393     * @param string $type       The service to display:
1394     *   - ajax: AJAX endpoint.
1395     *   - cache: Cached data output.
1396     *   - download: Download link.
1397     *   - emailconfirm: E-mail confirmation page.
1398     *   - go: URL redirection utility.
1399     *   - help: Help page.
1400     *   - imple: Imple endpoint.
1401     *   - login: Login page.
1402     *   - logintasks: Logintasks page.
1403     *   - logout: Logout page.
1404     *   - pixel: Pixel generation page.
1405     *   - portal: Main portal page.
1406     *   - prefs: Preferences UI.
1407     *   - problem: Problem reporting page.
1408     * @param string $app        The name of the current Horde application.
1409     * @param boolean $full      Return a full url? @since 2.4.0
1410     *
1411     * @return Horde_Url  The link.
1412     * @throws Horde_Exception
1413     */
1414    public function getServiceLink($type, $app = null, $full = false)
1415    {
1416        $opts = array('app' => 'horde');
1417
1418        switch ($type) {
1419        case 'ajax':
1420            if (is_null($app)) {
1421                $app = 'horde';
1422            }
1423            return Horde::url('services/ajax.php/' . $app . '/', $full, $opts)
1424                       ->add('token', $GLOBALS['session']->getToken());
1425
1426        case 'cache':
1427            $opts['append_session'] = -1;
1428            return Horde::url('services/cache.php', $full, $opts);
1429
1430        case 'download':
1431            return Horde::url('services/download/', $full, $opts)
1432                ->add('app', $app);
1433
1434        case 'emailconfirm':
1435            return Horde::url('services/confirm.php', $full, $opts);
1436
1437        case 'go':
1438            return Horde::url('services/go.php', $full, $opts);
1439
1440        case 'help':
1441            return Horde::url('services/help/', $full, $opts)
1442                ->add('module', $app);
1443
1444        case 'imple':
1445            return Horde::url('services/imple.php', $full, $opts);
1446
1447        case 'login':
1448            return Horde::url('login.php', $full, $opts);
1449
1450        case 'logintasks':
1451            return Horde::url('services/logintasks.php', $full, $opts)
1452                ->add('app', $app);
1453
1454        case 'logout':
1455            return $this->getLogoutUrl(array(
1456                'reason' => Horde_Auth::REASON_LOGOUT
1457            ));
1458
1459        case 'pixel':
1460            return Horde::url('services/images/pixel.php', $full, $opts);
1461
1462        case 'prefs':
1463            if (!in_array($GLOBALS['conf']['prefs']['driver'], array('', 'none'))) {
1464                $url = Horde::url('services/prefs.php', $full, $opts);
1465                if (!is_null($app)) {
1466                    $url->add('app', $app);
1467                }
1468                return $url;
1469            }
1470            break;
1471
1472        case 'portal':
1473            return ($this->getView() == Horde_Registry::VIEW_SMARTMOBILE)
1474                ? Horde::url('services/portal/smartmobile.php', $full, $opts)
1475                : Horde::url('services/portal/', $full, $opts);
1476            break;
1477
1478        case 'problem':
1479            return Horde::url('services/problem.php', $full, $opts)
1480                ->add(
1481                    'return_url',
1482                    Horde_Util::getFormData(
1483                        'location', Horde::signUrl(Horde::selfUrl(true, true, true))
1484                    )
1485                );
1486
1487        case 'sidebar':
1488            return Horde::url('services/sidebar.php', $full, $opts);
1489
1490        case 'twitter':
1491            return Horde::url('services/twitter/', true);
1492        }
1493
1494        throw new BadFunctionCallException('Invalid service requested: ' . print_r(debug_backtrace(false), true));
1495    }
1496
1497    /**
1498     * Set the current application, adding it to the top of the Horde
1499     * application stack. If this is the first application to be
1500     * pushed, retrieve session information as well.
1501     *
1502     * pushApp() also reads the application's configuration file and
1503     * sets up its global $conf hash.
1504     *
1505     * @param string $app     The name of the application to push.
1506     * @param array $options  Additional options:
1507     *   - check_perms: (boolean) Make sure that the current user has
1508     *                  permissions to the application being loaded. Should
1509     *                  ONLY be disabled by system scripts (cron jobs, etc.)
1510     *                  and scripts that handle login.
1511     *                  DEFAULT: true
1512     *   - logintasks: (boolean) Perform login tasks? Only performed if
1513     *                 'check_perms' is also true. System tasks are always
1514     *                 peformed if the user is authorized.
1515     *                 DEFAULT: false
1516     *   - notransparent: (boolean) Do not attempt transparent authentication.
1517     *                    DEFAULT: false
1518     *
1519     * @return boolean  Whether or not the _appStack was modified.
1520     * @throws Horde_Exception_PushApp
1521     */
1522    public function pushApp($app, array $options = array())
1523    {
1524        global $injector, $notification, $language, $session;
1525
1526        if ($app == $this->getApp()) {
1527            return false;
1528        }
1529
1530        /* Bail out if application is not present or inactive. */
1531        if ($this->isInactive($app)) {
1532            throw new Horde_Exception_PushApp(
1533                sprintf(
1534                    Horde_Core_Translation::t("%s is not activated."),
1535                    $this->applications[$app]['name']
1536                ),
1537                self::NOT_ACTIVE,
1538                $app
1539            );
1540        }
1541
1542        $checkPerms = ((!isset($options['check_perms']) ||
1543                       !empty($options['check_perms'])) &&
1544                       $this->currentProcessAuth());
1545
1546        /* If permissions checking is requested, return an error if the
1547         * current user does not have read perms to the application being
1548         * loaded. We allow access:
1549         *  - To all admins.
1550         *  - To all authenticated users if no permission is set on $app.
1551         *  - To anyone who is allowed by an explicit ACL on $app. */
1552        if ($checkPerms) {
1553            $error = $error_log = null;
1554            $error_app = $this->applications[$app]['name'];
1555            $error_type = self::AUTH_FAILURE;
1556
1557            if (($auth = $this->getAuth()) && !$this->checkExistingAuth()) {
1558                $error = '%s is not authorized %s(Remote host: %s)';
1559                $error_app = '';
1560            }
1561
1562            if (!$error &&
1563                !$this->hasPermission($app, Horde_Perms::READ, array('notransparent' => !empty($options['notransparent'])))) {
1564                $error = '%s is not authorized for %s (Host: %s).';
1565
1566                if ($this->isAuthenticated(array('app' => $app))) {
1567                    $error_log = '%s does not have READ permission for %s (Host: %s)';
1568                    $error_type = self::PERMISSION_DENIED;
1569                }
1570            }
1571
1572            if ($error) {
1573                $auth = $auth ? 'User ' . $auth : 'Guest user';
1574                $remote = $this->remoteHost();
1575
1576                if ($error_log) {
1577                    Horde::log(
1578                        sprintf($error_log, $auth, $error_app, $remote->host),
1579                        'DEBUG'
1580                    );
1581                }
1582
1583                throw new Horde_Exception_PushApp(
1584                    sprintf($error, $auth, $error_app, $remote->host),
1585                    $error_type,
1586                    $app
1587                );
1588            }
1589        }
1590
1591        /* Push application on the stack. */
1592        $this->_appStack[] = $app;
1593
1594        /* Chicken and egg problem: the language environment has to be loaded
1595         * before loading the configuration file, because it might contain
1596         * gettext strings. Though the preferences can specify a different
1597         * language for this app, they have to be loaded after the
1598         * configuration, because they rely on configuration settings. So try
1599         * with the current language, and reset the language later. */
1600        $this->setLanguageEnvironment($language, $app);
1601
1602        /* Load config and prefs. */
1603        $this->importConfig('horde');
1604        $this->importConfig($app);
1605        $this->loadPrefs($app);
1606
1607        /* Reset language, since now we can grab language from prefs. */
1608        if (!$checkPerms && (count($this->_appStack) == 1)) {
1609            $this->setLanguageEnvironment(null, $app);
1610        }
1611
1612        /* Run authenticated hooks, if necessary. */
1613        $hooks = $injector->getInstance('Horde_Core_Hooks');
1614        if ($session->get('horde', 'auth_app_init/' . $app)) {
1615            try {
1616                $error = self::INITCALLBACK_FATAL;
1617                $this->callAppMethod($app, 'authenticated');
1618
1619                $error = self::HOOK_FATAL;
1620                $hooks->callHook('appauthenticated', $app);
1621            } catch (Exception $e) {
1622                $this->_pushAppError($e, $error);
1623            }
1624
1625            $session->remove('horde', 'auth_app_init/' . $app);
1626            unset($this->_appsInit[$app]);
1627        }
1628
1629        /* Initialize application. */
1630        if (!isset($this->_appsInit[$app])) {
1631            $notification->addAppHandler($app);
1632
1633            try {
1634                $error = self::INITCALLBACK_FATAL;
1635                $this->callAppMethod($app, 'init');
1636
1637                $error = self::HOOK_FATAL;
1638                $hooks->callHook('pushapp', $app);
1639            } catch (Exception $e) {
1640                $this->_pushAppError($e, $error);
1641            }
1642
1643            $this->_appsInit[$app] = true;
1644        }
1645
1646        /* Do login tasks. */
1647        if ($checkPerms &&
1648            !empty($options['logintasks']) &&
1649            ($tasks = $injector->getInstance('Horde_Core_Factory_LoginTasks')->create($app))) {
1650            $tasks->runTasks();
1651        }
1652
1653        return true;
1654    }
1655
1656    /**
1657     * Process Exceptions thrown when pushing app on stack.
1658     *
1659     * @param Exception $e    The thrown Exception.
1660     * @param integer $error  The pushApp() error type.
1661     *
1662     * @throws Horde_Exception_PushApp
1663     */
1664    protected function _pushAppError(Exception $e, $error)
1665    {
1666        if ($e instanceof Horde_Exception_HookNotSet) {
1667            return;
1668        }
1669
1670        /* Hook errors are already logged. */
1671        if ($error == self::INITCALLBACK_FATAL) {
1672            Horde::log($e);
1673        }
1674
1675        $app = $this->getApp();
1676        $this->applications[$app]['status'] = 'inactive';
1677        $this->popApp();
1678
1679        throw new Horde_Exception_PushApp($e, $error, $app);
1680    }
1681
1682    /**
1683     * Remove the current app from the application stack, setting the current
1684     * app to whichever app was current before this one took over.
1685     *
1686     * @return string  The name of the application that was popped.
1687     * @throws Horde_Exception
1688     */
1689    public function popApp()
1690    {
1691        /* Pop the current application off of the stack. */
1692        $previous = array_pop($this->_appStack);
1693
1694        /* Import the new active application's configuration values
1695         * and set the gettext domain and the preferred language. */
1696        $app = $this->getApp();
1697        if ($app) {
1698            /* Load config and prefs. */
1699            $this->importConfig('horde');
1700            $this->importConfig($app);
1701            $this->loadPrefs($app);
1702            $this->setTextdomain(
1703                $app,
1704                $this->get('fileroot', $app) . '/locale'
1705            );
1706        }
1707
1708        return $previous;
1709    }
1710
1711    /**
1712     * Return the current application - the app at the top of the application
1713     * stack.
1714     *
1715     * @return string  The current application.
1716     */
1717    public function getApp()
1718    {
1719        return end($this->_appStack);
1720    }
1721
1722    /**
1723     * Check permissions on an application.
1724     *
1725     * @param string $app     The name of the application
1726     * @param integer $perms  The permission level to check for.
1727     * @param array $params   Additional options:
1728     *   - notransparent: (boolean) Do not attempt transparent authentication.
1729     *                    DEFAULT: false
1730     *
1731     * @return boolean  Whether access is allowed.
1732     */
1733    public function hasPermission($app, $perms = Horde_Perms::READ,
1734                                  array $params = array())
1735    {
1736        /* Always do isAuthenticated() check first. You can be an admin, but
1737         * application auth != Horde admin auth. And there can *never* be
1738         * non-SHOW access to an application that requires authentication. */
1739        if (!$this->isAuthenticated(array('app' => $app, 'notransparent' => !empty($params['notransparent']))) &&
1740            $GLOBALS['injector']->getInstance('Horde_Core_Factory_Auth')->create($app)->requireAuth() &&
1741            ($perms != Horde_Perms::SHOW)) {
1742            return false;
1743        }
1744
1745        /* Otherwise, allow access for admins, for apps that do not have any
1746         * explicit permissions, or for apps that allow the given permission. */
1747        return $this->isAdmin() ||
1748            ($GLOBALS['injector']->getInstance('Horde_Perms')->exists($app)
1749             ? $GLOBALS['injector']->getInstance('Horde_Perms')->hasPermission($app, $this->getAuth(), $perms)
1750             : (bool)$this->getAuth());
1751    }
1752
1753    /**
1754     * Reads the configuration values for the given application and imports
1755     * them into the global $conf variable.
1756     *
1757     * @param string $app  The application name.
1758     */
1759    public function importConfig($app)
1760    {
1761        /* Make sure Horde is always loaded. */
1762        if (!isset($this->_cache['conf']['horde'])) {
1763            $this->_cache['conf']['horde'] = new Horde_Registry_Hordeconfig(array('app' => 'horde'));
1764        }
1765
1766        if (!isset($this->_cache['conf'][$app])) {
1767            $this->_cache['conf'][$app] = new Horde_Registry_Hordeconfig_Merged(array(
1768                'aconfig' => new Horde_Registry_Hordeconfig(array('app' => $app)),
1769                'hconfig' => $this->_cache['conf']['horde']
1770            ));
1771        }
1772
1773        $GLOBALS['conf'] = $this->_cache['conf'][$app]->toArray();
1774    }
1775
1776    /**
1777     * Loads the preferences for the current user for the current application
1778     * and imports them into the global $prefs variable.
1779     * $app will be the active application after calling this function.
1780     *
1781     * @param string $app  The name of the application.
1782     *
1783     * @throws Horde_Exception
1784     */
1785    public function loadPrefs($app = null)
1786    {
1787        global $injector, $prefs;
1788
1789        if (strlen($app)) {
1790            $this->pushApp($app);
1791        } elseif (($app = $this->getApp()) === false) {
1792            $app = 'horde';
1793        }
1794
1795        $user = $this->getAuth();
1796        if ($user) {
1797            if (isset($prefs) && ($prefs->getUser() == $user)) {
1798                $prefs->changeScope($app);
1799                return;
1800            }
1801
1802            $opts = array(
1803                'user' => $user
1804            );
1805        } else {
1806            /* If there is no logged in user, return an empty Horde_Prefs
1807             * object with just default preferences. */
1808            $opts = array(
1809                'driver' => 'Horde_Prefs_Storage_Null'
1810            );
1811        }
1812
1813        $prefs = $injector->getInstance('Horde_Core_Factory_Prefs')->create($app, $opts);
1814    }
1815
1816    /**
1817     * Load a configuration file from a Horde application's config directory.
1818     * This call is cached (a config file is only loaded once, regardless of
1819     * the $vars value).
1820     *
1821     * @since 2.12.0
1822     *
1823     * @param string $conf_file  Configuration file name.
1824     * @param mixed $vars        List of config variables to load.
1825     * @param string $app        Application.
1826     *
1827     * @return Horde_Registry_Loadconfig  The config object.
1828     * @throws Horde_Exception
1829     */
1830    public function loadConfigFile($conf_file, $vars = null, $app = null)
1831    {
1832        if (is_null($app)) {
1833            $app = $this->getApp();
1834        }
1835
1836        if (!isset($this->_cache['cfile'][$app][$conf_file])) {
1837            $this->_cache['cfile'][$app][$conf_file] = new Horde_Registry_Loadconfig(
1838                $app,
1839                $conf_file,
1840                $vars
1841            );
1842        }
1843
1844        return $this->_cache['cfile'][$app][$conf_file];
1845    }
1846
1847    /**
1848     * Return the requested configuration parameter for the specified
1849     * application. If no application is specified, the value of
1850     * the current application is used. However, if the parameter is not
1851     * present for that application, the Horde-wide value is used instead.
1852     * If that is not present, we return null.
1853     *
1854     * @param string $parameter  The configuration value to retrieve.
1855     * @param string $app        The application to get the value for.
1856     *
1857     * @return string  The requested parameter, or null if it is not set.
1858     */
1859    public function get($parameter, $app = null)
1860    {
1861        if (is_null($app)) {
1862            $app = $this->getApp();
1863        }
1864
1865        if (isset($this->applications[$app][$parameter])) {
1866            $pval = $this->applications[$app][$parameter];
1867        } else {
1868            switch ($parameter) {
1869            case 'icon':
1870                $pval = Horde_Themes::img($app . '.png', $app);
1871                if ((string)$pval == '') {
1872                    $pval = Horde_Themes::img('app-unknown.png', 'horde');
1873                }
1874                break;
1875
1876            case 'initial_page':
1877                $pval = null;
1878                break;
1879
1880            default:
1881                $pval = isset($this->applications['horde'][$parameter])
1882                    ? $this->applications['horde'][$parameter]
1883                    : null;
1884                break;
1885            }
1886        }
1887
1888        return ($parameter == 'name')
1889            ? (strlen($pval) ? _($pval) : '')
1890            : $pval;
1891    }
1892
1893    /**
1894     * Return the version string for a given application.
1895     *
1896     * @param string $app      The application to get the value for.
1897     * @param boolean $number  Return the raw version number, suitable for
1898     *                         comparison purposes.
1899     *
1900     * @return string  The version string for the application.
1901     */
1902    public function getVersion($app = null, $number = false)
1903    {
1904        if (empty($app)) {
1905            $app = $this->getApp();
1906        }
1907
1908        try {
1909            $api = $this->getApiInstance($app, 'application');
1910        } catch (Horde_Exception $e) {
1911            return 'unknown';
1912        }
1913
1914        return $number
1915            ? preg_replace('/H\d \((.*)\)/', '$1', $api->version)
1916            : $api->version;
1917    }
1918
1919    /**
1920     * Does the application have the queried feature?
1921     *
1922     * @param string $id   Feature ID.
1923     * @param string $app  The application to check (defaults to current app).
1924     *
1925     * @return boolean  True if the application has the feature.
1926     */
1927    public function hasFeature($id, $app = null)
1928    {
1929        if (empty($app)) {
1930            $app = $this->getApp();
1931        }
1932
1933        try {
1934            $api = $this->getApiInstance($app, 'application');
1935        } catch (Horde_Exception $e) {
1936            return false;
1937        }
1938
1939        return !empty($api->features[$id]);
1940    }
1941
1942    /**
1943     * Does the given application have the queried view?
1944     *
1945     * @param integer $view  The view type (VIEW_* constant).
1946     * @param string $app    The application to check (defaults to current
1947     *                       app).
1948     *
1949     * @return boolean  True if the view is available in the application.
1950     */
1951    public function hasView($view, $app = null)
1952    {
1953        switch ($view) {
1954        case self::VIEW_BASIC:
1955            // For now, consider all apps to have BASIC view.
1956            return true;
1957
1958        case self::VIEW_DYNAMIC:
1959            return $this->hasFeature('dynamicView', $app);
1960
1961        case self::VIEW_MINIMAL:
1962            return $this->hasFeature('minimalView', $app);
1963
1964        case self::VIEW_SMARTMOBILE:
1965            return $this->hasFeature('smartmobileView', $app);
1966        }
1967    }
1968
1969    /**
1970     * Set current view.
1971     *
1972     * @param integer $view  The view type.
1973     */
1974    public function setView($view = self::VIEW_BASIC)
1975    {
1976        $GLOBALS['session']->set('horde', 'view', $view);
1977    }
1978
1979    /**
1980     * Get current view.
1981     *
1982     * @return integer  The view type.
1983     */
1984    public function getView()
1985    {
1986        global $session;
1987
1988        return $session->exists('horde', 'view')
1989            ? $session->get('horde', 'view')
1990            : self::VIEW_BASIC;
1991    }
1992
1993    /**
1994     * Returns a list of available drivers for a library that are available
1995     * in an application.
1996     *
1997     * @param string $app     The application name.
1998     * @param string $prefix  The library prefix.
1999     *
2000     * @return array  The list of available class names.
2001     */
2002    public function getAppDrivers($app, $prefix)
2003    {
2004        $classes = array();
2005        $fileprefix = strtr($prefix, '_', '/');
2006        $fileroot = $this->get('fileroot', $app);
2007
2008        if (!is_null($fileroot)) {
2009            try {
2010                $pushed = $this->pushApp($app);
2011            } catch (Horde_Exception $e) {
2012                if ($e->getCode() == Horde_Registry::AUTH_FAILURE) {
2013                    return array();
2014                }
2015                throw $e;
2016            }
2017
2018            if (is_dir($fileroot . '/lib/' . $fileprefix)) {
2019                try {
2020                    $di = new DirectoryIterator($fileroot . '/lib/' . $fileprefix);
2021
2022                    foreach ($di as $val) {
2023                        if (!$val->isDir() && !$di->isDot()) {
2024                            $class = $app . '_' . $prefix . '_' . basename($val, '.php');
2025                            if (class_exists($class)) {
2026                                $classes[] = $class;
2027                            }
2028                        }
2029                    }
2030                } catch (UnexpectedValueException $e) {}
2031            }
2032
2033            if ($pushed) {
2034                $this->popApp();
2035            }
2036        }
2037
2038        return $classes;
2039    }
2040
2041    /**
2042     * Query the initial page for an application - the webroot, if there is no
2043     * initial_page set, and the initial_page, if it is set.
2044     *
2045     * @param string $app  The name of the application.
2046     *
2047     * @return string  URL pointing to the initial page of the application.
2048     * @throws Horde_Exception
2049     */
2050    public function getInitialPage($app = null)
2051    {
2052        try {
2053            if (($url = $this->callAppMethod($app, 'getInitialPage')) !== null) {
2054                return $url;
2055            }
2056        } catch (Horde_Exception $e) {}
2057
2058        if (($webroot = $this->get('webroot', $app)) !== null) {
2059            return $webroot . '/' . strval($this->get('initial_page', $app));
2060        }
2061
2062        throw new Horde_Exception(sprintf(
2063            Horde_Core_Translation::t("\"%s\" is not configured in the Horde Registry."),
2064            is_null($app) ? $this->getApp() : $app
2065        ));
2066    }
2067
2068    /**
2069     * Clears any authentication tokens in the current session.
2070     *
2071     * @param boolean $destroy  Destroy the session?
2072     */
2073    public function clearAuth($destroy = true)
2074    {
2075        global $session;
2076
2077        /* Do application logout tasks. */
2078        /* @todo: Replace with exclusively registered logout tasks. */
2079        foreach ($this->getAuthApps() as $app) {
2080            try {
2081                $this->callAppMethod($app, 'logout');
2082            } catch (Horde_Exception $e) {}
2083        }
2084
2085        /* Do registered logout tasks. */
2086        $logout = new Horde_Registry_Logout();
2087        $logout->run();
2088
2089        // @suspicious shouldn't this be 'auth/'
2090        $session->remove('horde', 'auth');
2091        $session->remove('horde', 'auth_app/');
2092
2093        $this->_cache['auth'] = null;
2094        $this->_cache['existing'] = $this->_cache['isauth'] = array();
2095
2096        if ($destroy) {
2097            $session->destroy();
2098        }
2099    }
2100
2101    /**
2102     * Clears authentication tokens for a given application in the current
2103     * session.
2104     *
2105     * @return boolean  If false, did not remove authentication token because
2106     *                  the application is in control of Horde's auth.
2107     */
2108    public function clearAuthApp($app)
2109    {
2110        global $session;
2111
2112        if ($session->get('horde', 'auth/credentials') == $app) {
2113            return false;
2114        }
2115
2116        if ($this->isAuthenticated(array('app' => $app, 'notransparent' => true))) {
2117            $this->callAppMethod($app, 'logout');
2118            $session->remove($app);
2119            $session->remove('horde', 'auth_app/' . $app);
2120            $session->remove('horde', 'auth_app_init/' . $app);
2121        }
2122
2123        unset(
2124            $this->_cache['existing'][$app],
2125            $this->_cache['isauth'][$app]
2126        );
2127
2128        return true;
2129    }
2130
2131    /**
2132     * Is a user an administrator?
2133     *
2134     * @param array $options  Options:
2135     *   - permission: (string) Allow users with this permission admin access
2136     *                 in the current context.
2137     *   - permlevel: (integer) The level of permissions to check for.
2138     *                Defaults to Horde_Perms::EDIT.
2139     *   - user: (string) The user to check.
2140     *           Defaults to self::getAuth().
2141     *
2142     * @return boolean  Whether or not this is an admin user.
2143     */
2144    public function isAdmin(array $options = array())
2145    {
2146        $user = isset($options['user'])
2147            ? $options['user']
2148            : $this->getAuth();
2149
2150        if ($user &&
2151            @is_array($GLOBALS['conf']['auth']['admins']) &&
2152            in_array($user, $GLOBALS['conf']['auth']['admins'])) {
2153            return true;
2154        }
2155
2156        return isset($options['permission'])
2157            ? $GLOBALS['injector']->getInstance('Horde_Perms')->hasPermission($options['permission'], $user, isset($options['permlevel']) ? $options['permlevel'] : Horde_Perms::EDIT)
2158            : false;
2159    }
2160
2161    /**
2162     * Checks if there is a session with valid auth information. If there
2163     * isn't, but the configured Auth driver supports transparent
2164     * authentication, then we try that.
2165     *
2166     * @param array $opts  Additional options:
2167     *   - app: (string) Check authentication for this app.
2168     *          DEFAULT: Checks horde-wide authentication.
2169     *   - notransparent: (boolean) Do not attempt transparent authentication.
2170     *                    DEFAULT: false
2171     *
2172     * @return boolean  Whether or not the user is authenticated.
2173     */
2174    public function isAuthenticated(array $opts = array())
2175    {
2176        global $injector, $session;
2177
2178        $app = empty($opts['app'])
2179            ? 'horde'
2180            : $opts['app'];
2181        $transparent = intval(empty($opts['notransparent']));
2182
2183        if (isset($this->_cache['isauth'][$app][$transparent])) {
2184            return $this->_cache['isauth'][$app][$transparent];
2185        }
2186
2187        /* Check for cached authentication results. */
2188        if ($this->getAuth() &&
2189            (($app == 'horde') ||
2190             $session->exists('horde', 'auth_app/' . $app)) &&
2191            $this->checkExistingAuth($app)) {
2192            $res = true;
2193        } elseif ($transparent) {
2194            try {
2195                $res = $injector->getInstance('Horde_Core_Factory_Auth')->create($app)->transparent();
2196            } catch (Horde_Exception $e) {
2197                Horde::log($e);
2198                $res = false;
2199            }
2200        } else {
2201            $res = false;
2202        }
2203
2204        $this->_cache['isauth'][$app][$transparent] = $res;
2205
2206        return $res;
2207    }
2208
2209    /**
2210     * Checks whether this process required authentication.
2211     *
2212     * @since 2.11.0
2213     *
2214     * @return boolean  True if the current process required authentication.
2215     */
2216    public function currentProcessAuth()
2217    {
2218        return ($this->_args['authentication'] !== 'none');
2219    }
2220
2221    /**
2222     * Returns a URL to the login screen, adding the necessary logout
2223     * parameters.
2224     *
2225     * If no reason/msg is passed in, uses the current global authentication
2226     * error message.
2227     *
2228     * @param array $options  Additional options:
2229     *     - app: (string) Authenticate to this application
2230     *            DEFAULT: Horde
2231     *     - msg: (string) If reason is Horde_Auth::REASON_MESSAGE, the message
2232     *            to display to the user.
2233     *            DEFAULT: None
2234     *     - params: (array) Additional params to add to the URL (not allowed:
2235     *               'app', 'horde_logout_token', 'msg', 'reason', 'url').
2236     *               DEFAULT: None
2237     *     - reason: (integer) The reason for logout
2238     *               DEFAULT: None
2239     *
2240     * @return Horde_Url  The formatted URL.
2241     */
2242    public function getLogoutUrl(array $options = array())
2243    {
2244        if (!isset($options['reason'])) {
2245            // TODO: This only returns the error for Horde-wide
2246            // authentication, not for application auth.
2247            $options['reason'] = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Auth')->create()->getError();
2248        }
2249
2250        $params = array();
2251        if (!in_array($options['reason'], array(Horde_Auth::REASON_LOGOUT, Horde_Auth::REASON_MESSAGE))) {
2252            $params['url'] = Horde::signUrl(Horde::selfUrl(true, true, true));
2253        }
2254
2255        if (empty($options['app']) ||
2256            ($options['app'] == 'horde') ||
2257            ($options['reason'] == Horde_Auth::REASON_LOGOUT)) {
2258            $params['horde_logout_token'] = $GLOBALS['session']->getToken();
2259       }
2260
2261        if (isset($options['app'])) {
2262            $params['app'] = $options['app'];
2263        }
2264
2265        if ($options['reason']) {
2266            $params['logout_reason'] = $options['reason'];
2267            if ($options['reason'] == Horde_Auth::REASON_MESSAGE) {
2268                $params['logout_msg'] = empty($options['msg'])
2269                    ? $GLOBALS['injector']->getInstance('Horde_Core_Factory_Auth')->create()->getError(true)
2270                    : $options['msg'];
2271            }
2272        }
2273
2274        return $this->getServiceLink('login', 'horde')->add($params)->setRaw(true);
2275    }
2276
2277    /**
2278     * Returns a URL to be used for downloading data.
2279     *
2280     * @param string $filename  The filename of the download data.
2281     * @param array $params     Additional URL parameters needed.
2282     *
2283     * @return Horde_Url  The download URL. This URL should be used as-is,
2284     *                    since the filename MUST be the last parameter added
2285     *                    to the URL.
2286     */
2287    public function downloadUrl($filename, array $params = array())
2288    {
2289        $url = $this->getServiceLink('download', $this->getApp())
2290            /* Add parameters. */
2291            ->add($params);
2292
2293        if (strlen($filename)) {
2294            /* Add the filename to the end of the URL. Although not necessary
2295             * for many browsers, this should allow every browser to download
2296             * correctly. */
2297            $url->add('fn', '/' . $filename);
2298        }
2299
2300        return $url;
2301    }
2302
2303    /**
2304     * Converts an authentication username to a unique Horde username.
2305     *
2306     * @param string $userId    The username to convert.
2307     * @param boolean $toHorde  If true, convert to a Horde username. If
2308     *                          false, convert to the auth username.
2309     *
2310     * @return string  The converted username.
2311     * @throws Horde_Exception
2312     */
2313    public function convertUsername($userId, $toHorde)
2314    {
2315        try {
2316            return $GLOBALS['injector']->getInstance('Horde_Core_Hooks')->
2317                callHook('authusername', 'horde', array($userId, $toHorde));
2318        } catch (Horde_Exception_HookNotSet $e) {
2319            return $userId;
2320        }
2321    }
2322
2323    /**
2324     * Returns the currently logged in user, if there is one.
2325     *
2326     * @param string $format  The return format, defaults to the unique Horde
2327     *                        ID. Alternative formats:
2328     *   - bare: (string) Horde ID without any domain information.
2329     *           EXAMPLE: foo@example.com would be returned as 'foo'.
2330     *   - domain: (string) Domain of the Horde ID.
2331     *             EXAMPLE: foo@example.com would be returned as 'example.com'.
2332     *   - original: (string) The username used to originally login to Horde.
2333     *
2334     * @return mixed  The user ID or false if no user is logged in.
2335     */
2336    public function getAuth($format = null)
2337    {
2338        global $session;
2339
2340        if (is_null($format) && !is_null($this->_cache['auth'])) {
2341            return $this->_cache['auth'];
2342        }
2343
2344        if (!isset($session)) {
2345            return false;
2346        }
2347
2348        if ($format == 'original') {
2349            return $session->exists('horde', 'auth/authId')
2350                ? $session->get('horde', 'auth/authId')
2351                : false;
2352        }
2353
2354        $user = $session->get('horde', 'auth/userId');
2355        if (is_null($user)) {
2356            return false;
2357        }
2358
2359        switch ($format) {
2360        case 'bare':
2361            return (($pos = strpos($user, '@')) === false)
2362                ? $user
2363                : substr($user, 0, $pos);
2364
2365        case 'domain':
2366            return (($pos = strpos($user, '@')) === false)
2367                ? false
2368                : substr($user, $pos + 1);
2369
2370        default:
2371            /* Specifically cache this result, since it generally is called
2372             * many times in a page. */
2373            $this->_cache['auth'] = $user;
2374            return $user;
2375        }
2376    }
2377
2378    /**
2379     * Return whether the authentication backend requested a password change.
2380     *
2381     * @return boolean  Whether the backend requested a password change.
2382     */
2383    public function passwordChangeRequested()
2384    {
2385        return (bool)$GLOBALS['session']->get('horde', 'auth/change');
2386    }
2387
2388    /**
2389     * Returns the requested credential for the currently logged in user, if
2390     * present.
2391     *
2392     * @param string $credential  The credential to retrieve.
2393     * @param string $app         The app to query. Defaults to Horde.
2394     *
2395     * @return mixed  The requested credential, all credentials if $credential
2396     *                is null, or false if no user is logged in.
2397     */
2398    public function getAuthCredential($credential = null, $app = null)
2399    {
2400        if (!$this->getAuth()) {
2401            return false;
2402        }
2403
2404        $credentials = $this->_getAuthCredentials($app);
2405
2406        return is_null($credential)
2407            ? $credentials
2408            : ((is_array($credentials) && isset($credentials[$credential]))
2409                   ? $credentials[$credential]
2410                   : false);
2411    }
2412
2413    /**
2414     * Sets the requested credential for the currently logged in user.
2415     *
2416     * @param mixed $credential  The credential to set.  If an array,
2417     *                           overwrites the current credentials array.
2418     * @param string $value      The value to set the credential to. If
2419     *                           $credential is an array, this value is
2420     *                           ignored.
2421     * @param string $app        The app to update. Defaults to Horde.
2422     */
2423    public function setAuthCredential($credential, $value = null, $app = null)
2424    {
2425        global $session;
2426
2427        if (!$this->getAuth()) {
2428            return;
2429        }
2430
2431        if (is_array($credential)) {
2432            $credentials = $credential;
2433        } else {
2434            if (($credentials = $this->_getAuthCredentials($app)) === false) {
2435                return;
2436            }
2437
2438            if (!is_array($credentials)) {
2439                $credentials = array();
2440            }
2441
2442            $credentials[$credential] = $value;
2443        }
2444
2445        $entry = $credentials;
2446        if (($base_app = $session->get('horde', 'auth/credentials')) &&
2447            ($session->get('horde', 'auth_app/' . $base_app) == $entry)) {
2448            $entry = true;
2449        }
2450
2451        if (is_null($app)) {
2452            $app = $base_app;
2453        }
2454
2455        /* The auth_app key contains application-specific authentication.
2456         * Session subkeys are the app names, values are an array containing
2457         * credentials. If the value is true, application does not require any
2458         * specific credentials. */
2459        $session->set('horde', 'auth_app/' . $app, $entry, $session::ENCRYPT);
2460        $session->set('horde', 'auth_app_init/' . $app, true);
2461
2462        unset(
2463            $this->_cache['existing'][$app],
2464            $this->_cache['isauth'][$app]
2465        );
2466    }
2467
2468    /**
2469     * Get the list of credentials for a given app.
2470     *
2471     * @param string $app  The application name.
2472     *
2473     * @return mixed  True, false, or the credential list.
2474     */
2475    protected function _getAuthCredentials($app)
2476    {
2477        global $session;
2478
2479        $base_app = $session->get('horde', 'auth/credentials');
2480        if (is_null($base_app)) {
2481            return false;
2482        }
2483
2484        if (is_null($app)) {
2485            $app = $base_app;
2486        }
2487
2488        if (!$session->exists('horde', 'auth_app/' . $app)) {
2489            return ($base_app != $app)
2490                ? $this->_getAuthCredentials($base_app)
2491                : false;
2492        }
2493
2494        return $session->get('horde', 'auth_app/' . $app);
2495    }
2496
2497    /**
2498     * Returns information about the remote host.
2499     *
2500     * @since 2.17.0
2501     *
2502     * @return object  An object with the following properties:
2503     * <pre>
2504     *   - addr: (string) Remote IP address.
2505     *   - host: (string) Remote hostname (if resolvable; otherwise, this value
2506     *           is identical to 'addr').
2507     *   - proxy: (boolean) True if this user is connecting through a proxy.
2508     * </pre>
2509     */
2510    public function remoteHost()
2511    {
2512        global $injector;
2513
2514        $out = new stdClass;
2515
2516        $dns = $injector->getInstance('Net_DNS2_Resolver');
2517        $old_error = error_reporting(0);
2518
2519        if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
2520            $remote_path = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
2521            $out->addr = $remote_path[0];
2522            $out->proxy = true;
2523        } else {
2524            $out->addr = $_SERVER['REMOTE_ADDR'];
2525            if (!empty($_SERVER['REMOTE_HOST'])) {
2526                $out->host = $_SERVER['REMOTE_HOST'];
2527            }
2528            $out->proxy = false;
2529        }
2530
2531        if ($dns && !isset($out->host)) {
2532            $out->host = $out->addr;
2533            try {
2534                if ($response = $dns->query($out->addr, 'PTR')) {
2535                    foreach ($response->answer as $val) {
2536                        if (isset($val->ptrdname)) {
2537                            $out->host = $val->ptrdname;
2538                            break;
2539                        }
2540                    }
2541                }
2542            } catch (Net_DNS2_Exception $e) {}
2543        } elseif (!isset($out->host)) {
2544            $out->host = gethostbyaddr($out->addr);
2545        }
2546        error_reporting($old_error);
2547
2548        return $out;
2549    }
2550
2551    /**
2552     * Sets data in the session saying that authorization has succeeded,
2553     * note which userId was authorized, and note when the login took place.
2554     *
2555     * If a user name hook was defined in the configuration, it gets applied
2556     * to the $userId at this point.
2557     *
2558     * @param string $authId      The userId that has been authorized.
2559     * @param array $credentials  The credentials of the user.
2560     * @param array $options      Additional options:
2561     *   - app: (string) The app to set authentication credentials for.
2562     *          DEFAULT: 'horde'
2563     *   - change: (boolean) Whether to request that the user change their
2564     *             password.
2565     *             DEFAULT: No
2566     *   - language: (string) The preferred language.
2567     *               DEFAULT: null
2568     *   - no_convert: (boolean) Don't convert the user name with the
2569     *                 authusername hook.
2570     *                 DEFAULT: false
2571     */
2572    public function setAuth($authId, $credentials, array $options = array())
2573    {
2574        global $browser, $injector, $session;
2575
2576        $app = empty($options['app'])
2577            ? 'horde'
2578            : $options['app'];
2579
2580        if ($this->getAuth() == $authId) {
2581            /* Store app credentials - base Horde session already exists. */
2582            $this->setAuthCredential($credentials, null, $app);
2583            return;
2584        }
2585
2586        /* Initial authentication to Horde. */
2587        $session->set('horde', 'auth/authId', $authId);
2588        $session->set('horde', 'auth/browser', $browser->getAgentString());
2589        if (!empty($options['change'])) {
2590            $session->set('horde', 'auth/change', 1);
2591        }
2592        $session->set('horde', 'auth/credentials', $app);
2593
2594        $remote = $this->remoteHost();
2595        $session->set('horde', 'auth/remoteAddr', $remote->addr);
2596
2597        $session->set('horde', 'auth/timestamp', time());
2598        $username = trim($authId);
2599        if (!empty($GLOBALS['conf']['auth']['lowercase'])) {
2600            $username = Horde_String::lower($username);
2601        }
2602        if (empty($options['no_convert'])) {
2603            $username = $this->convertUsername($username, true);
2604        }
2605        $session->set('horde', 'auth/userId', $username);
2606
2607        $this->_cache['auth'] = null;
2608        $this->_cache['existing'] = $this->_cache['isauth'] = array();
2609
2610        $this->setAuthCredential($credentials, null, $app);
2611
2612        /* Reload preferences for the new user. */
2613        unset($GLOBALS['prefs']);
2614        $this->loadPrefs($this->getApp());
2615
2616        $this->setLanguageEnvironment(isset($options['language']) ? $this->preferredLang($options['language']) : null, $app);
2617    }
2618
2619    /**
2620     * Check existing auth for triggers that might invalidate it.
2621     *
2622     * @param string $app  Check authentication for this app too.
2623     *
2624     * @return boolean  Is existing auth valid?
2625     */
2626    public function checkExistingAuth($app = 'horde')
2627    {
2628        global $browser, $conf, $injector, $session;
2629
2630        if (!empty($this->_cache['existing'][$app])) {
2631            return true;
2632        }
2633
2634        /* Tasks that only need to run once. */
2635        if (empty($this->_cache['existing'])) {
2636            if (!empty($conf['auth']['checkip']) &&
2637                ($remoteaddr = $session->get('horde', 'auth/remoteAddr')) &&
2638                ($remoteob = $this->remoteHost()) &&
2639                ($remoteaddr != $remoteob->addr)) {
2640                $injector->getInstance('Horde_Core_Factory_Auth')->create()
2641                    ->setError(Horde_Core_Auth_Application::REASON_SESSIONIP);
2642                return false;
2643            }
2644
2645            if (!empty($conf['auth']['checkbrowser']) &&
2646                ($session->get('horde', 'auth/browser') != $browser->getAgentString())) {
2647                $injector->getInstance('Horde_Core_Factory_Auth')->create()
2648                    ->setError(Horde_Core_Auth_Application::REASON_BROWSER);
2649                return false;
2650            }
2651
2652            if (!empty($conf['session']['max_time']) &&
2653                (($conf['session']['max_time'] + $session->begin) < time())) {
2654                $injector->getInstance('Horde_Core_Factory_Auth')->create()
2655                    ->setError(Horde_Core_Auth_Application::REASON_SESSIONMAXTIME);
2656                return false;
2657            }
2658        }
2659
2660        foreach (array_unique(array('horde', $app)) as $val) {
2661            if (!isset($this->_cache['existing'][$val])) {
2662                $auth = $injector->getInstance('Horde_Core_Factory_Auth')->create($val);
2663                if (!$auth->validateAuth()) {
2664                    if (!$auth->getError()) {
2665                        $auth->setError(Horde_Auth::REASON_SESSION);
2666                    }
2667                    return false;
2668                }
2669                $this->_cache['existing'][$val] = true;
2670            }
2671        }
2672
2673        return true;
2674    }
2675
2676    /**
2677     * Removes a user from the authentication backend and calls all
2678     * applications' removeUserData API methods.
2679     *
2680     * @param string $userId  The userId to delete.
2681     *
2682     * @throws Horde_Exception
2683     */
2684    public function removeUser($userId)
2685    {
2686        $GLOBALS['injector']
2687            ->getInstance('Horde_Core_Factory_Auth')
2688            ->create()
2689            ->removeUser($userId);
2690        $this->removeUserData($userId);
2691    }
2692
2693    /**
2694     * Removes user's application data.
2695     *
2696     * @param string $user  The user ID to delete.
2697     * @param string $app   If set, only removes data from this application.
2698     *                      By default, removes data from all apps.
2699     *
2700     * @throws Horde_Exception
2701     */
2702    public function removeUserData($user, $app = null)
2703    {
2704        if (!$this->isAdmin() && ($user != $this->getAuth())) {
2705            throw new Horde_Exception(Horde_Core_Translation::t("You are not allowed to remove user data."));
2706        }
2707
2708        $applist = empty($app)
2709            ? $this->listApps(
2710                array('notoolbar', 'hidden', 'active', 'admin', 'noadmin')
2711            )
2712            : array($app);
2713        $errApps = array();
2714        if (!empty($applist)) {
2715            $prefs_ob = $GLOBALS['injector']
2716                ->getInstance('Horde_Core_Factory_Prefs')
2717                ->create('horde', array('user' => $user));
2718
2719            // Remove all preference at once, if possible.
2720            if (empty($app)) {
2721                try {
2722                    $prefs_ob->removeAll();
2723                } catch (Horde_Exception $e) {
2724                    Horde::log($e);
2725                }
2726            }
2727        }
2728
2729        foreach ($applist as $item) {
2730            try {
2731                $this->callAppMethod($item, 'removeUserData', array(
2732                    'args' => array($user)
2733                ));
2734            } catch (Exception $e) {
2735                Horde::log($e);
2736                $errApps[] = $item;
2737            }
2738
2739            if (empty($app)) {
2740                continue;
2741            }
2742            try {
2743                $prefs_ob->retrieve($item);
2744                $prefs_ob->remove();
2745            } catch (Horde_Exception $e) {
2746                Horde::log($e);
2747                $errApps[] = $item;
2748            }
2749        }
2750
2751        if (count($errApps)) {
2752            throw new Horde_Exception(sprintf(Horde_Core_Translation::t("The following applications encountered errors removing user data: %s"), implode(', ', array_unique($errApps))));
2753        }
2754    }
2755
2756    /**
2757     * Returns authentication metadata information.
2758     *
2759     * @since 2.12.0
2760     *
2761     * @return array  Authentication metadata:
2762     *   - authId: (string) The username used during the original auth.
2763     *   - browser: (string) The remote browser string.
2764     *   - change: (boolean) Is a password change requested?
2765     *   - credentials: (string) The 'auth_app' entry that contains the Horde
2766     *                 credentials.
2767     *   - remoteAddr: (string) The remote IP address of the user.
2768     *   - timestamp: (integer) The login time.
2769     *   - userId: (string) The unique Horde username.
2770     */
2771    public function getAuthInfo()
2772    {
2773        global $session;
2774
2775        return $session->get('horde', 'auth/', $session::TYPE_ARRAY);
2776    }
2777
2778    /**
2779     * Returns the list of applications currently authenticated to.
2780     *
2781     * @since 2.12.0
2782     *
2783     * @return array  List of authenticated applications.
2784     */
2785    public function getAuthApps()
2786    {
2787        global $session;
2788
2789        return array_keys(
2790            $session->get('horde', 'auth_app/', $session::TYPE_ARRAY)
2791        );
2792    }
2793
2794    /* NLS functions. */
2795
2796    /**
2797     * Returns the charset for the current language.
2798     *
2799     * @return string  The character set that should be used with the current
2800     *                 locale settings.
2801     */
2802    public function getLanguageCharset()
2803    {
2804        return ($charset = $this->nlsconfig->curr_charset)
2805            ? $charset
2806            : 'ISO-8859-1';
2807    }
2808
2809    /**
2810     * Returns the charset to use for outgoing emails.
2811     *
2812     * @return string  The preferred charset for outgoing mails based on
2813     *                 the user's preferences and the current language.
2814     */
2815    public function getEmailCharset()
2816    {
2817        if (isset($GLOBALS['prefs']) &&
2818            ($charset = $GLOBALS['prefs']->getValue('sending_charset'))) {
2819            return $charset;
2820        }
2821
2822        return ($charset = $this->nlsconfig->curr_emails)
2823            ? $charset
2824            : $this->getLanguageCharset();
2825    }
2826
2827    /**
2828     * Selects the most preferred language for the current client session.
2829     *
2830     * @param string $lang  Force to use this language.
2831     *
2832     * @return string  The selected language abbreviation.
2833     */
2834    public function preferredLang($lang = null)
2835    {
2836        /* Check if we have a language set in the session */
2837        if ($GLOBALS['session']->exists('horde', 'language')) {
2838            return basename($GLOBALS['session']->get('horde', 'language'));
2839        }
2840
2841        /* If language pref exists, we should use that. */
2842        if (isset($GLOBALS['prefs']) &&
2843            ($language = $GLOBALS['prefs']->getValue('language'))) {
2844            return basename($language);
2845        }
2846
2847        /* Check if the user selected a language from the login screen */
2848        if (!empty($lang) && $this->nlsconfig->validLang($lang)) {
2849            return basename($lang);
2850        }
2851
2852        /* Try browser-accepted languages. */
2853        if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
2854            /* The browser supplies a list, so return the first valid one. */
2855            $browser_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
2856            foreach ($browser_langs as $lang) {
2857                /* Strip quality value for language */
2858                if (($pos = strpos($lang, ';')) !== false) {
2859                    $lang = substr($lang, 0, $pos);
2860                }
2861
2862                $lang = $this->_mapLang(trim($lang));
2863                if ($this->nlsconfig->validLang($lang)) {
2864                    return basename($lang);
2865                }
2866
2867                /* In case there's no full match, save our best guess. Try
2868                 * ll_LL, followed by just ll. */
2869                if (!isset($partial_lang)) {
2870                    $ll_LL = Horde_String::lower(substr($lang, 0, 2)) . '_' . Horde_String::upper(substr($lang, 0, 2));
2871                    if ($this->nlsconfig->validLang($ll_LL)) {
2872                        $partial_lang = $ll_LL;
2873                    } else {
2874                        $ll = $this->_mapLang(substr($lang, 0, 2));
2875                        if ($this->nlsconfig->validLang($ll))  {
2876                            $partial_lang = $ll;
2877                        }
2878                    }
2879                }
2880            }
2881
2882            if (isset($partial_lang)) {
2883                return basename($partial_lang);
2884            }
2885        }
2886
2887        /* Use site-wide default, if one is defined */
2888        return $this->nlsconfig->curr_default
2889            ? basename($this->nlsconfig->curr_default)
2890            /* No dice auto-detecting, default to US English. */
2891            : 'en_US';
2892    }
2893
2894    /**
2895     * Sets the language.
2896     *
2897     * @param string $lang  The language abbreviation.
2898     *
2899     * @return string  The current language (since 2.5.0).
2900     *
2901     * @throws Horde_Exception
2902     */
2903    public function setLanguage($lang = null)
2904    {
2905        if (empty($lang) || !$this->nlsconfig->validLang($lang)) {
2906            $lang = $this->preferredLang();
2907        }
2908
2909        $GLOBALS['session']->set('horde', 'language', $lang);
2910
2911        $changed = false;
2912        if (isset($GLOBALS['language'])) {
2913            if ($GLOBALS['language'] == $lang) {
2914                return $lang;
2915            }
2916            $changed = true;
2917        }
2918
2919        $GLOBALS['language'] = $lang;
2920
2921        $lang_charset = $lang . '.UTF-8';
2922        if (setlocale(LC_ALL, $lang_charset)) {
2923            putenv('LC_ALL=' . $lang_charset);
2924            putenv('LANG=' . $lang_charset);
2925            putenv('LANGUAGE=' . $lang_charset);
2926        } else {
2927            $changed = false;
2928        }
2929
2930        if ($changed) {
2931            $this->rebuild();
2932
2933            $this->_cache['cfile'] = array();
2934
2935            foreach ($this->listApps() as $app) {
2936                if ($this->isAuthenticated(array('app' => $app, 'notransparent' => true))) {
2937                    $this->callAppMethod($app, 'changeLanguage');
2938                }
2939            }
2940        }
2941
2942        return $lang;
2943    }
2944
2945    /**
2946     * Sets the language and reloads the whole NLS environment.
2947     *
2948     * When setting the language, the gettext catalogs have to be reloaded
2949     * too, charsets have to be updated etc. This method takes care of all
2950     * this.
2951     *
2952     * @param string $lang  The new language.
2953     * @param string $app   The application for reloading the gettext catalog.
2954     *                      Uses current application if null.
2955     */
2956    public function setLanguageEnvironment($lang = null, $app = null)
2957    {
2958        if (empty($app)) {
2959            $app = $this->getApp();
2960        }
2961
2962        $old_lang = $this->setLanguage($lang);
2963
2964        $this->setTextdomain(
2965            $app,
2966            $this->get('fileroot', $app) . '/locale'
2967        );
2968
2969        if ($old_lang == $GLOBALS['language']) {
2970            return;
2971        }
2972
2973        $GLOBALS['session']->remove('horde', 'nls/');
2974    }
2975
2976    /**
2977     * Sets the gettext domain.
2978     *
2979     * @param string $app        The application name.
2980     * @param string $directory  The directory where the application's
2981     *                           LC_MESSAGES directory resides.
2982     */
2983    public function setTextdomain($app, $directory)
2984    {
2985        bindtextdomain($app, $directory);
2986        textdomain($app);
2987
2988        /* The existence of this function depends on the platform. */
2989        if (function_exists('bind_textdomain_codeset')) {
2990            bind_textdomain_codeset($app, 'UTF-8');
2991        }
2992    }
2993
2994    /**
2995     * Sets the current timezone, if available.
2996     */
2997    public function setTimeZone()
2998    {
2999        $tz = $GLOBALS['prefs']->getValue('timezone');
3000        if (!empty($tz)) {
3001            @date_default_timezone_set($tz);
3002        }
3003    }
3004
3005    /**
3006     * Maps languages with common two-letter codes (such as nl) to the full
3007     * locale code (in this case, nl_NL). Returns the language unmodified if
3008     * it isn't an alias.
3009     *
3010     * @param string $language  The language code to map.
3011     *
3012     * @return string  The mapped language code.
3013     */
3014    protected function _mapLang($language)
3015    {
3016        // Translate the $language to get broader matches.
3017        // (eg. de-DE should match de_DE)
3018        $trans_lang = str_replace('-', '_', $language);
3019        $lang_parts = explode('_', $trans_lang);
3020        $trans_lang = Horde_String::lower($lang_parts[0]);
3021        if (isset($lang_parts[1])) {
3022            $trans_lang .= '_' . Horde_String::upper($lang_parts[1]);
3023        }
3024
3025        return empty($this->nlsconfig->aliases[$trans_lang])
3026            ? $trans_lang
3027            : $this->nlsconfig->aliases[$trans_lang];
3028    }
3029
3030    /**
3031     * Is the registry in 'test' mode?
3032     *
3033     * @since 2.12.0
3034     *
3035     * @return boolean  True if in testing mode.
3036     */
3037    public function isTest()
3038    {
3039        return !empty($this->_args['test']);
3040    }
3041
3042}
3043