Completed
Push — master ( c85f1f...df263c )
by Ralf
74:39 queued 51:38
created

api/src/Session.php (1 issue)

1
<?php
2
/**
3
 * EGroupware API: session handling
4
 *
5
 * This class is based on the old phpgwapi/inc/class.sessions(_php4).inc.php:
6
 * (c) 1998-2000 NetUSE AG Boris Erdmann, Kristian Koehntopp
7
 * (c) 2003 FreeSoftware Foundation
8
 * Not sure how much the current code still has to do with it.
9
 *
10
 * Former authers were:
11
 * - NetUSE AG Boris Erdmann, Kristian Koehntopp
12
 * - Dan Kuykendall <[email protected]>
13
 * - Joseph Engo <[email protected]>
14
 *
15
 * @link http://www.egroupware.org
16
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
17
 * @package api
18
 * @subpackage session
19
 * @author Ralf Becker <[email protected]> since 2003 on
20
 */
21
22
namespace EGroupware\Api;
23
24
use PragmaRX\Google2FA;
25
use EGroupware\Api\Mail\Credentials;
26
use EGroupware\OpenID;
27
use League\OAuth2\Server\Exception\OAuthServerException;
28
29
/**
30
 * Create, verifies or destroys an EGroupware session
31
 *
32
 * If you want to analyse the memory usage in the session, you can uncomment the following call:
33
 *
34
 * 	static function encrypt($kp3)
35
 *	{
36
 *		// switch that on to analyse memory usage in the session
37
 *		//self::log_session_usage($_SESSION[self::EGW_APPSESSION_VAR],'_SESSION['.self::EGW_APPSESSION_VAR.']',true,5000);
38
 */
39
class Session
40
{
41
	/**
42
	 * Write debug messages about session verification and creation to the error_log
43
	 *
44
	 * This will contain passwords! Don't leave it permanently switched on!
45
	 */
46
	const ERROR_LOG_DEBUG = false;
47
48
	/**
49
	 * key of eGW's session-data in $_SESSION
50
	 */
51
	const EGW_SESSION_VAR = 'egw_session';
52
53
	/**
54
	 * key of eGW's application session-data in $_SESSION
55
	 */
56
	const EGW_APPSESSION_VAR = 'egw_app_session';
57
58
	/**
59
	 * key of eGW's required files in $_SESSION
60
	 *
61
	 * These files get set by Db and Egw class, for classes which get not autoloaded (eg. ADOdb, idots_framework)
62
	 */
63
	const EGW_REQUIRED_FILES = 'egw_required_files';
64
65
	/**
66
	 * key of  eGW's egw_info cached in $_SESSION
67
	 */
68
	const EGW_INFO_CACHE = 'egw_info_cache';
69
70
	/**
71
	 * key of  eGW's egw object cached in $_SESSION
72
	 */
73
	const EGW_OBJECT_CACHE = 'egw_object_cache';
74
75
	/**
76
	 * Name of cookie or get-parameter with session-id
77
	 */
78
	const EGW_SESSION_NAME = 'sessionid';
79
80
	/**
81
	 * Name of cookie with remember me token
82
	 */
83
	const REMEMBER_ME_COOKIE = 'eGW_remember';
84
85
	/**
86
	* current user login (account_lid@domain)
87
	*
88
	* @var string
89
	*/
90
	var $login;
91
92
	/**
93
	* current user password
94
	*
95
	* @var string
96
	*/
97
	var $passwd;
98
99
	/**
100
	* current user db/ldap account id
101
	*
102
	* @var int
103
	*/
104
	var $account_id;
105
106
	/**
107
	* current user account login id (without the eGW-domain/-instance part
108
	*
109
	* @var string
110
	*/
111
	var $account_lid;
112
113
	/**
114
	* domain for current user
115
	*
116
	* @var string
117
	*/
118
	var $account_domain;
119
120
	/**
121
	* type flag, A - anonymous session, N - None, normal session
122
	*
123
	* @var string
124
	*/
125
	var $session_flags;
126
127
	/**
128
	* current user session id
129
	*
130
	* @var string
131
	*/
132
	var $sessionid;
133
134
	/**
135
	* an other session specific id (md5 from a random string),
136
	* used together with the sessionid for xmlrpc basic auth and the encryption of session-data (if that's enabled)
137
	*
138
	* @var string
139
	*/
140
	var $kp3;
141
142
	/**
143
	 * Primary key of egw_access_log row for updates
144
	 *
145
	 * @var int
146
	 */
147
	var $sessionid_access_log;
148
149
	/**
150
	* name of XML-RPC/SOAP method called
151
	*
152
	* @var string
153
	*/
154
	var $xmlrpc_method_called;
155
156
	/**
157
	* Array with the name of the system domains
158
	*
159
	* @var array
160
	*/
161
	private $egw_domains;
162
163
	/**
164
	 * $_SESSION at the time the constructor was called
165
	 *
166
	 * @var array
167
	 */
168
	var $required_files;
169
170
	/**
171
	 * Nummeric code why session creation failed
172
	 *
173
	 * @var int
174
	 */
175
	var $cd_reason;
176
	const CD_BAD_LOGIN_OR_PASSWORD = 5;
177
	const CD_SECOND_FACTOR_REQUIRED = 96;
178
	const CD_FORCE_PASSWORD_CHANGE = 97;
179
	const CD_ACCOUNT_EXPIRED = 98;
180
	const CD_BLOCKED = 99;	// to many failed attempts to loing
181
182
	/**
183
	 * Verbose reason why session creation failed
184
	 *
185
	 * @var string
186
	 */
187
	var $reason;
188
189
	/**
190
	 * Session action set by update_dla or set_action and stored in __destruct
191
	 *
192
	 * @var string
193
	 */
194
	protected $action;
195
196
	/**
197
	 * Constructor just loads up some defaults from cookies
198
	 *
199
	 * @param array $domain_names =null domain-names used in this install
200
	 */
201
	function __construct(array $domain_names=null)
202
	{
203
		$this->required_files = $_SESSION[self::EGW_REQUIRED_FILES];
204
205
		$this->sessionid = self::get_sessionid();
206
		$this->kp3       = self::get_request('kp3');
207
208
		$this->egw_domains = $domain_names;
209
210
		if (!isset($GLOBALS['egw_setup']))
211
		{
212
			// verfiy and if necessary create and save our config settings
213
			//
214
			$save_rep = false;
215
			if (!isset($GLOBALS['egw_info']['server']['max_access_log_age']))
216
			{
217
				$GLOBALS['egw_info']['server']['max_access_log_age'] = 90;	// default 90 days
218
				$save_rep = true;
219
			}
220
			if (!isset($GLOBALS['egw_info']['server']['block_time']))
221
			{
222
				$GLOBALS['egw_info']['server']['block_time'] = 1;	// default 1min, its enough to slow down brute force attacks
223
				$save_rep = true;
224
			}
225
			if (!isset($GLOBALS['egw_info']['server']['num_unsuccessful_id']))
226
			{
227
				$GLOBALS['egw_info']['server']['num_unsuccessful_id']  = 3;	// default 3 trys per id
228
				$save_rep = true;
229
			}
230
			if (!isset($GLOBALS['egw_info']['server']['num_unsuccessful_ip']))
231
			{
232
				$GLOBALS['egw_info']['server']['num_unsuccessful_ip']  = $GLOBALS['egw_info']['server']['num_unsuccessful_id'] * 5;	// default is 5 times as high as the id default; since accessing via proxy is quite common
233
				$save_rep = true;
234
			}
235
			if (!isset($GLOBALS['egw_info']['server']['install_id']))
236
			{
237
				$GLOBALS['egw_info']['server']['install_id']  = md5(Auth::randomstring(15));
238
			}
239
			if (!isset($GLOBALS['egw_info']['server']['max_history']))
240
			{
241
				$GLOBALS['egw_info']['server']['max_history'] = 20;
242
				$save_rep = true;
243
			}
244
245
			if ($save_rep)
246
			{
247
				$config = new Config('phpgwapi');
248
				$config->read_repository();
249
				$config->value('max_access_log_age',$GLOBALS['egw_info']['server']['max_access_log_age']);
250
				$config->value('block_time',$GLOBALS['egw_info']['server']['block_time']);
251
				$config->value('num_unsuccessful_id',$GLOBALS['egw_info']['server']['num_unsuccessful_id']);
252
				$config->value('num_unsuccessful_ip',$GLOBALS['egw_info']['server']['num_unsuccessful_ip']);
253
				$config->value('install_id',$GLOBALS['egw_info']['server']['install_id']);
254
				$config->value('max_history',$GLOBALS['egw_info']['server']['max_history']);
255
				try
256
				{
257
					$config->save_repository();
258
				}
259
				catch (Db\Exception $e) {
260
					_egw_log_exception($e);	// ignore exception, as it blocks session creation, if database is not writable
261
				}
262
			}
263
		}
264
		self::set_cookiedomain();
265
266
		// set session_timeout from global php.ini and default to 14400=4h, if not set
267
		if (!($GLOBALS['egw_info']['server']['sessions_timeout'] = ini_get('session.gc_maxlifetime')))
268
      	{
269
      		ini_set('session.gc_maxlifetime', $GLOBALS['egw_info']['server']['sessions_timeout']=14400);
270
      	}
271
	}
272
273
	/**
274
	 * Magic function called when this class get's restored from the session
275
	 *
276
	 */
277
	function __wakeup()
278
	{
279
		if (!empty($GLOBALS['egw_info']['server']['sessions_timeout']) && session_status() === PHP_SESSION_NONE)
280
		{
281
			ini_set('session.gc_maxlifetime', $GLOBALS['egw_info']['server']['sessions_timeout']);
282
		}
283
		$this->action = null;
284
	}
285
286
	/**
287
	 * Destructor: update access-log and encrypt session
288
	 */
289
	function __destruct()
290
	{
291
		self::encrypt($this->kp3);
292
	}
293
294
	/**
295
	 * commit the sessiondata to storage
296
	 *
297
	 * It's necessary to use this function instead of session_write_close() direct, as otherwise the session is not encrypted!
298
	 */
299
	function commit_session()
300
	{
301
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."() sessionid=$this->sessionid, _SESSION[".self::EGW_SESSION_VAR.']='.array2string($_SESSION[self::EGW_SESSION_VAR]).' '.function_backtrace());
302
		self::encrypt($this->kp3);
303
304
		session_write_close();
305
	}
306
307
	/**
308
	 * Keys of session variables which get encrypted
309
	 *
310
	 * @var array
311
	 */
312
	static $egw_session_vars = array(
313
		//self::EGW_SESSION_VAR, no need to encrypt and required by the session list
314
		self::EGW_APPSESSION_VAR,
315
		self::EGW_INFO_CACHE,
316
		self::EGW_OBJECT_CACHE,
317
	);
318
319
	static $mcrypt;
320
321
	/**
322
	 * Name of flag in session to signal it is encrypted or not
323
	 */
324
	const EGW_SESSION_ENCRYPTED = 'egw_session_encrypted';
325
326
	/**
327
	 * Encrypt the variables in the session
328
	 *
329
	 * Is called by self::__destruct().
330
	 */
331
	static function encrypt($kp3)
332
	{
333
		// switch that on to analyse memory usage in the session
334
		//self::log_session_usage($_SESSION[self::EGW_APPSESSION_VAR],'_SESSION['.self::EGW_APPSESSION_VAR.']',true,5000);
335
336
		if (!isset($_SESSION[self::EGW_SESSION_ENCRYPTED]) && self::init_crypt($kp3))
337
		{
338
			foreach(self::$egw_session_vars as $name)
339
			{
340
				if (isset($_SESSION[$name]))
341
				{
342
					$_SESSION[$name] = mcrypt_generic(self::$mcrypt,serialize($_SESSION[$name]));
343
					//error_log(__METHOD__."() 'encrypting' session var: $name, len=".strlen($_SESSION[$name]));
344
				}
345
			}
346
			$_SESSION[self::EGW_SESSION_ENCRYPTED] = true;	// flag session as encrypted
347
348
			mcrypt_generic_deinit(self::$mcrypt);
349
			self::$mcrypt = null;
350
		}
351
	}
