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