352
353
	/**
354
	 * Log the usage of session-vars
355
	 *
356
	 * @param array &$arr
357
	 * @param string $label
358
	 * @param boolean $recursion =true if true call itself for every item > $limit
359
	 * @param int $limit =1000 log only differences > $limit
360
	 */
361
	static function log_session_usage(&$arr,$label,$recursion=true,$limit=1000)
362
	{
363
		if (!is_array($arr)) return;
364
365
		$sizes = array();
366
		foreach($arr as $key => &$data)
367
		{
368
			$sizes[$key] = strlen(serialize($data));
369
		}
370
		arsort($sizes,SORT_NUMERIC);
371
		foreach($sizes as $key => $size)
372
		{
373
			$diff = $size - (int)$_SESSION[$label.'-sizes'][$key];
374
			$_SESSION[$label.'-sizes'][$key] = $size;
375
			if ($diff > $limit)
376
			{
377
				error_log("strlen({$label}[$key])=".Vfs::hsize($size).", diff=".Vfs::hsize($diff));
378
				if ($recursion) self::log_session_usage($arr[$key],$label.'['.$key.']',$recursion,$limit);
379
			}
380
		}
381
	}
382
383
	/**
384
	 * Decrypt the variables in the session
385
	 *
386
	 * Is called by self::init_handler from api/src/loader.php (called from the header.inc.php)
387
	 * before the restore of the eGW enviroment takes place, so that the whole thing can be encrypted
388
	 */
389
	static function decrypt()
390
	{
391
		if ($_SESSION[self::EGW_SESSION_ENCRYPTED] && self::init_crypt(self::get_request('kp3')))
392
		{
393
			foreach(self::$egw_session_vars as $name)
394
			{
395
				if (isset($_SESSION[$name]))
396
				{
397
					$_SESSION[$name] = unserialize(trim(mdecrypt_generic(self::$mcrypt,$_SESSION[$name])));
398
					//error_log(__METHOD__."() 'decrypting' session var $name: gettype($name) = ".gettype($_SESSION[$name]));
399
				}
400
			}
401
			unset($_SESSION[self::EGW_SESSION_ENCRYPTED]);	// delete encryption flag
402
		}
403
	}
404
405
	/**
406
	 * Check if session encryption is configured, possible and initialise it
407
	 *
408
	 * If mcrypt extension is not available (eg. in PHP 7.2+ no longer contains it) fail gracefully.
409
	 *
410
	 * @param string $kp3 mcrypt key transported via cookie or get parameter like the session id,
411
	 *	unlike the session id it's not know on the server, so only the client-request can decrypt the session!
412
	 * @return boolean true if encryption is used, false otherwise
413
	 */
414
	static private function init_crypt($kp3)
415
	{
416
		if(!$GLOBALS['egw_info']['server']['mcrypt_enabled'])
417
		{
418
			return false;	// session encryption is switched off
419
		}
420
		if ($GLOBALS['egw_info']['currentapp'] == 'syncml' || !$kp3)
421
		{
422
			$kp3 = 'staticsyncmlkp3';	// syncml has no kp3!
423
		}
424
		if (is_null(self::$mcrypt))
425
		{
426
			if (!check_load_extension('mcrypt'))
427
			{
428
				error_log(__METHOD__."() required PHP extension mcrypt not loaded and can not be loaded, sessions get NOT encrypted!");
429
				return false;
430
			}
431
			if (!(self::$mcrypt = mcrypt_module_open(MCRYPT_TRIPLEDES, '', MCRYPT_MODE_ECB, '')))
432
			{
433
				error_log(__METHOD__."() could not mcrypt_module_open(MCRYPT_TRIPLEDES,'',MCRYPT_MODE_ECB,''), sessions get NOT encrypted!");
434
				return false;
435
			}
436
			$iv_size = mcrypt_enc_get_iv_size(self::$mcrypt);
437
			$iv = !isset($GLOBALS['egw_info']['server']['mcrypt_iv']) || strlen($GLOBALS['egw_info']['server']['mcrypt_iv']) < $iv_size ?
438
				mcrypt_create_iv ($iv_size, MCRYPT_RAND) : substr($GLOBALS['egw_info']['server']['mcrypt_iv'],0,$iv_size);
439
440
			if (mcrypt_generic_init(self::$mcrypt,$kp3, $iv) < 0)
441
			{
442
				error_log(__METHOD__."() could not initialise mcrypt, sessions get NOT encrypted!");
443
				return self::$mcrypt = false;
444
			}
445
		}
446
		return is_resource(self::$mcrypt);
447
	}
448
449
	/**
450
	 * Create a new eGW session
451
	 *
452
	 * @param string $login user login
453
	 * @param string $passwd user password
454
	 * @param string $passwd_type type of password being used, ie plaintext, md5, sha1
455
	 * @param boolean $no_session =false dont create a real session, eg. for GroupDAV clients using only basic auth, no cookie support
456
	 * @param boolean $auth_check =true if false, the user is loged in without checking his password (eg. for single sign on), default = true
457
	 * @param boolean $fail_on_forced_password_change =false true: do NOT create session, if password change requested
458
	 * @param string|boolean $check_2fa =false string: 2fa-code to check (only if exists) and fail if wrong, false: do NOT check 2fa
459
	 * @param string $remember_me =null "True" for checkbox checked, or periode for user-choice select-box eg. "P1W" or "" for NOT remember
460
	 * @return string|boolean session id or false if session was not created, $this->(cd_)reason contains cause
461
	 */
462
	function create($login,$passwd = '',$passwd_type = '',$no_session=false,$auth_check=true,$fail_on_forced_password_change=false,$check_2fa=false,$remember_me=null)
463
	{
464
		try {
465
			if (is_array($login))
466
			{
467
				$this->login       = $login['login'];
468
				$this->passwd      = $login['passwd'];
469
				$this->passwd_type = $login['passwd_type'];
470
				$login             = $this->login;
471
			}
472
			else
473
			{
474
				$this->login       = $login;
475
				$this->passwd      = $passwd;
476
				$this->passwd_type = $passwd_type;
477
			}
478
			if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) starting ...");
479
480
			self::split_login_domain($login,$this->account_lid,$this->account_domain);
481
			// add domain to the login, if not already there
482
			if (substr($this->login,-strlen($this->account_domain)-1) != '@'.$this->account_domain)
483
			{
484
				$this->login .= '@'.$this->account_domain;
485
			}
486
			$now = time();
487
			//error_log(__METHOD__."($login,$passwd,$passwd_type,$no_session,$auth_check) account_lid=$this->account_lid, account_domain=$this->account_domain, default_domain={$GLOBALS['egw_info']['server']['default_domain']}, user/domain={$GLOBALS['egw_info']['user']['domain']}");
488
489
			// This is to ensure that we authenticate to the correct domain (might not be default)
490
			// if no domain is given we use the default domain, so we dont need to re-create everything
491
			if (!$GLOBALS['egw_info']['user']['domain'] && $this->account_domain == $GLOBALS['egw_info']['server']['default_domain'])
492
			{
493
				$GLOBALS['egw_info']['user']['domain'] = $this->account_domain;
494
			}
495
			elseif (!$this->account_domain && $GLOBALS['egw_info']['user']['domain'])
496
			{
497
				$this->account_domain = $GLOBALS['egw_info']['user']['domain'];
498
			}
499
			elseif($this->account_domain != $GLOBALS['egw_info']['user']['domain'])
500
			{
501
				throw new Exception("Wrong domain! '$this->account_domain' != '{$GLOBALS['egw_info']['user']['domain']}'");
502
			}
503
			unset($GLOBALS['egw_info']['server']['default_domain']); // we kill this for security reasons
504
505
			$user_ip = self::getuser_ip();
506
507
			$this->account_id = $GLOBALS['egw']->accounts->name2id($this->account_lid,'account_lid','u');
508
509
			// do we need to check 'remember me' token (to bypass authentication)
510
			if ($auth_check && !empty($_COOKIE[self::REMEMBER_ME_COOKIE]))
511
			{
512
				$auth_check = !$this->skipPasswordAuth($_COOKIE[self::REMEMBER_ME_COOKIE], $this->account_id);
513
			}
514
515
			if (($blocked = $this->login_blocked($login,$user_ip)) ||	// too many unsuccessful attempts
516
				$GLOBALS['egw_info']['server']['global_denied_users'][$this->account_lid] ||
517
				$auth_check && !$GLOBALS['egw']->auth->authenticate($this->account_lid, $this->passwd, $this->passwd_type) ||
518
				$this->account_id && $GLOBALS['egw']->accounts->get_type($this->account_id) == 'g')
519
			{
520
				$this->reason = $blocked ? 'blocked, too many attempts' : 'bad login or password';
521
				$this->cd_reason = $blocked ? self::CD_BLOCKED : self::CD_BAD_LOGIN_OR_PASSWORD;
522
523
				// we dont log anon users as it would block the website
524
				if (!$GLOBALS['egw']->acl->get_specific_rights_for_account($this->account_id,'anonymous','phpgwapi'))
525
				{
526
					$this->log_access($this->reason,$login,$user_ip,0);	// log unsuccessfull login
527
				}
528
				if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) UNSUCCESSFULL ($this->reason)");
529
				return false;
530
			}
531
532
			if (!$this->account_id && $GLOBALS['egw_info']['server']['auto_create_acct'])
533
			{
534
				if ($GLOBALS['egw_info']['server']['auto_create_acct'] == 'lowercase')
535
				{
536
					$this->account_lid = strtolower($this->account_lid);
537
				}
538
				$this->account_id = $GLOBALS['egw']->accounts->auto_add($this->account_lid, $passwd);
539
			}
540
			// fix maybe wrong case in username, it makes problems eg. in filemanager (name of homedir)
541
			if ($this->account_lid != ($lid = $GLOBALS['egw']->accounts->id2name($this->account_id)))
542
			{
543
				$this->account_lid = $lid;
544
				$this->login = $lid.substr($this->login,strlen($lid));
545
			}
546
547
			$GLOBALS['egw_info']['user']['account_id'] = $this->account_id;
548
549
			// for *DAV and eSync we use a pseudo sessionid created from md5(user:passwd)
550
			// --> allows this stateless protocolls which use basic auth to use sessions!
551
			if (($this->sessionid = self::get_sessionid(true)))
552
			{
553
				if (session_status() !== PHP_SESSION_ACTIVE)	// gives warning including password
554
				{
555
					session_id($this->sessionid);
556
				}
557
			}
558
			else
559
			{
560
				self::cache_control();
561
				session_start();
562
				// set a new session-id, if not syncml (already done in Horde code and can NOT be changed)
563
				if (!$no_session && $GLOBALS['egw_info']['flags']['currentapp'] != 'syncml')
564
				{
565
					session_regenerate_id(true);
566
				}
567
				$this->sessionid = session_id();
568
			}
569
			$this->kp3       = Auth::randomstring(24);
570
571
			$GLOBALS['egw_info']['user'] = $this->read_repositories();
572
			if ($GLOBALS['egw']->accounts->is_expired($GLOBALS['egw_info']['user']))
573
			{
574
				$this->reason = 'account is expired';
575
				$this->cd_reason = self::CD_ACCOUNT_EXPIRED;
576
577
				if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) UNSUCCESSFULL ($this->reason)");
578
				return false;
579
			}
580
581
			Cache::setSession('phpgwapi', 'password', base64_encode($this->passwd));
582
583
			// if we have a second factor, check it before forced password change
584
			if ($check_2fa !== false)
585
			{
586
				try {
587
					$this->checkMultifactorAuth($check_2fa, $_COOKIE[self::REMEMBER_ME_COOKIE]);
588
				}
589
				catch(\Exception $e) {
590
					$this->cd_reason = $e->getCode();
591
					$this->reason = $e->getMessage();
592
					$this->log_access($this->reason, $login, $user_ip, 0);	// log unsuccessfull login
593
					if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check,$fail_on_forced_password_change,'$check_2fa') UNSUCCESSFULL ($this->reason)");
594
					return false;
595
				}
596
			}
597
598
			if ($fail_on_forced_password_change && Auth::check_password_change($this->reason) === false)
599
			{
600
				$this->cd_reason = self::CD_FORCE_PASSWORD_CHANGE;
601
				return false;
602
			}
603
604
			if ($GLOBALS['egw']->acl->check('anonymous',1,'phpgwapi'))
605
			{
606
				$this->session_flags = 'A';
607
			}
608
			else
609
			{
610
				$this->session_flags = 'N';
611
			}
612
613
			if (($hook_result = Hooks::process(array(
614
				'location'       => 'session_creation',
615
				'sessionid'      => $this->sessionid,
616
				'session_flags'  => $this->session_flags,
617
				'account_id'     => $this->account_id,
618
				'account_lid'    => $this->account_lid,
619
				'passwd'         => $this->passwd,
620
				'account_domain' => $this->account_domain,
621
				'user_ip'        => $user_ip,
622
			),'',true)))	// true = run hooks from all apps, not just the ones the current user has perms to run
623
			{
624
				foreach($hook_result as $reason)
625
				{
626
					if ($reason)	// called hook requests to deny the session
627
					{
628
						$this->reason = $this->cd_reason = $reason;
629
						$this->log_access($this->reason,$login,$user_ip,0);		// log unsuccessfull login
630
						if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) UNSUCCESSFULL ($this->reason)");
631
						return false;
632
					}
633
				}
634
			}
635
			$GLOBALS['egw']->db->transaction_begin();
636
			$this->register_session($this->login,$user_ip,$now,$this->session_flags);
637
			if ($this->session_flags != 'A')		// dont log anonymous sessions
638
			{
639
				$this->sessionid_access_log = $this->log_access($this->sessionid,$login,$user_ip,$this->account_id);
640
				// We do NOT log anonymous sessions to not block website and also to cope with
641
				// high rate anon endpoints might be called creating a bottleneck in the egw_accounts table.
642
				Cache::setSession('phpgwapi', 'account_previous_login', $GLOBALS['egw']->auth->previous_login);
643
				$GLOBALS['egw']->accounts->update_lastlogin($this->account_id,$user_ip);
644
			}
645
			$GLOBALS['egw']->db->transaction_commit();
646
647
			if ($GLOBALS['egw_info']['server']['usecookies'] && !$no_session)
648
			{
649
				self::egw_setcookie(self::EGW_SESSION_NAME,$this->sessionid);
650
				self::egw_setcookie('kp3',$this->kp3);
651
				self::egw_setcookie('domain',$this->account_domain);
652
			}
653
			if ($GLOBALS['egw_info']['server']['usecookies'] && !$no_session || isset($_COOKIE['last_loginid']))
654
			{
655
				self::egw_setcookie('last_loginid', $this->account_lid ,$now+1209600); /* For 2 weeks */
656
				self::egw_setcookie('last_domain',$this->account_domain,$now+1209600);
657
			}
658
659
			// set new remember me token/cookie, if requested and necessary
660
			$expiration = null;
661
			if (($token = $this->checkSetRememberMeToken($remember_me, $_COOKIE[self::REMEMBER_ME_COOKIE], $expiration)))
662
			{
663
				self::egw_setcookie(self::REMEMBER_ME_COOKIE, $token, $expiration);
664
			}
665
666
			if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) successfull sessionid=$this->sessionid");
667
668
			// hook called once session is created
669
			Hooks::process(array(
670
				'location'       => 'session_created',
671
				'sessionid'      => $this->sessionid,
672
				'session_flags'  => $this->session_flags,
673
				'account_id'     => $this->account_id,
674
				'account_lid'    => $this->account_lid,
675
				'passwd'         => $this->passwd,
676
				'account_domain' => $this->account_domain,
677
				'user_ip'        => $user_ip,
678
				'session_type'   => Session\Type::get($_SERVER['REQUEST_URI'],
679
					$GLOBALS['egw_info']['flags']['current_app'],
680
					true),	// true return WebGUI instead of login, as we are logged in now
681
			),'',true);
682
683
			return $this->sessionid;
684
		}
685
		// catch all exceptions, as their (allways logged) trace (eg. on a database error) would contain the user password
686
		catch(Exception $e) {
687
			$this->reason = $this->cd_reason = is_a($e, Db\Exception::class) ?
688
				// do not output specific database error, eg. invalid SQL statement
689
				lang('Database Error!') : $e->getMessage();
690
			error_log(__METHOD__."('$login', ".array2string(str_repeat('*', strlen($passwd))).
691
				", '$passwd_type', no_session=".array2string($no_session).
692
				", auth_check=".array2string($auth_check).
693
				", fail_on_forced_password_change=".array2string($fail_on_forced_password_change).
694
				") Exception ".$e->getMessage());
695
			return false;
696
		}
697
	}
698
699
	/**
700
	 * Check if password authentication is required or given token is sufficient
701
	 *
702
	 * Token is only checked for 'remember_me_token' === 'always', not for default of only for 2FA!
703
	 *
704
	 * Password auth is also required if 2FA is not disabled and either required or configured by user.
705
	 *
706
	 * @param string $token value of token
707
	 * @param int& $account_id =null account_id of token-owner to limit check on that user, on return account_id of token owner
708
	 * @return boolean false: if further auth check is required, true: if token is sufficient for authentication
709
	 */
710
	public function skipPasswordAuth($token, &$account_id=null)
711
	{
712
		// if token is empty or disabled --> password authentication required
713
		if (empty($token) || $GLOBALS['egw_info']['server']['remember_me_token'] !== 'always' ||
714
			!($client = $this->checkOpenIDconfigured()))
715
		{
716
			return false;
717
		}
718
719
		// check if token exists and is (still) valid
720
		$tokenRepo = new OpenID\Repositories\AccessTokenRepository();
721
		if (!($access_token = $tokenRepo->findToken($client, $account_id, 'PT1S', $token)))
722
		{
723
			return false;
724
		}
725
		$account_id = $access_token->getUserIdentifier();
726
727
		// check if we need a second factor
728
		if ($GLOBALS['egw_info']['server']['2fa_required'] !== 'disabled' &&
729
			(($creds = Credentials::read(0, Credentials::TWOFA, $account_id)) ||
730
				$GLOBALS['egw_info']['server']['2fa_required'] === 'strict'))
731
		{
732
			return false;
733
		}
734
735
		// access-token is sufficient
736
		return true;
737
	}
738
739
	/**
740
	 * Check multifcator authemtication
741
	 *
742
	 * @param string $code 2fa-code
743
	 * @param string $token remember me token
744
	 * @throws \Exception with error-message if NOT successful
745
	 */
746
	protected function checkMultifactorAuth($code, $token)
747
	{
748
		$errors = $factors = [];
749
750
		if ($GLOBALS['egw_info']['server']['2fa_required'] === 'disabled')
751
		{
752
			return;	// nothing to check
753
		}
754
755
		// check if token exists and is (still) valid
756
		if (!empty($token) && $GLOBALS['egw_info']['server']['remember_me_token'] !== 'disabled' &&
757
			($client = $this->checkOpenIDconfigured()))
758
		{
759
			$tokenRepo = new OpenID\Repositories\AccessTokenRepository();
760
			if ($tokenRepo->findToken($client, $this->account_id, 'PT1S', $token))
761
			{
762
				$factors['remember_me_token'] = true;
763
			}
764
			else
765
			{
766
				$errors['remember_me_token'] = lang("Invalid or expired 'remember me' token");
767
			}
768
		}
769
770
		// if 2fa is configured by user, check it
771
		if (($creds = Credentials::read(0, Credentials::TWOFA, $this->account_id)))
772
		{
773
			if (empty($code))
774
			{
775
				$errors['2fa_code'] = lang('2-Factor Authentication code required');
776
			}
777
			else
778
			{
779
				$google2fa = new Google2FA\Google2FA();
780
				if (!empty($code) && $google2fa->verify($code, $creds['2fa_password']))
781
				{
782
					$factors['2fa_code'] = true;
783
				}
784
				else
785
				{
786
					$errors['2fa_code'] = lang('Invalid 2-Factor Authentication code');
787
				}
788
			}
789
		}
790
791
		// check for more factors and/or policies
792
		// hook can add factors, errors or throw \Exception with error-message and -code
793
		Hooks::process([
794
			'location' => 'multifactor_policy',
795
			'factors' => &$factors,
796
			'errors' => &$errors,
797
			'2fa_code' => $code,
798
			'remember_me_token' => $token,
799
		], [], true);
800
801
		if (!count($factors) && (count($errors) ||
802
			$GLOBALS['egw_info']['server']['2fa_required'] === 'strict'))
803
		{
804
			if (!empty($code) && isset($errors['2fa_code']))
805
			{
806
				// we log the missing factor, but externally only show "Bad Login or Password"
807
				// to give no indication that the password was already correct
808
				throw new \Exception(implode(', ', $errors), self::CD_BAD_LOGIN_OR_PASSWORD);
809
			}
810
			else
811
			{
812
				throw new \Exception(implode(', ', $errors), self::CD_SECOND_FACTOR_REQUIRED);
813
			}
814
		}
815
	}
816
817
	/**
818
	 * Check if we need to set a remember me token/cookie
819
	 *
820
	 * @param string $remember_me =null "True" for checkbox checked, or periode for user-choice select-box eg. "P1W" or "" for NOT remember
821
	 * @param string $token current remember me token
822
	 * @param int& $expriation on return expiration time of new cookie
823
	 * @return string new token to set as Cookieor null to not set a new one
824
	 */
825
	protected function checkSetRememberMeToken($remember_me, $token, &$expiration)
826
	{
827
		// do we need a new token
828
		if (!empty($remember_me) && $GLOBALS['egw_info']['server']['remember_me_token'] !== 'disabled' &&
829
			($client = $this->checkOpenIDconfigured()))
830
		{
831
			if (!empty($token))
832
			{
833
				// check if token exists and is (still) valid
834
				$tokenRepo = new OpenID\Repositories\AccessTokenRepository();
835
				if ($tokenRepo->findToken($client, $this->account_id, 'PT1S', $token))
836
				{
837
					return null;	// token still valid, no need to set it again
838
				}
839
			}
840
			$lifetime = $this->rememberMeTokenLifetime(is_string($remember_me) ? $remember_me : null);
841
			$expiration = $this->rememberMeTokenLifetime(is_string($remember_me) ? $remember_me : null, true);
842
843
			$tokenFactory = new OpenID\Token();
844
			if (($token = $tokenFactory->accessToken(self::OPENID_REMEMBER_ME_CLIENT_ID, [], $lifetime, false, $lifetime, false)))
845
			{
846
				return $token->getIdentifier();
847
			}
848
		}
849
		return null;
850
	}
851
852
	/**
853
	 * Check if 'remember me' token should be deleted on explict logout
854
	 *
855
	 * @return boolean false: if 2FA is enabeld for user, true: otherwise
856
	 */
857
	public function removeRememberMeTokenOnLogout()
858
	{
859
		return $GLOBALS['egw_info']['server']['2fa_required'] === 'disabled' ||
860
			$GLOBALS['egw_info']['server']['2fa_required'] !== 'strict' &&
861
			!($creds = Credentials::read(0, Credentials::TWOFA, $this->account_id));
862
	}
863
864
	/**
865
	 * OpenID Client ID for remember me token
866
	 */
867
	const OPENID_REMEMBER_ME_CLIENT_ID = 'login-remember-me';
868
869
	/**
870
	 * Check and if not configure OpenID app to generate 'remember me' tokens
871
	 *
872
	 * @return OpenID\Entities\ClientEntity|null null if OpenID Server app is not installed
873
	 */
874
	protected function checkOpenIDconfigured()
875
	{
876
		// OpenID app not installed --> password authentication required
877
		if (!isset($GLOBALS['egw_info']['apps']))
878
		{
879
			$GLOBALS['egw']->applications->read_installed_apps();
880
		}
881
		if (empty($GLOBALS['egw_info']['apps']['openid']))
882
		{
883
			return null;
884
		}
885
886
		$clients = new OpenID\Repositories\ClientRepository();
887
		try {
888
			$client = $clients->getClientEntity(self::OPENID_REMEMBER_ME_CLIENT_ID, null, null, false);	// false = do NOT check client-secret
889
		}
890
		catch (OAuthServerException $e)
891
		{
892
			unset($e);
893
			$client = new OpenID\Entities\ClientEntity();
894
			$client->setIdentifier(self::OPENID_REMEMBER_ME_CLIENT_ID);
895
			$client->setSecret(Auth::randomstring(24));	// must not be unset
896
			$client->setName(lang('Remember me token'));
897
			$client->setAccessTokenTTL($this->rememberMeTokenLifetime());
898
			$client->setRefreshTokenTTL('P0S');	// no refresh token
899
			$client->setRedirectUri($GLOBALS['egw_info']['server']['webserver_url'].'/');
900
			$clients->persistNewClient($client);
901
		}
902
		return $client;
903
	}
904
905
	/**
906
	 * Return lifetime for remember me token
907
	 *
908
	 * @param string $user user choice, if allowed
909
	 * @param boolean $ts =false false: return periode string, true: return integer timestamp
910
	 * @return string periode spec eg. 'P1M'
911
	 */
912
	protected function rememberMeTokenLifetime($user=null, $ts=false)
913
	{
914
		switch ((string)$GLOBALS['egw_info']['server']['remember_me_lifetime'])
915
		{
916
			case 'user':
917
				if (!empty($user))
918
				{
919
					$lifetime = $user;
920
					break;
921
				}
922
				// fall-through for default lifetime
923
			case '':	// default lifetime
924
				$lifetime = 'P1M';
925
				break;
926
			default:
927
				$lifetime = $GLOBALS['egw_info']['server']['remember_me_lifetime'];
928
				break;
929
		}
930
		if ($ts)
931
		{
932
			$expiration = new DateTime('now', DateTime::$server_timezone);
933
			$expiration->add(new \DateInterval($lifetime));
934
			return $expiration->format('ts');
935
		}
936
		return $lifetime;
937
	}
938
939
	/**
940
	 * Store eGW specific session-vars
941
	 *
942
	 * @param string $login
943
	 * @param string $user_ip
944
	 * @param int $now
945
	 * @param string $session_flags
946
	 */
947
	private function register_session($login,$user_ip,$now,$session_flags)
948
	{
949
		// restore session vars set before session was started
950
		if (is_array($this->required_files))
951
		{
952
			$_SESSION[self::EGW_REQUIRED_FILES] = !is_array($_SESSION[self::EGW_REQUIRED_FILES]) ? $this->required_files :
953
				array_unique(array_merge($_SESSION[self::EGW_REQUIRED_FILES],$this->required_files));
954
			unset($this->required_files);
955
		}
956
		$_SESSION[self::EGW_SESSION_VAR] = array(
957
			'session_id'     => $this->sessionid,
958
			'session_lid'    => $login,
959
			'session_ip'     => $user_ip,
960
			'session_logintime' => $now,
961
			'session_dla'    => $now,
962
			'session_action' => $_SERVER['PHP_SELF'],
963
			'session_flags'  => $session_flags,
964
			// we need the install-id to differ between serveral installs shareing one tmp-dir
965
			'session_install_id' => $GLOBALS['egw_info']['server']['install_id']
966
		);
967
	}
968
969
	/**
970
	 * name of access-log table
971
	 */
972
	const ACCESS_LOG_TABLE = 'egw_access_log';
973
974
	/**
975
	 * Prefix used to log unsucessful login attempts in cache, if DB is unavailable
976
	 */
977
	const FALSE_IP_CACHE_PREFIX = 'false_ip-';
978
	const FALSE_ID_CACHE_PREFIX = 'false_id-';
979
980
	/**
981
     * Write or update (for logout) the access_log
982
	 *
983
	 * We do NOT log anonymous sessions to not block website and also to cope with
984
	 * high rate anon endpoints might be called creating a bottleneck in the egw_access_log table.
985
	 *
986
	 * @param string|int $sessionid nummeric or PHP session id or error-message for unsuccessful logins
987
	 * @param string $login ='' account_lid (evtl. with domain) or '' for setting the logout-time
988
	 * @param string $user_ip ='' ip to log
989
	 * @param int $account_id =0 numerical account_id
990
	 * @return int $sessionid primary key of egw_access_log for login, null otherwise
991
	 */
992
	private function log_access($sessionid,$login='',$user_ip='',$account_id=0)
993
	{
994
		// do not log anything for anonymous sessions
995
		if ($this->session_flags === 'A')
996
		{
997
			return;
998
		}
999
		$now = time();
1000
1001
		// if sessionid contains non-ascii chars (only happens for error-messages)
1002
		// --> transliterate it to ascii, as session_php only allows ascii chars
1003
		if (preg_match('/[^\x20-\x7f]/', $sessionid))
1004
		{
1005
			$sessionid = Translation::to_ascii($sessionid);
1006
		}
1007
1008
		if ($login)
1009
		{
1010
			$GLOBALS['egw']->db->insert(self::ACCESS_LOG_TABLE,array(
1011
				'session_php' => $sessionid,
1012
				'loginid'   => $login,
1013
				'ip'        => $user_ip,
1014
				'li'        => $now,
1015
				'account_id'=> $account_id,
1016
				'user_agent'=> $_SERVER['HTTP_USER_AGENT'],
1017
				'session_dla'    => $now,
1018
				'session_action' => $this->update_dla(false),	// dont update egw_access_log
1019
			),false,__LINE__,__FILE__);
1020
1021
			$_SESSION[self::EGW_SESSION_VAR]['session_logged_dla'] = $now;
1022
1023
			$ret = $GLOBALS['egw']->db->get_last_insert_id(self::ACCESS_LOG_TABLE,'sessionid');
1024
1025
			// if we can not store failed login attempts in database, store it in cache
1026
			if (!$ret && !$account_id)
1027
			{
1028
				Cache::setInstance(__CLASS__, self::FALSE_IP_CACHE_PREFIX.$user_ip,
1029
					1+Cache::getInstance(__CLASS__, self::FALSE_IP_CACHE_PREFIX.$user_ip),
1030
					$GLOBALS['egw_info']['server']['block_time'] * 60);
1031
1032
				Cache::setInstance(__CLASS__, self::FALSE_ID_CACHE_PREFIX.$login,
1033
					1+Cache::getInstance(__CLASS__, self::FALSE_ID_CACHE_PREFIX.$login),
1034
					$GLOBALS['egw_info']['server']['block_time'] * 60);
1035
			}
1036
		}
1037
		else
1038
		{
1039
			if (!is_numeric($sessionid) && $sessionid == $this->sessionid && $this->sessionid_access_log)
1040
			{
1041
				$sessionid = $this->sessionid_access_log;
1042
			}
1043
			$GLOBALS['egw']->db->update(self::ACCESS_LOG_TABLE,array(
1044
				'lo' => $now
1045
			),is_numeric($sessionid) ? array(
1046
				'sessionid' => $sessionid,
1047
			) : array(
1048
				'session_php' => $sessionid,
1049
			),__LINE__,__FILE__);
1050
1051
			// run maintenance only on logout, to not delay login
1052
			if ($GLOBALS['egw_info']['server']['max_access_log_age'])
1053
			{
1054
				$max_age = $now - $GLOBALS['egw_info']['server']['max_access_log_age'] * 24 * 60 * 60;
1055
1056
				$GLOBALS['egw']->db->delete(self::ACCESS_LOG_TABLE,"li < $max_age",__LINE__,__FILE__);
1057
			}
1058
		}
1059
		//error_log(__METHOD__."('$sessionid', '$login', '$user_ip', $account_id) returning ".array2string($ret));
1060
		return $ret;
1061
	}
1062
1063
	/**
1064
	 * Protect against brute force attacks, block login if too many unsuccessful login attmepts
1065
     *
1066
	 * @param string $login account_lid (evtl. with domain)
1067
	 * @param string $ip ip of the user
1068
	 * @returns bool login blocked?
1069
	 */
1070
	private function login_blocked($login,$ip)
1071
	{
1072
		$block_time = time() - $GLOBALS['egw_info']['server']['block_time'] * 60;
1073
1074
		$false_id = $false_ip = 0;
1075
		foreach($GLOBALS['egw']->db->union(array(
1076
			array(
1077
				'table' => self::ACCESS_LOG_TABLE,
1078
				'cols'  => "'false_ip' AS name,COUNT(*) AS num",
1079
				'where' => array(
1080
					'account_id' => 0,
1081
					'ip' => $ip,
1082
					"li > $block_time",
1083
				),
1084
			),
1085
			array(
1086
				'table' => self::ACCESS_LOG_TABLE,
1087
				'cols'  => "'false_id' AS name,COUNT(*) AS num",
1088
				'where' => array(
1089
					'account_id' => 0,
1090
					'loginid' => $login,
1091
					"li > $block_time",
1092
				),
1093
			),
1094
			array(
1095
				'table' => self::ACCESS_LOG_TABLE,
1096
				'cols'  => "'false_id' AS name,COUNT(*) AS num",
1097
				'where' => array(
1098
					'account_id' => 0,
1099
					'loginid LIKE '.$GLOBALS['egw']->db->quote($login.'@%'),
1100
					"li > $block_time",
1101
				)
1102
			),
1103
		), __LINE__, __FILE__) as $row)
1104
		{
1105
			${$row['name']} += $row['num'];
1106
		}
1107
1108
		// check cache too, in case DB is readonly
1109
		$false_ip += Cache::getInstance(__CLASS__, self::FALSE_IP_CACHE_PREFIX.$ip);
1110
		$false_id += Cache::getInstance(__CLASS__, self::FALSE_ID_CACHE_PREFIX.$login);
1111
1112
		// if IP matches one in the (comma-separated) whitelist
1113
		// --> check with whitelists optional number (none means never block)
1114
		$matches = null;
1115
		if (!empty($GLOBALS['egw_info']['server']['unsuccessful_ip_whitelist']) &&
1116
			preg_match_all('/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d+)?/',
1117
				$GLOBALS['egw_info']['server']['unsuccessful_ip_whitelist'], $matches) &&
1118
			($key=array_search($ip, $matches[1])) !== false)
1119
		{
1120
			$blocked = !empty($matches[3][$key]) && $false_ip > $matches[3][$key];
1121
		}
1122
		else	// else check with general number
1123
		{
1124
			$blocked = $false_ip > $GLOBALS['egw_info']['server']['num_unsuccessful_ip'];
1125
		}
1126
		if (!$blocked)
1127
		{
1128
			$blocked = $false_id > $GLOBALS['egw_info']['server']['num_unsuccessful_id'];
1129
		}
1130
		//error_log(__METHOD__."('$login', '$ip') false_ip=$false_ip, false_id=$false_id --> blocked=".array2string($blocked));
1131
1132
		if ($blocked && $GLOBALS['egw_info']['server']['admin_mails'] &&
1133
			$GLOBALS['egw_info']['server']['login_blocked_mail_time'] < time()-5*60)	// max. one mail every 5mins
1134
		{
1135
			try {
1136
				$mailer = new Mailer();
1137
				// notify admin(s) via email
1138
				$mailer->setFrom('eGroupWare@'.$GLOBALS['egw_info']['server']['mail_suffix']);
1139
				$mailer->addHeader('Subject', lang("eGroupWare: login blocked for user '%1', IP %2",$login,$ip));
1140
				$mailer->setBody(lang("Too many unsucessful attempts to login: %1 for the user '%2', %3 for the IP %4",$false_id,$login,$false_ip,$ip));
1141
				foreach(preg_split('/,\s*/',$GLOBALS['egw_info']['server']['admin_mails']) as $mail)
1142
				{
1143
					$mailer->addAddress($mail);
1144
				}
1145
				$mailer->send();
1146
			}
1147
			catch(\Exception $e) {
1148
				// ignore exception, but log it, to block the account and give a correct error-message to user
1149
				error_log(__METHOD__."('$login', '$ip') ".$e->getMessage());
1150
			}
1151
			// save time of mail, to not send to many mails
1152
			$config = new Config('phpgwapi');
1153
			$config->read_repository();
1154
			$config->value('login_blocked_mail_time',time());
1155
			$config->save_repository();
1156
		}
1157
		return $blocked;
1158
	}
1159
1160
	/**
1161
	 * Basename of scripts for which we create a pseudo session-id based on user-credentials
1162
	 *
1163
	 * @var array
1164
	 */
1165
	static $pseudo_session_scripts = array(
1166
		'webdav.php', 'groupdav.php', 'remote.php', 'share.php'
1167
	);
1168
1169
	/**
1170
	 * Get the sessionid from Cookie, Get-Parameter or basic auth
1171
	 *
1172
	 * @param boolean $only_basic_auth =false return only a basic auth pseudo sessionid, default no
1173
	 * @return string|null (pseudo-)session-id use or NULL if no Cookie or Basic-Auth credentials
1174
	 */
1175
	static function get_sessionid($only_basic_auth=false)
1176
	{
1177
		// for WebDAV and GroupDAV we use a pseudo sessionid created from md5(user:passwd)
1178
		// --> allows this stateless protocolls which use basic auth to use sessions!
1179
		if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW']) &&
1180
			(in_array(basename($_SERVER['SCRIPT_NAME']), self::$pseudo_session_scripts) ||
1181
				$_SERVER['SCRIPT_NAME'] === '/Microsoft-Server-ActiveSync'))
1182
		{
1183
			// we generate a pseudo-sessionid from the basic auth credentials
1184
			$sessionid = md5($_SERVER['PHP_AUTH_USER'].':'.$_SERVER['PHP_AUTH_PW'].':'.$_SERVER['HTTP_HOST'].':'.
1185
				EGW_SERVER_ROOT.':'.self::getuser_ip().':'.filemtime(EGW_SERVER_ROOT.'/api/setup/setup.inc.php').
1186
				// for ActiveSync we add the DeviceID
1187
				(isset($_GET['DeviceId']) && $_SERVER['SCRIPT_NAME'] === '/Microsoft-Server-ActiveSync' ? ':'.$_GET['DeviceId'] : '').
1188
				':'.$_SERVER['HTTP_USER_AGENT']);
1189
			//error_log(__METHOD__."($only_basic_auth) HTTP_HOST=$_SERVER[HTTP_HOST], PHP_AUTH_USER=$_SERVER[PHP_AUTH_USER], DeviceId=$_GET[DeviceId]: sessionid=$sessionid");
1190
		}
1191
		// same for digest auth
1192
		elseif (isset($_SERVER['PHP_AUTH_DIGEST']) &&
1193
			in_array(basename($_SERVER['SCRIPT_NAME']), self::$pseudo_session_scripts))
1194
		{
1195
			// we generate a pseudo-sessionid from the digest username, realm and nounce
1196
			// can't use full $_SERVER['PHP_AUTH_DIGEST'], as it changes (contains eg. the url)
1197
			$data = Header\Authenticate::parse_digest($_SERVER['PHP_AUTH_DIGEST']);
1198
			$sessionid = md5($data['username'].':'.$data['realm'].':'.$data['nonce'].':'.$_SERVER['HTTP_HOST'].
1199
				EGW_SERVER_ROOT.':'.self::getuser_ip().':'.filemtime(EGW_SERVER_ROOT.'/api/setup/setup.inc.php').
1200
				':'.$_SERVER['HTTP_USER_AGENT']);
1201
		}
1202
		elseif(!$only_basic_auth && isset($_REQUEST[self::EGW_SESSION_NAME]))
1203
		{
1204
			$sessionid = $_REQUEST[self::EGW_SESSION_NAME];
1205
		}
1206
		elseif(!$only_basic_auth && isset($_COOKIE[self::EGW_SESSION_NAME]))
1207
		{
1208
			$sessionid = $_COOKIE[self::EGW_SESSION_NAME];
1209
		}
1210
		else
1211
		{
1212
			$sessionid = null;
1213
		}
1214
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."() _SERVER[REQUEST_URI]='$_SERVER[REQUEST_URI]' returning ".print_r($sessionid,true));
1215
		return $sessionid;
1216
	}
1217
1218
	/**
1219
	 * Get request or cookie variable with higher precedence to $_REQUEST then $_COOKIE
1220
	 *
1221
	 * In php < 5.3 that's identical to $_REQUEST[$name], but php5.3+ does no longer register cookied in $_REQUEST by default
1222
	 *
1223
	 * As a workaround for a bug in Safari Version 3.2.1 (5525.27.1), where cookie first letter get's upcased, we check that too.
1224
	 *
1225
	 * @param string $name eg. 'kp3' or domain
1226
	 * @return mixed null if it's neither set in $_REQUEST or $_COOKIE
1227
	 */
1228
	static function get_request($name)
1229
	{
1230
		return isset($_REQUEST[$name]) ? $_REQUEST[$name] :
1231
			(isset($_COOKIE[$name]) ? $_COOKIE[$name] :
1232
			(isset($_COOKIE[$name=ucfirst($name)]) ? $_COOKIE[$name] : null));
1233
	}
1234
1235
	/**
1236
	 * Check to see if a session is still current and valid
1237
	 *
1238
	 * @param string $sessionid session id to be verfied
1239
	 * @param string $kp3 ?? to be verified
1240
	 * @return bool is the session valid?
1241
	 */
1242
	function verify($sessionid=null,$kp3=null)
1243
	{
1244
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$sessionid','$kp3') ".function_backtrace());
1245
1246
		$fill_egw_info_and_repositories = !$GLOBALS['egw_info']['flags']['restored_from_session'];
1247
1248
		if(!$sessionid)
1249
		{
1250
			$sessionid = self::get_sessionid();
1251
			$kp3       = self::get_request('kp3');
1252
		}
1253
1254
		$this->sessionid = $sessionid;
1255
		$this->kp3       = $kp3;
1256
1257
1258
		if (!$this->sessionid)
1259
		{
1260
			if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$sessionid') get_sessionid()='".self::get_sessionid()."' No session ID");
1261
			return false;
1262
		}
1263
1264
		switch (session_status())
1265
		{
1266
			case PHP_SESSION_DISABLED:
1267
				throw new ErrorException('EGroupware requires the PHP session extension!');
1268
			case PHP_SESSION_NONE:
1269
				session_name(self::EGW_SESSION_NAME);
1270
				session_id($this->sessionid);
1271
				self::cache_control();
1272
				session_start();
1273
				break;
1274
			case PHP_SESSION_ACTIVE:
1275
				// session already started eg. by managementserver_client
1276
		}
1277
1278
		// check if we have a eGroupware session --> return false if not (but dont destroy it!)
1279
		if (is_null($_SESSION) || !isset($_SESSION[self::EGW_SESSION_VAR]))
1280
		{
1281
			if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$sessionid') session does NOT exist!");
1282
			return false;
1283
		}
1284
		$session =& $_SESSION[self::EGW_SESSION_VAR];
1285
1286
		if ($session['session_dla'] <= time() - $GLOBALS['egw_info']['server']['sessions_timeout'])
1287
		{
1288
			if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$sessionid') session timed out!");
1289
			$this->destroy($sessionid,$kp3);
1290
			return false;
1291
		}
1292
1293
		$this->session_flags = $session['session_flags'];
1294
1295
		$this->split_login_domain($session['session_lid'],$this->account_lid,$this->account_domain);
1296
1297
		// This is to ensure that we authenticate to the correct domain (might not be default)
1298
		if($GLOBALS['egw_info']['user']['domain'] && $this->account_domain != $GLOBALS['egw_info']['user']['domain'])
1299
		{
1300
			return false;	// session not verified, domain changed
1301
		}
1302
		$GLOBALS['egw_info']['user']['kp3'] = $this->kp3;
1303
1304
		// allow xajax / notifications to not update the dla, so sessions can time out again
1305
		if (!isset($GLOBALS['egw_info']['flags']['no_dla_update']) || !$GLOBALS['egw_info']['flags']['no_dla_update'])
1306
		{
1307
			$this->update_dla(true);
1308
		}
1309
		elseif ($GLOBALS['egw_info']['flags']['currentapp'] == 'notifications')
1310
		{
1311
			$this->update_notification_heartbeat();
1312
		}
1313
		$this->account_id = $GLOBALS['egw']->accounts->name2id($this->account_lid,'account_lid','u');
1314
		if (!$this->account_id)
1315
		{
1316
			if (self::ERROR_LOG_DEBUG) error_log("*** Session::verify($sessionid) !accounts::name2id('$this->account_lid')");
1317
			return false;
1318
		}
1319
1320
		$GLOBALS['egw_info']['user']['account_id'] = $this->account_id;
1321
1322
		if ($fill_egw_info_and_repositories)
1323
		{
1324
			$GLOBALS['egw_info']['user'] = $this->read_repositories();
1325
		}
1326
		else
1327
		{
1328
			// restore apps to $GLOBALS['egw_info']['apps']
1329
			$GLOBALS['egw']->applications->read_installed_apps();
1330
1331
			// session only stores app-names, restore apps from egw_info[apps]
1332
			if (isset($GLOBALS['egw_info']['user']['apps'][0]))
1333
			{
1334
				$GLOBALS['egw_info']['user']['apps'] = array_intersect_key($GLOBALS['egw_info']['apps'], array_flip($GLOBALS['egw_info']['user']['apps']));
1335
			}
1336
1337
			// set prefs, they are no longer stored in session
1338
			$GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->read_repository();
1339
		}
1340
1341
		if ($GLOBALS['egw']->accounts->is_expired($GLOBALS['egw_info']['user']))
1342
		{
1343
			if (self::ERROR_LOG_DEBUG) error_log("*** Session::verify($sessionid) accounts is expired");
1344
			return false;
1345
		}
1346
		$this->passwd = base64_decode(Cache::getSession('phpgwapi', 'password'));
1347
		if ($fill_egw_info_and_repositories)
1348
		{
1349
			$GLOBALS['egw_info']['user']['session_ip'] = $session['session_ip'];
1350
			$GLOBALS['egw_info']['user']['passwd']     = $this->passwd;
1351
		}
1352
		if ($this->account_domain != $GLOBALS['egw_info']['user']['domain'])
1353
		{
1354
			if (self::ERROR_LOG_DEBUG) error_log("*** Session::verify($sessionid) wrong domain");
1355
			return false;
1356
		}
1357
1358
		if ($GLOBALS['egw_info']['server']['sessions_checkip'])
1359
		{
1360
			if (strtoupper(substr(PHP_OS,0,3)) != 'WIN' && (!$GLOBALS['egw_info']['user']['session_ip'] ||
1361
				$GLOBALS['egw_info']['user']['session_ip'] != $this->getuser_ip()))
1362
			{
1363
				if (self::ERROR_LOG_DEBUG) error_log("*** Session::verify($sessionid) wrong IP");
1364
				return false;
1365
			}
1366
		}
1367
1368
		if ($fill_egw_info_and_repositories)
1369
		{
1370
			$GLOBALS['egw']->acl->__construct($this->account_id);
1371
			$GLOBALS['egw']->preferences->__construct($this->account_id);
1372
			$GLOBALS['egw']->applications->__construct($this->account_id);
1373
		}
1374
		if (!$this->account_lid)
1375
		{
1376
			if (self::ERROR_LOG_DEBUG) error_log("*** Session::verify($sessionid) !account_lid");
1377
			return false;
1378
		}
1379
1380
		// query accesslog-id, if not set in session (session is made persistent after login!)
1381
		if (!$this->sessionid_access_log && $this->session_flags != 'A')
1382
		{
1383
			$this->sessionid_access_log = $GLOBALS['egw']->db->select(self::ACCESS_LOG_TABLE,'sessionid',array(
1384
				'session_php' => $this->sessionid,
1385
			),__LINE__,__FILE__)->fetchColumn();
1386
			//error_log(__METHOD__."() sessionid=$this->sessionid --> sessionid_access_log=$this->sessionid_access_log");
1387
		}
1388
1389
		// check if we use cookies for the session, but no cookie set
1390
		// happens eg. in sitemgr (when redirecting to a different domain) or with new java notification app
1391
		if ($GLOBALS['egw_info']['server']['usecookies'] && isset($_REQUEST[self::EGW_SESSION_NAME]) &&
1392
			$_REQUEST[self::EGW_SESSION_NAME] === $this->sessionid &&
1393
			(!isset($_COOKIE[self::EGW_SESSION_NAME]) || $_COOKIE[self::EGW_SESSION_NAME] !== $_REQUEST[self::EGW_SESSION_NAME]))
1394
		{
1395
			if (self::ERROR_LOG_DEBUG) error_log("--> Session::verify($sessionid) SUCCESS, but NO required cookies set --> setting them now");
1396
			self::egw_setcookie(self::EGW_SESSION_NAME,$this->sessionid);
1397
			self::egw_setcookie('kp3',$this->kp3);
1398
			self::egw_setcookie('domain',$this->account_domain);
1399
		}
1400
1401
		if (self::ERROR_LOG_DEBUG) error_log("--> Session::verify($sessionid) SUCCESS");
1402
1403
		return true;
1404
	}
1405
1406
	/**
1407
	 * Terminate a session
1408
	 *
1409
	 * @param int|string $sessionid nummeric or php session id of session to be terminated
1410
	 * @param string $kp3
1411
	 * @return boolean true on success, false on error
1412
	 */
1413
	function destroy($sessionid, $kp3='')
1414
	{
1415
		if (!$sessionid && $kp3)
1416
		{
1417
			return false;
1418
		}
1419
		$this->log_access($sessionid);	// log logout-time
1420
1421
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($sessionid,$kp3)");
1422
1423
		if (is_numeric($sessionid))	// do we have a access-log-id --> get PHP session id
1424
		{
1425
			$sessionid = $GLOBALS['egw']->db->select(self::ACCESS_LOG_TABLE,'session_php',array(
1426
					'sessionid' => $sessionid,
1427
				),__LINE__,__FILE__)->fetchColumn();
1428
		}
1429
1430
		Hooks::process(array(
1431
			'location'  => 'session_destroyed',
1432
			'sessionid' => $sessionid,
1433
		),'',true);	// true = run hooks from all apps, not just the ones the current user has perms to run
1434
1435
		// Only do the following, if where working with the current user
1436
		if (!$GLOBALS['egw_info']['user']['sessionid'] || $sessionid == $GLOBALS['egw_info']['user']['sessionid'])
1437
		{
1438
			if (self::ERROR_LOG_DEBUG) error_log(__METHOD__." ********* about to call session_destroy!");
1439
			session_unset();
1440
			@session_destroy();
1441
			// we need to (re-)load the eGW session-handler, as session_destroy unloads custom session-handlers
1442
			if (function_exists('init_session_handler'))
1443
			{
1444
				init_session_handler();
1445
			}
1446
1447
			if ($GLOBALS['egw_info']['server']['usecookies'])
1448
			{
1449
				self::egw_setcookie(session_name());
1450
			}
1451
		}
1452
		else
1453
		{
1454
			$this->commit_session();	// close our own session
1455
1456
			session_id($sessionid);
1457
			if (session_start())
1458
			{
1459
				session_destroy();
1460
			}
1461
		}
1462
		return true;
1463
	}
1464
1465
	/**
1466
	 * Generate a url which supports url or cookies based sessions
1467
	 *
1468
	 * Please note, the values of the query get url encoded!
1469
	 *
1470
	 * @param string $url a url relative to the egroupware install root, it can contain a query too
1471
	 * @param array|string $extravars query string arguements as string or array (prefered)
1472
	 * 	if string is used ambersands in vars have to be already urlencoded as '%26', function ensures they get NOT double encoded
1473
	 * @return string generated url
1474
	 */
1475
	public static function link($url, $extravars = '')
1476
	{
1477
		//error_log(_METHOD__."(url='$url',extravars='".array2string($extravars)."')");
1478
1479
		if ($url[0] != '/')
1480
		{
1481
			$app = $GLOBALS['egw_info']['flags']['currentapp'];
1482
			if ($app != 'login' && $app != 'logout')
1483
			{
1484
				$url = $app.'/'.$url;
1485
			}
1486
		}
1487
1488
		// append the url to the webserver url, but avoid more then one slash between the parts of the url
1489
		$webserver_url = $GLOBALS['egw_info']['server']['webserver_url'];
1490
		// patch inspired by vladimir kolobkov -> we should not try to match the webserver url against the url without '/' as delimiter,
1491
		// as $webserver_url may be part of $url (as /egw is part of phpgwapi/js/egw_instant_load.html)
1492
		if (($url[0] != '/' || $webserver_url != '/') && (!$webserver_url || strpos($url, $webserver_url.'/') === false))
1493
		{
1494
			if($url[0] != '/' && substr($webserver_url,-1) != '/')
1495
			{
1496
				$url = $webserver_url .'/'. $url;
1497
			}
1498
			else
1499
			{
1500
				$url = $webserver_url . $url;
1501
			}
1502
		}
1503
1504
		if(isset($GLOBALS['egw_info']['server']['enforce_ssl']) && $GLOBALS['egw_info']['server']['enforce_ssl'])
1505
		{
1506
			if(substr($url ,0,4) != 'http')
1507
			{
1508
				$url = 'https://'.$_SERVER['HTTP_HOST'].$url;
1509
			}
1510
			else
1511
			{
1512
				$url = str_replace ( 'http:', 'https:', $url);
1513
			}
1514
		}
1515
		$vars = array();
1516
		// add session params if not using cookies
1517
		if (!$GLOBALS['egw_info']['server']['usecookies'])
1518
		{
1519
			$vars[self::EGW_SESSION_NAME] = $GLOBALS['egw']->session->sessionid;
1520
			$vars['kp3'] = $GLOBALS['egw']->session->kp3;
1521
			$vars['domain'] = $GLOBALS['egw']->session->account_domain;
1522
		}
1523
1524
		// check if the url already contains a query and ensure that vars is an array and all strings are in extravars
1525
		list($ret_url,$othervars) = explode('?', $url, 2);
1526
		if ($extravars && is_array($extravars))
1527
		{
1528
			$vars += $extravars;
1529
			$extravars = $othervars;
1530
		}
1531
		else
1532
		{
1533
			if ($othervars) $extravars .= ($extravars?'&':'').$othervars;
1534
		}
1535
1536
		// parse extravars string into the vars array
1537
		if ($extravars)
1538
		{
1539
			foreach(explode('&',$extravars) as $expr)
1540
			{
1541
				list($var,$val) = explode('=', $expr,2);
1542
				if (strpos($val,'%26') != false) $val = str_replace('%26','&',$val);	// make sure to not double encode &
1543
				if (substr($var,-2) == '[]')
1544
				{
1545
					$vars[substr($var,0,-2)][] = $val;
1546
				}
1547
				else
1548
				{
1549
					$vars[$var] = $val;
1550
				}
1551
			}
1552
		}
1553
1554
		// if there are vars, we add them urlencoded to the url
1555
		if (count($vars))
1556
		{
1557
			$query = array();
1558
			foreach($vars as $key => $value)
1559
			{
1560
				if (is_array($value))
1561
				{
1562
					foreach($value as $val)
1563
					{
1564
						$query[] = $key.'[]='.urlencode($val);
1565
					}
1566
				}
1567
				else
1568
				{
1569
					$query[] = $key.'='.urlencode($value);
1570
				}
1571
			}
1572
			$ret_url .= '?' . implode('&',$query);
1573
		}
1574
		return $ret_url;
1575
	}
1576
1577
	/**
1578
	 * Regexp to validate IPv4 and IPv6
1579
	 */
1580
	const IP_REGEXP = '/^(?>(?>([a-f0-9]{1,4})(?>:(?1)){7}|(?!(?:.*[a-f0-9](?>:|$)){8,})((?1)(?>:(?1)){0,6})?::(?2)?)|(?>(?>(?1)(?>:(?1)){5}:|(?!(?:.*[a-f0-9]:){6,})(?3)?::(?>((?1)(?>:(?1)){0,4}):)?)?(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?>\.(?4)){3}))$/iD';
1581
1582
	/**
1583
	 * Get the ip address of current users
1584
	 *
1585
	 * We remove further private IPs (from proxys) as they invalidate user
1586
	 * sessions, when they change because of multiple proxys.
1587
	 *
1588
	 * @return string ip address
1589
	 */
1590
	public static function getuser_ip()
1591
	{
1592
		if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
1593
		{
1594
			$forwarded_for = preg_replace('/, *10\..*$/', '', $_SERVER['HTTP_X_FORWARDED_FOR']);
1595
			if (preg_match(self::IP_REGEXP, $forwarded_for))
1596
			{
1597
				return $forwarded_for;
1598
			}
1599
		}
1600
		return $_SERVER['REMOTE_ADDR'];
1601
	}
1602
1603
	/**
1604
	 * domain for cookies
1605
	 *
1606
	 * @var string
1607
	 */
1608
	private static $cookie_domain = '';
1609
1610
	/**
1611
	 * path for cookies
1612
	 *
1613
	 * @var string
1614
	 */
1615
	private static $cookie_path = '/';
1616
1617
	/**
1618
	 * iOS web-apps will loose cookie if set with a livetime of 0 / session-cookie
1619
	 *
1620
	 * Therefore we set a fixed lifetime of 24h from session-start instead.
1621
	 * Server-side session will timeout earliert anyway, if there's no activity.
1622
	 */
1623
	const IOS_SESSION_COOKIE_LIFETIME = 86400;
1624
1625
	/**
1626
	 * Set a cookie with eGW's cookie-domain and -path settings
1627
	 *
1628
	 * @param string $cookiename name of cookie to be set
1629
	 * @param string $cookievalue ='' value to be used, if unset cookie is cleared (optional)
1630
	 * @param int $cookietime =0 when cookie should expire, 0 for session only (optional)
1631
	 * @param string $cookiepath =null optional path (eg. '/') if the eGW install-dir should not be used
1632
	 */
1633
	public static function egw_setcookie($cookiename,$cookievalue='',$cookietime=0,$cookiepath=null)
1634
	{
1635
		if (empty(self::$cookie_domain) || empty(self::$cookie_path))
1636
		{
1637
			self::set_cookiedomain();
1638
		}
1639
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($cookiename,$cookievalue,$cookietime,$cookiepath,".self::$cookie_domain.")");
1640
1641
		// if we are installed in iOS as web-app, we must not set a cookietime==0 (session-cookie),
1642
		// as every change between apps will cause the cookie to get lost
1643
		static $is_iOS = null;
1644
		if (!$cookietime && !isset($is_iOS)) $is_iOS = (bool)preg_match('/^(iPhone|iPad|iPod)/i', Header\UserAgent::mobile());
1645
1646
		if(!headers_sent())	// gives only a warning, but can not send the cookie anyway
1647
		{
1648
			setcookie($cookiename, $cookievalue,
1649
				!$cookietime && $is_iOS ? time()+self::IOS_SESSION_COOKIE_LIFETIME : $cookietime,
1650
				is_null($cookiepath) ? self::$cookie_path : $cookiepath,self::$cookie_domain,
1651
				// if called via HTTPS, only send cookie for https and only allow cookie access via HTTP (true)
1652
				empty($GLOBALS['egw_info']['server']['insecure_cookies']) && Header\Http::schema() === 'https', true);
1653
		}
1654
	}
1655
1656
	/**
1657
	 * Set the domain and path used for cookies
1658
	 */
1659
	private static function set_cookiedomain()
1660
	{
1661
		if ($GLOBALS['egw_info']['server']['cookiedomain'])
1662
		{
1663
			// Admin set domain, eg. .domain.com to allow egw.domain.com and www.domain.com
1664
			self::$cookie_domain = $GLOBALS['egw_info']['server']['cookiedomain'];
1665
		}
1666
		else
1667
		{
1668
			// Use HTTP_X_FORWARDED_HOST if set, which is the case behind a none-transparent proxy
1669
			self::$cookie_domain = Header\Http::host();
1670
		}
1671
		// remove port from HTTP_HOST
1672
		$arr = null;
1673
		if (preg_match("/^(.*):(.*)$/",self::$cookie_domain,$arr))
1674
		{
1675
			self::$cookie_domain = $arr[1];
1676
		}
1677
		if (count(explode('.',self::$cookie_domain)) <= 1)
1678
		{
1679
			// setcookie dont likes domains without dots, leaving it empty, gets setcookie to fill the domain in
1680
			self::$cookie_domain = '';
1681
		}
1682
		if (!$GLOBALS['egw_info']['server']['cookiepath'] ||
1683
			!(self::$cookie_path = parse_url($GLOBALS['egw_info']['server']['webserver_url'],PHP_URL_PATH)))
1684
		{
1685
			self::$cookie_path = '/';
1686
		}
1687
1688
		session_set_cookie_params(0, self::$cookie_path, self::$cookie_domain,
1689
			// if called via HTTPS, only send cookie for https and only allow cookie access via HTTP (true)
1690
			empty($GLOBALS['egw_info']['server']['insecure_cookies']) && Header\Http::schema() === 'https', true);
1691
	}
1692
1693
	/**
1694
	 * Search the instance matching the request
1695
	 *
1696
	 * @param string $login on login $_POST['login'], $_SERVER['PHP_AUTH_USER'] or $_SERVER['REMOTE_USER']
1697
	 * @param string $domain_requested usually self::get_request('domain')
1698
	 * @param string &$default_domain usually $default_domain get's set eg. by sitemgr
1699
	 * @param string|array $server_names usually array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME'])
1700
	 * @param array $domains =null defaults to $GLOBALS['egw_domain'] from the header
1701
	 * @return string $GLOBALS['egw_info']['user']['domain'] set with the domain/instance to use
1702
	 */
1703
	public static function search_instance($login,$domain_requested,&$default_domain,$server_names,array $domains=null)
1704
	{
1705
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$login','$domain_requested',".array2string($default_domain).".".array2string($server_names).".".array2string($domains).")");
1706
1707
		if (is_null($domains)) $domains = $GLOBALS['egw_domain'];
1708
1709
		if (!isset($default_domain) || !isset($domains[$default_domain]))	// allow to overwrite the default domain
1710
		{
1711
			foreach((array)$server_names as $server_name)
1712
			{
1713
				list($server_name) = explode(':', $server_name);	// remove port from HTTP_HOST
1714
				if(isset($domains[$server_name]))
1715
				{
1716
					$default_domain = $server_name;
1717
					break;
1718
				}
1719
				else
1720
				{
1721
					$parts = explode('.', $server_name);
1722
					array_shift($parts);
1723
					$domain_part = implode('.', $parts);
1724
					if(isset($domains[$domain_part]))
1725
					{
1726
						$default_domain = $domain_part;
1727
						break;
1728
					}
1729
					else
1730
					{
1731
						reset($domains);
1732
						$default_domain = key($domains);
1733
					}
1734
					unset($domain_part);
1735
				}
1736
			}
1737
		}
1738
		if (isset($login))	// on login
1739
		{
1740
			if (strpos($login,'@') === false || count($domains) == 1)
1741
			{
1742
				$login .= '@' . (isset($_POST['logindomain']) ? $_POST['logindomain'] : $default_domain);
1743
			}
1744
			$parts = explode('@',$login);
1745
			$domain = array_pop($parts);
1746
			$GLOBALS['login'] = $login;
1747
		}
1748
		else	// on "normal" pageview
1749
		{
1750
			$domain = $domain_requested;
1751
		}
1752
		if (!isset($domains[$domain]))
1753
		{
1754
			$domain = $default_domain;
1755
		}
1756
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."() default_domain=".array2string($default_domain).', login='.array2string($login)." returning ".array2string($domain));
1757
1758
		return $domain;
1759
	}
1760
1761
	/**
1762
	 * Set action logged in access-log
1763
	 *
1764
	 * Non-ascii chars in $action get transliterate to ascii, as our session_action column allows only ascii.
1765
	 *
1766
	 * @param string $action
1767
	 */
1768
	public function set_action($action)
1769
	{
1770
		if (preg_match('/[^\x20-\x7f]/', $action))
1771
		{
1772
			$action = Translation::to_ascii($action);
1773
		}
1774
		$this->action = $action;
1775
	}
1776
1777
	/**
1778
	 * Ignore dla logging for a maximum of 900s = 15min
1779
	 */
1780
	const MAX_IGNORE_DLA_LOG = 900;
1781
1782
	/**
1783
	 * Update session_action and session_dla (session last used time)
1784
	 *
1785
	 * @param boolean $update_access_log =false false: dont update egw_access_log table, but set $this->action
1786
	 * @return string action as written to egw_access_log.session_action
1787
	 */
1788
	private function update_dla($update_access_log=false)
1789
	{
1790
		// This way XML-RPC users aren't always listed as xmlrpc.php
1791
		if (isset($_GET['menuaction']))
1792
		{
1793
			list(, $action) = explode('.ajax_exec.template.', $_GET['menuaction']);
1794
1795
			if (empty($action)) $action = $_GET['menuaction'];
1796
		}
1797
		else
1798
		{
1799
			$action = $_SERVER['PHP_SELF'];
1800
			// remove EGroupware path, if not installed in webroot
1801
			$egw_path = $GLOBALS['egw_info']['server']['webserver_url'];
1802
			if ($egw_path[0] != '/') $egw_path = parse_url($egw_path,PHP_URL_PATH);
1803
			if ($action == '/Microsoft-Server-ActiveSync')
1804
			{
1805
				$action .= '?Cmd='.$_GET['Cmd'].'&DeviceId='.$_GET['DeviceId'];
1806
			}
1807
			elseif ($egw_path)
1808
			{
1809
				list(,$action) = explode($egw_path,$action,2);
1810
			}
1811
		}
1812
		$this->set_action($action);
1813
1814
		// update dla in access-log table, if we have an access-log row (non-anonymous session)
1815
		if ($this->sessionid_access_log && $update_access_log &&
1816
			// ignore updates (session creation is written) of *dav, avatar and thumbnail, due to possible high volume of updates
1817
			(!preg_match('#^(/webdav|/groupdav|/api/avatar|/api/thumbnail)\.php#', $this->action) ||
1818
				(time() - $_SESSION[self::EGW_SESSION_VAR]['session_logged_dla']) > self::MAX_IGNORE_DLA_LOG) &&
1819
			is_object($GLOBALS['egw']->db))
1820
		{
1821
			$_SESSION[self::EGW_SESSION_VAR]['session_logged_dla'] = time();
1822
1823
			$GLOBALS['egw']->db->update(self::ACCESS_LOG_TABLE,array(
1824
				'session_dla' => time(),
1825
				'session_action' => $this->action,
1826
			) + ($this->action === '/logout.php' ? array() : array(
1827
				'lo' => null,	// just in case it was (automatic) timed out before
1828
			)),array(
1829
				'sessionid' => $this->sessionid_access_log,
1830
			),__LINE__,__FILE__);
1831
		}
1832
1833
		$_SESSION[self::EGW_SESSION_VAR]['session_dla'] = time();
1834
		$_SESSION[self::EGW_SESSION_VAR]['session_action'] = $this->action;
1835
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__.'() _SESSION['.self::EGW_SESSION_VAR.']='.array2string($_SESSION[self::EGW_SESSION_VAR]));
1836
1837
		return $this->action;
1838
	}
1839
1840
	/**
1841
	 * Update notification_heartbeat time of session
1842
	 */
1843
	private function update_notification_heartbeat()
1844
	{
1845
		// update dla in access-log table, if we have an access-log row (non-anonymous session)
1846
		if ($this->sessionid_access_log)
1847
		{
1848
			$GLOBALS['egw']->db->update(self::ACCESS_LOG_TABLE,array(
1849
				'notification_heartbeat' => time(),
1850
			),array(
1851
				'sessionid' => $this->sessionid_access_log,
1852
				'lo IS NULL',
1853
			),__LINE__,__FILE__);
1854
		}
1855
	}
1856
1857
	/**
1858
	 * Read the diverse repositories / init classes with data from the just loged in user
1859
	 *
1860
	 * @return array used to assign to $GLOBALS['egw_info']['user']
1861
	 */
1862
	public function read_repositories()
1863
	{
1864
		$GLOBALS['egw']->acl->__construct($this->account_id);
1865
		$GLOBALS['egw']->preferences->__construct($this->account_id);
1866
		$GLOBALS['egw']->applications->__construct($this->account_id);
1867
1868
		$user = $GLOBALS['egw']->accounts->read($this->account_id);
1869
		// set homedirectory from auth_ldap or auth_ads, to be able to use it in vfs
1870
		if (!isset($user['homedirectory']))
1871
		{
1872
			// authentication happens in login.php, which does NOT yet create egw-object in session
1873
			// --> need to store homedirectory in session
1874
			if(isset($GLOBALS['auto_create_acct']['homedirectory']))
1875
			{
1876
				Cache::setSession(__CLASS__, 'homedirectory',
1877
					$user['homedirectory'] = $GLOBALS['auto_create_acct']['homedirectory']);
1878
			}
1879
			else
1880
			{
1881
				$user['homedirectory'] = Cache::getSession(__CLASS__, 'homedirectory');
1882
			}
1883
		}
1884
		$user['preferences'] = $GLOBALS['egw']->preferences->read_repository();
1885
		if (is_object($GLOBALS['egw']->datetime))
1886
		{
1887
			$GLOBALS['egw']->datetime->__construct();		// to set tz_offset from the now read prefs
1888
		}
1889
		$user['apps']        = $GLOBALS['egw']->applications->read_repository();
1890
		$user['domain']      = $this->account_domain;
1891
		$user['sessionid']   = $this->sessionid;
1892
		$user['kp3']         = $this->kp3;
1893
		$user['session_ip']  = $this->getuser_ip();
1894
		$user['session_lid'] = $this->account_lid.'@'.$this->account_domain;
1895
		$user['account_id']  = $this->account_id;
1896
		$user['account_lid'] = $this->account_lid;
1897
		$user['userid']      = $this->account_lid;
1898
		$user['passwd']      = $this->passwd;
1899
1900
		return $user;
1901
	}
1902
1903
	/**
1904
	 * Splits a login-name into account_lid and eGW-domain/-instance
1905
	 *
1906
	 * @param string $login login-name (ie. user@default)
1907
	 * @param string &$account_lid returned account_lid (ie. user)
1908
	 * @param string &$domain returned domain (ie. domain)
1909
	 */
1910
	private function split_login_domain($login,&$account_lid,&$domain)
1911
	{
1912
		$parts = explode('@',$login);
1913
1914
		//conference - for strings like [email protected]@default ,
1915
		//allows that user have a login that is his e-mail. (viniciuscb)
1916
		if (count($parts) > 1)
1917
		{
1918
			$probable_domain = array_pop($parts);
1919
			//Last part of login string, when separated by @, is a domain name
1920
			if (in_array($probable_domain,$this->egw_domains))
1921
			{
1922
				$got_login = true;
1923
				$domain = $probable_domain;
1924
				$account_lid = implode('@',$parts);
1925
			}
1926
		}
1927
1928
		if (!$got_login)
1929
		{
1930
			$domain = $GLOBALS['egw_info']['server']['default_domain'];
1931
			$account_lid = $login;
1932
		}
1933
	}
1934
1935
	/**
1936
	 * Create a hash from user and pw
1937
	 *
1938
	 * Can be used to check setup config user/password inside egroupware:
1939
	 *
1940
	 * if (Api\Session::user_pw_hash($user,$pw) === $GLOBALS['egw_info']['server']['config_hash'])
1941
	 *
1942
	 * @param string $user username
1943
	 * @param string $password password or md5 hash of password if $allow_password_md5
1944
	 * @param boolean $allow_password_md5 =false can password alread be an md5 hash
1945
	 * @return string
1946
	 */
1947
	static function user_pw_hash($user,$password,$allow_password_md5=false)
1948
	{
1949
		$password_md5 = $allow_password_md5 && preg_match('/^[a-f0-9]{32}$/',$password) ? $password : md5($password);
1950
1951
		$hash = sha1(strtolower($user).$password_md5);
1952
1953
		return $hash;
1954
	}
1955
1956
	/**
1957
	 * Initialise the used session handler
1958
	 *
1959
	 * @return boolean true if we have a session, false otherwise
1960
	 * @throws \ErrorException if there is no PHP session support
1961
	 */
1962
	public static function init_handler()
1963
	{
1964
		switch(session_status())
1965
		{
1966
			case PHP_SESSION_DISABLED:
1967
				throw new \ErrorException('EGroupware requires PHP session extension!');
1968
			case PHP_SESSION_NONE:
1969
				ini_set('session.use_cookies',0);	// disable the automatic use of cookies, as it uses the path / by default
1970
				session_name(self::EGW_SESSION_NAME);
1971
				if (($sessionid = self::get_sessionid()))
1972
				{
1973
					session_id($sessionid);
1974
					self::cache_control();
1975
					$ok = session_start();
1976
					self::decrypt();
1977
					if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."() sessionid=$sessionid, _SESSION[".self::EGW_SESSION_VAR.']='.array2string($_SESSION[self::EGW_SESSION_VAR]));
1978
					return $ok;
1979
				}
1980
				break;
1981
			case PHP_SESSION_ACTIVE:
1982
				return true;	// session created by MServer
1983
		}
1984
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."() no active session!");
1985
1986
		return false;
1987
	}
1988
1989
	/**
1990
	 * Controling caching and expires header
1991
	 *
1992
	 * Headers are send based on given parameters or $GLOBALS['egw_info']['flags']['nocachecontrol']:
1993
	 * - not set of false --> no caching (default)
1994
	 * - true --> private caching by browser (no expires header)
1995
	 * - "public" or integer --> public caching with given cache_expire in minutes or php.ini default session_cache_expire
1996
	 *
1997
	 * @param int $expire =null expiration time in seconds, default $GLOBALS['egw_info']['flags']['nocachecontrol'] or php.ini session.cache_expire
1998
	 * @param int $private =null allows to set private caching with given expiration time, by setting it to true
1999
	 */
2000
	public static function cache_control($expire=null, $private=null)
2001
	{
2002
		if (is_null($expire) && isset($GLOBALS['egw_info']['flags']['nocachecontrol']) && is_int($GLOBALS['egw_info']['flags']['nocachecontrol']))
2003
		{
2004
			$expire = $GLOBALS['egw_info']['flags']['nocachecontrol'];
2005
		}
2006
		// session not yet started: use PHP session_cache_limiter() and session_cache_expires() functions
2007
		if (!isset($_SESSION))
2008
		{
2009
			// controling caching and expires header
2010
			if(!isset($expire) && (!isset($GLOBALS['egw_info']['flags']['nocachecontrol']) ||
2011
				!$GLOBALS['egw_info']['flags']['nocachecontrol']))
2012
			{
2013
				session_cache_limiter('nocache');
2014
			}
2015
			elseif (isset($expire) || $GLOBALS['egw_info']['flags']['nocachecontrol'] === 'public' || is_int($GLOBALS['egw_info']['flags']['nocachecontrol']))
2016
			{
2017
				// allow public caching: proxys, cdns, ...
2018
				if (isset($expire))
2019
				{
2020
					session_cache_expire((int)ceil($expire/60));	// in minutes
2021
				}
2022
				session_cache_limiter($private ? 'private' : 'public');
2023
			}
2024
			else
2025
			{
2026
				// allow caching by browser
2027
				session_cache_limiter('private_no_expire');
2028
			}
2029
		}
2030
		// session already started
2031
		if (isset($_SESSION))
2032
		{
2033
			if ($expire && (session_cache_limiter() !== ($expire===true?'private_no_expire':'public') ||
2034
				is_int($expire) && $expire/60 !== session_cache_expire()))
2035
			{
2036
				$file = $line = null;
2037
				if (headers_sent($file, $line))
2038
				{
2039
					error_log(__METHOD__."($expire) called, but header already sent in $file: $line");
2040
					return;
2041
				}
2042
				if($expire === true)	// same behavior as session_cache_limiter('private_no_expire')
2043
				{
2044
					header('Cache-Control: private, max-age='.(60*session_cache_expire()));
2045
					header_remove('Expires');
2046
				}
2047
				elseif ($private)
2048
				{
2049
					header('Cache-Control: private, max-age='.$expire);
0 ignored issues
show
Bug Best Practice introduced by
The expression $private of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2050
					header('Expires: ' . gmdate('D, d M Y H:i:s', time()+$expire) . ' GMT');
2051
				}
2052
				else
2053
				{
2054
					header('Cache-Control: public, max-age='.$expire);
2055
					header('Expires: ' . gmdate('D, d M Y H:i:s', time()+$expire) . ' GMT');
2056
				}
2057
				// remove Pragma header, might be set by old header
2058
				if (function_exists('header_remove'))	// PHP 5.3+
2059
				{
2060
					header_remove('Pragma');
2061
				}
2062
				else
2063
				{
2064
					header('Pragma:');
2065
				}
2066
			}
2067
		}
2068
	}
2069
2070
	/**
2071
	 * Get a session list (of the current instance)
2072
	 *
2073
	 * @param int $start
2074
	 * @param string $sort ='DESC' ASC or DESC
2075
	 * @param string $order ='session_dla' session_lid, session_id, session_started, session_logintime, session_action, or (default) session_dla
2076
	 * @param boolean $all_no_sort =False skip sorting and limiting to maxmatchs if set to true
2077
	 * @param array $filter =array() extra filter for sessions
2078
	 * @return array with sessions (values for keys as in $sort)
2079
	 */
2080
	public static function session_list($start,$sort='DESC',$order='session_dla',$all_no_sort=False,array $filter=array())
2081
	{
2082
		$sessions = array();
2083
		if (!preg_match('/^[a-z0-9_ ,]+$/i',$order_by=$order.' '.$sort) || $order_by == ' ')
2084
		{
2085
			$order_by = 'session_dla DESC';
2086
		}
2087
		$filter['lo'] = null;
2088
		$filter[] = 'account_id>0';
2089
		$filter[] = 'session_dla > '.(int)(time() - $GLOBALS['egw_info']['server']['sessions_timeout']);
2090
		$filter[] = '(notification_heartbeat IS NULL OR notification_heartbeat > '.self::heartbeat_limit().')';
2091
		foreach($GLOBALS['egw']->db->select(self::ACCESS_LOG_TABLE, '*', $filter, __LINE__, __FILE__,
2092
			$all_no_sort ? false : $start, 'ORDER BY '.$order_by) as $row)
2093
		{
2094
			$sessions[$row['sessionid']] = $row;
2095
		}
2096
		return $sessions;
2097
	}
2098
2099
	/**
2100
	 * Query number of sessions (not more then once every N secs)
2101
	 *
2102
	 * @param array $filter =array() extra filter for sessions
2103
	 * @return int number of active sessions
2104
	 */
2105
	public static function session_count(array $filter=array())
2106
	{
2107
		$filter['lo'] = null;
2108
		$filter[] = 'account_id>0';
2109
		$filter[] = 'session_dla > '.(int)(time() - $GLOBALS['egw_info']['server']['sessions_timeout']);
2110
		$filter[] = '(notification_heartbeat IS NULL OR notification_heartbeat > '.self::heartbeat_limit().')';
2111
		return $GLOBALS['egw']->db->select(self::ACCESS_LOG_TABLE, 'COUNT(*)', $filter, __LINE__, __FILE__)->fetchColumn();
2112
	}
2113
2114
	/**
2115
	 * Get limit / latest time of heartbeat for session to be active
2116
	 *
2117
	 * @return int TS in server-time
2118
	 */
2119
	public static function heartbeat_limit()
2120
	{
2121
		static $limit=null;
2122
2123
		if (is_null($limit))
2124
		{
2125
			$config = Config::read('notifications');
2126
			if (!($popup_poll_interval  = $config['popup_poll_interval']))
2127
			{
2128
				$popup_poll_interval = 60;
2129
			}
2130
			$limit = (int)(time() - $popup_poll_interval-10);	// 10s grace periode
2131
		}
2132
		return $limit;
2133
	}
2134
2135
	/**
2136
	 * Check if given user can be reached via notifications
2137
	 *
2138
	 * Checks if notifications callback checked in not more then heartbeat_limit() seconds ago
2139
	 *
2140
	 * @param int $account_id
2141
	 * @param int number of active sessions of given user with notifications running
2142
	 */
2143
	public static function notifications_active($account_id)
2144
	{
2145
		return $GLOBALS['egw']->db->select(self::ACCESS_LOG_TABLE, 'COUNT(*)', array(
2146
				'lo' => null,
2147
				'session_dla > '.(int)(time() - $GLOBALS['egw_info']['server']['sessions_timeout']),
2148
				'account_id' => $account_id,
2149
				'notification_heartbeat > '.self::heartbeat_limit(),
2150
		), __LINE__, __FILE__)->fetchColumn();
2151
	}
2152
}
2153