Test Setup Failed
Push — master ( 12c298...4a14e0 )
by Ralf
22:03
created

Session::__construct()   D

Complexity

Conditions 11
Paths 386

Size

Total Lines 69
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 39
c 1
b 0
f 0
nc 386
nop 1
dl 0
loc 69
rs 4.1583

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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]));
0 ignored issues
show
Deprecated Code introduced by
The function mcrypt_generic() has been deprecated: 7.1 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

342
					$_SESSION[$name] = /** @scrutinizer ignore-deprecated */ mcrypt_generic(self::$mcrypt,serialize($_SESSION[$name]));

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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);
0 ignored issues
show
Deprecated Code introduced by
The function mcrypt_generic_deinit() has been deprecated: 7.1 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

348
			/** @scrutinizer ignore-deprecated */ mcrypt_generic_deinit(self::$mcrypt);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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;
0 ignored issues
show
introduced by
The condition is_array($arr) is always true.
Loading history...
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])));
0 ignored issues
show
Deprecated Code introduced by
The function mdecrypt_generic() has been deprecated: 7.1 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

397
					$_SESSION[$name] = unserialize(trim(/** @scrutinizer ignore-deprecated */ mdecrypt_generic(self::$mcrypt,$_SESSION[$name])));

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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, '')))
0 ignored issues
show
Deprecated Code introduced by
The function mcrypt_module_open() has been deprecated: 7.1 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

431
			if (!(self::$mcrypt = /** @scrutinizer ignore-deprecated */ mcrypt_module_open(MCRYPT_TRIPLEDES, '', MCRYPT_MODE_ECB, '')))

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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);
0 ignored issues
show
Deprecated Code introduced by
The function mcrypt_enc_get_iv_size() has been deprecated: 7.1 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

436
			$iv_size = /** @scrutinizer ignore-deprecated */ mcrypt_enc_get_iv_size(self::$mcrypt);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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);
0 ignored issues
show
Deprecated Code introduced by
The function mcrypt_create_iv() has been deprecated: 7.1 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

438
				/** @scrutinizer ignore-deprecated */ mcrypt_create_iv ($iv_size, MCRYPT_RAND) : substr($GLOBALS['egw_info']['server']['mcrypt_iv'],0,$iv_size);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
439
440
			if (mcrypt_generic_init(self::$mcrypt,$kp3, $iv) < 0)
0 ignored issues
show
Deprecated Code introduced by
The function mcrypt_generic_init() has been deprecated: 7.1 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

440
			if (/** @scrutinizer ignore-deprecated */ mcrypt_generic_init(self::$mcrypt,$kp3, $iv) < 0)

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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))
0 ignored issues
show
introduced by
The condition is_array($login) is always false.
Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The property passwd_type does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
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);
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\Session::split_login_domain() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

480
			self::/** @scrutinizer ignore-call */ 
481
         split_login_domain($login,$this->account_lid,$this->account_domain);
Loading history...
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
				session_id($this->sessionid);
554
			}
555
			else
556
			{
557
				self::cache_control();
558
				session_start();
559
				// set a new session-id, if not syncml (already done in Horde code and can NOT be changed)
560
				if (!$no_session && $GLOBALS['egw_info']['flags']['currentapp'] != 'syncml')
561
				{
562
					session_regenerate_id(true);
563
				}
564
				$this->sessionid = session_id();
565
			}
566
			$this->kp3       = Auth::randomstring(24);
567
568
			$GLOBALS['egw_info']['user'] = $this->read_repositories();
569
			if ($GLOBALS['egw']->accounts->is_expired($GLOBALS['egw_info']['user']))
570
			{
571
				$this->reason = 'account is expired';
572
				$this->cd_reason = self::CD_ACCOUNT_EXPIRED;
573
574
				if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) UNSUCCESSFULL ($this->reason)");
575
				return false;
576
			}
577
578
			Cache::setSession('phpgwapi', 'password', base64_encode($this->passwd));
579
580
			// if we have a second factor, check it before forced password change
581
			if ($check_2fa !== false)
582
			{
583
				try {
584
					$this->checkMultifactorAuth($check_2fa, $_COOKIE[self::REMEMBER_ME_COOKIE]);
585
				}
586
				catch(\Exception $e) {
587
					$this->cd_reason = $e->getCode();
588
					$this->reason = $e->getMessage();
589
					$this->log_access($this->reason, $login, $user_ip, 0);	// log unsuccessfull login
590
					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)");
591
					return false;
592
				}
593
			}
594
595
			if ($fail_on_forced_password_change && Auth::check_password_change($this->reason) === false)
596
			{
597
				$this->cd_reason = self::CD_FORCE_PASSWORD_CHANGE;
598
				return false;
599
			}
600
601
			if ($GLOBALS['egw']->acl->check('anonymous',1,'phpgwapi'))
602
			{
603
				$this->session_flags = 'A';
604
			}
605
			else
606
			{
607
				$this->session_flags = 'N';
608
			}
609
610
			if (($hook_result = Hooks::process(array(
611
				'location'       => 'session_creation',
612
				'sessionid'      => $this->sessionid,
613
				'session_flags'  => $this->session_flags,
614
				'account_id'     => $this->account_id,
615
				'account_lid'    => $this->account_lid,
616
				'passwd'         => $this->passwd,
617
				'account_domain' => $this->account_domain,
618
				'user_ip'        => $user_ip,
619
			),'',true)))	// true = run hooks from all apps, not just the ones the current user has perms to run
620
			{
621
				foreach($hook_result as $reason)
622
				{
623
					if ($reason)	// called hook requests to deny the session
624
					{
625
						$this->reason = $this->cd_reason = $reason;
626
						$this->log_access($this->reason,$login,$user_ip,0);		// log unsuccessfull login
627
						if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) UNSUCCESSFULL ($this->reason)");
628
						return false;
629
					}
630
				}
631
			}
632
			$GLOBALS['egw']->db->transaction_begin();
633
			$this->register_session($this->login,$user_ip,$now,$this->session_flags);
634
			if ($this->session_flags != 'A')		// dont log anonymous sessions
635
			{
636
				$this->sessionid_access_log = $this->log_access($this->sessionid,$login,$user_ip,$this->account_id);
637
				// We do NOT log anonymous sessions to not block website and also to cope with
638
				// high rate anon endpoints might be called creating a bottleneck in the egw_accounts table.
639
				Cache::setSession('phpgwapi', 'account_previous_login', $GLOBALS['egw']->auth->previous_login);
640
				$GLOBALS['egw']->accounts->update_lastlogin($this->account_id,$user_ip);
641
			}
642
			$GLOBALS['egw']->db->transaction_commit();
643
644
			if ($GLOBALS['egw_info']['server']['usecookies'] && !$no_session)
645
			{
646
				self::egw_setcookie(self::EGW_SESSION_NAME,$this->sessionid);
647
				self::egw_setcookie('kp3',$this->kp3);
648
				self::egw_setcookie('domain',$this->account_domain);
649
			}
650
			if ($GLOBALS['egw_info']['server']['usecookies'] && !$no_session || isset($_COOKIE['last_loginid']))
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($GLOBALS['egw_info']['s...o_session) || IssetNode, Probably Intended Meaning: $GLOBALS['egw_info']['se...o_session || IssetNode)
Loading history...
651
			{
652
				self::egw_setcookie('last_loginid', $this->account_lid ,$now+1209600); /* For 2 weeks */
653
				self::egw_setcookie('last_domain',$this->account_domain,$now+1209600);
654
			}
655
656
			// set new remember me token/cookie, if requested and necessary
657
			$expiration = null;
658
			if (($token = $this->checkSetRememberMeToken($remember_me, $_COOKIE[self::REMEMBER_ME_COOKIE], $expiration)))
659
			{
660
				self::egw_setcookie(self::REMEMBER_ME_COOKIE, $token, $expiration);
661
			}
662
663
			if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) successfull sessionid=$this->sessionid");
664
665
			// hook called once session is created
666
			Hooks::process(array(
667
				'location'       => 'session_created',
668
				'sessionid'      => $this->sessionid,
669
				'session_flags'  => $this->session_flags,
670
				'account_id'     => $this->account_id,
671
				'account_lid'    => $this->account_lid,
672
				'passwd'         => $this->passwd,
673
				'account_domain' => $this->account_domain,
674
				'user_ip'        => $user_ip,
675
				'session_type'   => Session\Type::get($_SERVER['REQUEST_URI'],
676
					$GLOBALS['egw_info']['flags']['current_app'],
677
					true),	// true return WebGUI instead of login, as we are logged in now
678
			),'',true);
679
680
			return $this->sessionid;
681
		}
682
		// catch all exceptions, as their (allways logged) trace (eg. on a database error) would contain the user password
683
		catch(Exception $e) {
684
			$this->reason = $this->cd_reason = is_a($e, Db\Exception::class) ?
0 ignored issues
show
Documentation Bug introduced by
The property $cd_reason was declared of type integer, but is_a($e, EGroupware\Api\...r!') : $e->getMessage() is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
685
				// do not output specific database error, eg. invalid SQL statement
686
				lang('Database Error!') : $e->getMessage();
687
			error_log(__METHOD__."('$login', ".array2string(str_repeat('*', strlen($passwd))).
688
				", '$passwd_type', no_session=".array2string($no_session).
689
				", auth_check=".array2string($auth_check).
690
				", fail_on_forced_password_change=".array2string($fail_on_forced_password_change).
691
				") Exception ".$e->getMessage());
692
			return false;
693
		}
694
	}
695
696
	/**
697
	 * Check if password authentication is required or given token is sufficient
698
	 *
699
	 * Token is only checked for 'remember_me_token' === 'always', not for default of only for 2FA!
700
	 *
701
	 * Password auth is also required if 2FA is not disabled and either required or configured by user.
702
	 *
703
	 * @param string $token value of token
704
	 * @param int& $account_id =null account_id of token-owner to limit check on that user, on return account_id of token owner
705
	 * @return boolean false: if further auth check is required, true: if token is sufficient for authentication
706
	 */
707
	public function skipPasswordAuth($token, &$account_id=null)
708
	{
709
		// if token is empty or disabled --> password authentication required
710
		if (empty($token) || $GLOBALS['egw_info']['server']['remember_me_token'] !== 'always' ||
711
			!($client = $this->checkOpenIDconfigured()))
712
		{
713
			return false;
714
		}
715
716
		// check if token exists and is (still) valid
717
		$tokenRepo = new OpenID\Repositories\AccessTokenRepository();
718
		if (!($access_token = $tokenRepo->findToken($client, $account_id, 'PT1S', $token)))
0 ignored issues
show
Unused Code introduced by
The call to EGroupware\OpenID\Reposi...Repository::findToken() has too many arguments starting with $token. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

718
		if (!($access_token = $tokenRepo->/** @scrutinizer ignore-call */ findToken($client, $account_id, 'PT1S', $token)))

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
719
		{
720
			return false;
721
		}
722
		$account_id = $access_token->getUserIdentifier();
723
724
		// check if we need a second factor
725
		if ($GLOBALS['egw_info']['server']['2fa_required'] !== 'disabled' &&
726
			(($creds = Credentials::read(0, Credentials::TWOFA, $account_id)) ||
0 ignored issues
show
Unused Code introduced by
The assignment to $creds is dead and can be removed.
Loading history...
727
				$GLOBALS['egw_info']['server']['2fa_required'] === 'strict'))
728
		{
729
			return false;
730
		}
731
732
		// access-token is sufficient
733
		return true;
734
	}
735
736
	/**
737
	 * Check multifcator authemtication
738
	 *
739
	 * @param string $code 2fa-code
740
	 * @param string $token remember me token
741
	 * @throws \Exception with error-message if NOT successful
742
	 */
743
	protected function checkMultifactorAuth($code, $token)
744
	{
745
		$errors = $factors = [];
746
747
		if ($GLOBALS['egw_info']['server']['2fa_required'] === 'disabled')
748
		{
749
			return;	// nothing to check
750
		}
751
752
		// check if token exists and is (still) valid
753
		if (!empty($token) && $GLOBALS['egw_info']['server']['remember_me_token'] !== 'disabled' &&
754
			($client = $this->checkOpenIDconfigured()))
755
		{
756
			$tokenRepo = new OpenID\Repositories\AccessTokenRepository();
757
			if ($tokenRepo->findToken($client, $this->account_id, 'PT1S', $token))
0 ignored issues
show
Unused Code introduced by
The call to EGroupware\OpenID\Reposi...Repository::findToken() has too many arguments starting with $token. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

757
			if ($tokenRepo->/** @scrutinizer ignore-call */ findToken($client, $this->account_id, 'PT1S', $token))

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
758
			{
759
				$factors['remember_me_token'] = true;
760
			}
761
			else
762
			{
763
				$errors['remember_me_token'] = lang("Invalid or expired 'remember me' token");
764
			}
765
		}
766
767
		// if 2fa is configured by user, check it
768
		if (($creds = Credentials::read(0, Credentials::TWOFA, $this->account_id)))
769
		{
770
			if (empty($code))
771
			{
772
				$errors['2fa_code'] = lang('2-Factor Authentication code required');
773
			}
774
			else
775
			{
776
				$google2fa = new Google2FA\Google2FA();
777
				if (!empty($code) && $google2fa->verify($code, $creds['2fa_password']))
778
				{
779
					$factors['2fa_code'] = true;
780
				}
781
				else
782
				{
783
					$errors['2fa_code'] = lang('Invalid 2-Factor Authentication code');
784
				}
785
			}
786
		}
787
788
		// check for more factors and/or policies
789
		// hook can add factors, errors or throw \Exception with error-message and -code
790
		Hooks::process([
791
			'location' => 'multifactor_policy',
792
			'factors' => &$factors,
793
			'errors' => &$errors,
794
			'2fa_code' => $code,
795
			'remember_me_token' => $token,
796
		], [], true);
797
798
		if (!count($factors) && (count($errors) ||
799
			$GLOBALS['egw_info']['server']['2fa_required'] === 'strict'))
800
		{
801
			if (!empty($code) && isset($errors['2fa_code']))
802
			{
803
				// we log the missing factor, but externally only show "Bad Login or Password"
804
				// to give no indication that the password was already correct
805
				throw new \Exception(implode(', ', $errors), self::CD_BAD_LOGIN_OR_PASSWORD);
806
			}
807
			else
808
			{
809
				throw new \Exception(implode(', ', $errors), self::CD_SECOND_FACTOR_REQUIRED);
810
			}
811
		}
812
	}
813
814
	/**
815
	 * Check if we need to set a remember me token/cookie
816
	 *
817
	 * @param string $remember_me =null "True" for checkbox checked, or periode for user-choice select-box eg. "P1W" or "" for NOT remember
818
	 * @param string $token current remember me token
819
	 * @param int& $expriation on return expiration time of new cookie
820
	 * @return string new token to set as Cookieor null to not set a new one
821
	 */
822
	protected function checkSetRememberMeToken($remember_me, $token, &$expiration)
823
	{
824
		// do we need a new token
825
		if (!empty($remember_me) && $GLOBALS['egw_info']['server']['remember_me_token'] !== 'disabled' &&
826
			($client = $this->checkOpenIDconfigured()))
827
		{
828
			if (!empty($token))
829
			{
830
				// check if token exists and is (still) valid
831
				$tokenRepo = new OpenID\Repositories\AccessTokenRepository();
832
				if ($tokenRepo->findToken($client, $this->account_id, 'PT1S', $token))
0 ignored issues
show
Unused Code introduced by
The call to EGroupware\OpenID\Reposi...Repository::findToken() has too many arguments starting with $token. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

832
				if ($tokenRepo->/** @scrutinizer ignore-call */ findToken($client, $this->account_id, 'PT1S', $token))

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
833
				{
834
					return null;	// token still valid, no need to set it again
835
				}
836
			}
837
			$lifetime = $this->rememberMeTokenLifetime(is_string($remember_me) ? $remember_me : null);
0 ignored issues
show
introduced by
The condition is_string($remember_me) is always true.
Loading history...
838
			$expiration = $this->rememberMeTokenLifetime(is_string($remember_me) ? $remember_me : null, true);
0 ignored issues
show
introduced by
The condition is_string($remember_me) is always true.
Loading history...
839
840
			$tokenFactory = new OpenID\Token();
841
			if (($token = $tokenFactory->accessToken(self::OPENID_REMEMBER_ME_CLIENT_ID, [], $lifetime, false, $lifetime, false)))
0 ignored issues
show
Unused Code introduced by
The call to EGroupware\OpenID\Token::accessToken() has too many arguments starting with false. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

841
			if (($token = $tokenFactory->/** @scrutinizer ignore-call */ accessToken(self::OPENID_REMEMBER_ME_CLIENT_ID, [], $lifetime, false, $lifetime, false)))

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
842
			{
843
				return $token->getIdentifier();
844
			}
845
		}
846
		return null;
847
	}
848
849
	/**
850
	 * Check if 'remember me' token should be deleted on explict logout
851
	 *
852
	 * @return boolean false: if 2FA is enabeld for user, true: otherwise
853
	 */
854
	public function removeRememberMeTokenOnLogout()
855
	{
856
		return $GLOBALS['egw_info']['server']['2fa_required'] === 'disabled' ||
857
			$GLOBALS['egw_info']['server']['2fa_required'] !== 'strict' &&
858
			!($creds = Credentials::read(0, Credentials::TWOFA, $this->account_id));
0 ignored issues
show
Unused Code introduced by
The assignment to $creds is dead and can be removed.
Loading history...
859
	}
860
861
	/**
862
	 * OpenID Client ID for remember me token
863
	 */
864
	const OPENID_REMEMBER_ME_CLIENT_ID = 'login-remember-me';
865
866
	/**
867
	 * Check and if not configure OpenID app to generate 'remember me' tokens
868
	 *
869
	 * @return OpenID\Entities\ClientEntity|null null if OpenID Server app is not installed
870
	 */
871
	protected function checkOpenIDconfigured()
872
	{
873
		// OpenID app not installed --> password authentication required
874
		if (!isset($GLOBALS['egw_info']['apps']))
875
		{
876
			$GLOBALS['egw']->applications->read_installed_apps();
877
		}
878
		if (empty($GLOBALS['egw_info']['apps']['openid']))
879
		{
880
			return null;
881
		}
882
883
		$clients = new OpenID\Repositories\ClientRepository();
884
		try {
885
			$client = $clients->getClientEntity(self::OPENID_REMEMBER_ME_CLIENT_ID, null, null, false);	// false = do NOT check client-secret
886
		}
887
		catch (OAuthServerException $e)
888
		{
889
			unset($e);
890
			$client = new OpenID\Entities\ClientEntity();
891
			$client->setIdentifier(self::OPENID_REMEMBER_ME_CLIENT_ID);
892
			$client->setSecret(Auth::randomstring(24));	// must not be unset
0 ignored issues
show
Bug introduced by
The method setSecret() does not exist on EGroupware\OpenID\Entities\ClientEntity. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

892
			$client->/** @scrutinizer ignore-call */ 
893
            setSecret(Auth::randomstring(24));	// must not be unset

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
893
			$client->setName(lang('Remember me token'));
894
			$client->setAccessTokenTTL($this->rememberMeTokenLifetime());
895
			$client->setRefreshTokenTTL('P0S');	// no refresh token
896
			$client->setRedirectUri($GLOBALS['egw_info']['server']['webserver_url'].'/');
897
			$clients->persistNewClient($client);
0 ignored issues
show
Bug introduced by
The method persistNewClient() does not exist on EGroupware\OpenID\Repositories\ClientRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

897
			$clients->/** @scrutinizer ignore-call */ 
898
             persistNewClient($client);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
898
		}
899
		return $client;
900
	}
901
902
	/**
903
	 * Return lifetime for remember me token
904
	 *
905
	 * @param string $user user choice, if allowed
906
	 * @param boolean $ts =false false: return periode string, true: return integer timestamp
907
	 * @return string periode spec eg. 'P1M'
908
	 */
909
	protected function rememberMeTokenLifetime($user=null, $ts=false)
910
	{
911
		switch ((string)$GLOBALS['egw_info']['server']['remember_me_lifetime'])
912
		{
913
			case 'user':
914
				if (!empty($user))
915
				{
916
					$lifetime = $user;
917
					break;
918
				}
919
				// fall-through for default lifetime
920
			case '':	// default lifetime
921
				$lifetime = 'P1M';
922
				break;
923
			default:
924
				$lifetime = $GLOBALS['egw_info']['server']['remember_me_lifetime'];
925
				break;
926
		}
927
		if ($ts)
928
		{
929
			$expiration = new DateTime('now', DateTime::$server_timezone);
930
			$expiration->add(new \DateInterval($lifetime));
931
			return $expiration->format('ts');
932
		}
933
		return $lifetime;
934
	}
935
936
	/**
937
	 * Store eGW specific session-vars
938
	 *
939
	 * @param string $login
940
	 * @param string $user_ip
941
	 * @param int $now
942
	 * @param string $session_flags
943
	 */
944
	private function register_session($login,$user_ip,$now,$session_flags)
945
	{
946
		// restore session vars set before session was started
947
		if (is_array($this->required_files))
0 ignored issues
show
introduced by
The condition is_array($this->required_files) is always true.
Loading history...
948
		{
949
			$_SESSION[self::EGW_REQUIRED_FILES] = !is_array($_SESSION[self::EGW_REQUIRED_FILES]) ? $this->required_files :
950
				array_unique(array_merge($_SESSION[self::EGW_REQUIRED_FILES],$this->required_files));
951
			unset($this->required_files);
952
		}
953
		$_SESSION[self::EGW_SESSION_VAR] = array(
954
			'session_id'     => $this->sessionid,
955
			'session_lid'    => $login,
956
			'session_ip'     => $user_ip,
957
			'session_logintime' => $now,
958
			'session_dla'    => $now,
959
			'session_action' => $_SERVER['PHP_SELF'],
960
			'session_flags'  => $session_flags,
961
			// we need the install-id to differ between serveral installs shareing one tmp-dir
962
			'session_install_id' => $GLOBALS['egw_info']['server']['install_id']
963
		);
964
	}
965
966
	/**
967
	 * name of access-log table
968
	 */
969
	const ACCESS_LOG_TABLE = 'egw_access_log';
970
971
	/**
972
	 * Prefix used to log unsucessful login attempts in cache, if DB is unavailable
973
	 */
974
	const FALSE_IP_CACHE_PREFIX = 'false_ip-';
975
	const FALSE_ID_CACHE_PREFIX = 'false_id-';
976
977
	/**
978
     * Write or update (for logout) the access_log
979
	 *
980
	 * We do NOT log anonymous sessions to not block website and also to cope with
981
	 * high rate anon endpoints might be called creating a bottleneck in the egw_access_log table.
982
	 *
983
	 * @param string|int $sessionid nummeric or PHP session id or error-message for unsuccessful logins
984
	 * @param string $login ='' account_lid (evtl. with domain) or '' for setting the logout-time
985
	 * @param string $user_ip ='' ip to log
986
	 * @param int $account_id =0 numerical account_id
987
	 * @return int $sessionid primary key of egw_access_log for login, null otherwise
988
	 */
989
	private function log_access($sessionid,$login='',$user_ip='',$account_id=0)
990
	{
991
		// do not log anything for anonymous sessions
992
		if ($this->session_flags === 'A')
993
		{
994
			return;
995
		}
996
		$now = time();
997
998
		// if sessionid contains non-ascii chars (only happens for error-messages)
999
		// --> transliterate it to ascii, as session_php only allows ascii chars
1000
		if (preg_match('/[^\x20-\x7f]/', $sessionid))
1001
		{
1002
			$sessionid = Translation::to_ascii($sessionid);
1003
		}
1004
1005
		if ($login)
1006
		{
1007
			$GLOBALS['egw']->db->insert(self::ACCESS_LOG_TABLE,array(
1008
				'session_php' => $sessionid,
1009
				'loginid'   => $login,
1010
				'ip'        => $user_ip,
1011
				'li'        => $now,
1012
				'account_id'=> $account_id,
1013
				'user_agent'=> $_SERVER['HTTP_USER_AGENT'],
1014
				'session_dla'    => $now,
1015
				'session_action' => $this->update_dla(false),	// dont update egw_access_log
1016
			),false,__LINE__,__FILE__);
1017
1018
			$_SESSION[self::EGW_SESSION_VAR]['session_logged_dla'] = $now;
1019
1020
			$ret = $GLOBALS['egw']->db->get_last_insert_id(self::ACCESS_LOG_TABLE,'sessionid');
1021
1022
			// if we can not store failed login attempts in database, store it in cache
1023
			if (!$ret && !$account_id)
1024
			{
1025
				Cache::setInstance(__CLASS__, self::FALSE_IP_CACHE_PREFIX.$user_ip,
1026
					1+Cache::getInstance(__CLASS__, self::FALSE_IP_CACHE_PREFIX.$user_ip),
1027
					$GLOBALS['egw_info']['server']['block_time'] * 60);
1028
1029
				Cache::setInstance(__CLASS__, self::FALSE_ID_CACHE_PREFIX.$login,
1030
					1+Cache::getInstance(__CLASS__, self::FALSE_ID_CACHE_PREFIX.$login),
1031
					$GLOBALS['egw_info']['server']['block_time'] * 60);
1032
			}
1033
		}
1034
		else
1035
		{
1036
			if (!is_numeric($sessionid) && $sessionid == $this->sessionid && $this->sessionid_access_log)
1037
			{
1038
				$sessionid = $this->sessionid_access_log;
1039
			}
1040
			$GLOBALS['egw']->db->update(self::ACCESS_LOG_TABLE,array(
1041
				'lo' => $now
1042
			),is_numeric($sessionid) ? array(
1043
				'sessionid' => $sessionid,
1044
			) : array(
1045
				'session_php' => $sessionid,
1046
			),__LINE__,__FILE__);
1047
1048
			// run maintenance only on logout, to not delay login
1049
			if ($GLOBALS['egw_info']['server']['max_access_log_age'])
1050
			{
1051
				$max_age = $now - $GLOBALS['egw_info']['server']['max_access_log_age'] * 24 * 60 * 60;
1052
1053
				$GLOBALS['egw']->db->delete(self::ACCESS_LOG_TABLE,"li < $max_age",__LINE__,__FILE__);
1054
			}
1055
		}
1056
		//error_log(__METHOD__."('$sessionid', '$login', '$user_ip', $account_id) returning ".array2string($ret));
1057
		return $ret;
1058
	}
1059
1060
	/**
1061
	 * Protect against brute force attacks, block login if too many unsuccessful login attmepts
1062
     *
1063
	 * @param string $login account_lid (evtl. with domain)
1064
	 * @param string $ip ip of the user
1065
	 * @returns bool login blocked?
1066
	 */
1067
	private function login_blocked($login,$ip)
1068
	{
1069
		$block_time = time() - $GLOBALS['egw_info']['server']['block_time'] * 60;
1070
1071
		$false_id = $false_ip = 0;
1072
		foreach($GLOBALS['egw']->db->union(array(
1073
			array(
1074
				'table' => self::ACCESS_LOG_TABLE,
1075
				'cols'  => "'false_ip' AS name,COUNT(*) AS num",
1076
				'where' => array(
1077
					'account_id' => 0,
1078
					'ip' => $ip,
1079
					"li > $block_time",
1080
				),
1081
			),
1082
			array(
1083
				'table' => self::ACCESS_LOG_TABLE,
1084
				'cols'  => "'false_id' AS name,COUNT(*) AS num",
1085
				'where' => array(
1086
					'account_id' => 0,
1087
					'loginid' => $login,
1088
					"li > $block_time",
1089
				),
1090
			),
1091
			array(
1092
				'table' => self::ACCESS_LOG_TABLE,
1093
				'cols'  => "'false_id' AS name,COUNT(*) AS num",
1094
				'where' => array(
1095
					'account_id' => 0,
1096
					'loginid LIKE '.$GLOBALS['egw']->db->quote($login.'@%'),
1097
					"li > $block_time",
1098
				)
1099
			),
1100
		), __LINE__, __FILE__) as $row)
1101
		{
1102
			${$row['name']} += $row['num'];
1103
		}
1104
1105
		// check cache too, in case DB is readonly
1106
		$false_ip += Cache::getInstance(__CLASS__, self::FALSE_IP_CACHE_PREFIX.$ip);
1107
		$false_id += Cache::getInstance(__CLASS__, self::FALSE_ID_CACHE_PREFIX.$login);
1108
1109
		// if IP matches one in the (comma-separated) whitelist
1110
		// --> check with whitelists optional number (none means never block)
1111
		$matches = null;
1112
		if (!empty($GLOBALS['egw_info']['server']['unsuccessful_ip_whitelist']) &&
1113
			preg_match_all('/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d+)?/',
1114
				$GLOBALS['egw_info']['server']['unsuccessful_ip_whitelist'], $matches) &&
1115
			($key=array_search($ip, $matches[1])) !== false)
1116
		{
1117
			$blocked = !empty($matches[3][$key]) && $false_ip > $matches[3][$key];
1118
		}
1119
		else	// else check with general number
1120
		{
1121
			$blocked = $false_ip > $GLOBALS['egw_info']['server']['num_unsuccessful_ip'];
1122
		}
1123
		if (!$blocked)
1124
		{
1125
			$blocked = $false_id > $GLOBALS['egw_info']['server']['num_unsuccessful_id'];
1126
		}
1127
		//error_log(__METHOD__."('$login', '$ip') false_ip=$false_ip, false_id=$false_id --> blocked=".array2string($blocked));
1128
1129
		if ($blocked && $GLOBALS['egw_info']['server']['admin_mails'] &&
1130
			$GLOBALS['egw_info']['server']['login_blocked_mail_time'] < time()-5*60)	// max. one mail every 5mins
1131
		{
1132
			try {
1133
				$mailer = new Mailer();
1134
				// notify admin(s) via email
1135
				$mailer->setFrom('eGroupWare@'.$GLOBALS['egw_info']['server']['mail_suffix']);
1136
				$mailer->addHeader('Subject', lang("eGroupWare: login blocked for user '%1', IP %2",$login,$ip));
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $login. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1136
				$mailer->addHeader('Subject', /** @scrutinizer ignore-call */ lang("eGroupWare: login blocked for user '%1', IP %2",$login,$ip));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1137
				$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));
1138
				foreach(preg_split('/,\s*/',$GLOBALS['egw_info']['server']['admin_mails']) as $mail)
1139
				{
1140
					$mailer->addAddress($mail);
1141
				}
1142
				$mailer->send();
1143
			}
1144
			catch(\Exception $e) {
1145
				// ignore exception, but log it, to block the account and give a correct error-message to user
1146
				error_log(__METHOD__."('$login', '$ip') ".$e->getMessage());
1147
			}
1148
			// save time of mail, to not send to many mails
1149
			$config = new Config('phpgwapi');
1150
			$config->read_repository();
1151
			$config->value('login_blocked_mail_time',time());
1152
			$config->save_repository();
1153
		}
1154
		return $blocked;
1155
	}
1156
1157
	/**
1158
	 * Basename of scripts for which we create a pseudo session-id based on user-credentials
1159
	 *
1160
	 * @var array
1161
	 */
1162
	static $pseudo_session_scripts = array(
1163
		'webdav.php', 'groupdav.php', 'remote.php', 'share.php'
1164
	);
1165
1166
	/**
1167
	 * Get the sessionid from Cookie, Get-Parameter or basic auth
1168
	 *
1169
	 * @param boolean $only_basic_auth =false return only a basic auth pseudo sessionid, default no
1170
	 * @return string|null (pseudo-)session-id use or NULL if no Cookie or Basic-Auth credentials
1171
	 */
1172
	static function get_sessionid($only_basic_auth=false)
1173
	{
1174
		// for WebDAV and GroupDAV we use a pseudo sessionid created from md5(user:passwd)
1175
		// --> allows this stateless protocolls which use basic auth to use sessions!
1176
		if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW']) &&
1177
			(in_array(basename($_SERVER['SCRIPT_NAME']), self::$pseudo_session_scripts) ||
1178
				$_SERVER['SCRIPT_NAME'] === '/Microsoft-Server-ActiveSync'))
1179
		{
1180
			// we generate a pseudo-sessionid from the basic auth credentials
1181
			$sessionid = md5($_SERVER['PHP_AUTH_USER'].':'.$_SERVER['PHP_AUTH_PW'].':'.$_SERVER['HTTP_HOST'].':'.
1182
				EGW_SERVER_ROOT.':'.self::getuser_ip().':'.filemtime(EGW_SERVER_ROOT.'/api/setup/setup.inc.php').
1183
				// for ActiveSync we add the DeviceID
1184
				(isset($_GET['DeviceId']) && $_SERVER['SCRIPT_NAME'] === '/Microsoft-Server-ActiveSync' ? ':'.$_GET['DeviceId'] : '').
1185
				':'.$_SERVER['HTTP_USER_AGENT']);
1186
			//error_log(__METHOD__."($only_basic_auth) HTTP_HOST=$_SERVER[HTTP_HOST], PHP_AUTH_USER=$_SERVER[PHP_AUTH_USER], DeviceId=$_GET[DeviceId]: sessionid=$sessionid");
1187
		}
1188
		// same for digest auth
1189
		elseif (isset($_SERVER['PHP_AUTH_DIGEST']) &&
1190
			in_array(basename($_SERVER['SCRIPT_NAME']), self::$pseudo_session_scripts))
1191
		{
1192
			// we generate a pseudo-sessionid from the digest username, realm and nounce
1193
			// can't use full $_SERVER['PHP_AUTH_DIGEST'], as it changes (contains eg. the url)
1194
			$data = Header\Authenticate::parse_digest($_SERVER['PHP_AUTH_DIGEST']);
1195
			$sessionid = md5($data['username'].':'.$data['realm'].':'.$data['nonce'].':'.$_SERVER['HTTP_HOST'].
1196
				EGW_SERVER_ROOT.':'.self::getuser_ip().':'.filemtime(EGW_SERVER_ROOT.'/api/setup/setup.inc.php').
1197
				':'.$_SERVER['HTTP_USER_AGENT']);
1198
		}
1199
		elseif(!$only_basic_auth && isset($_REQUEST[self::EGW_SESSION_NAME]))
1200
		{
1201
			$sessionid = $_REQUEST[self::EGW_SESSION_NAME];
1202
		}
1203
		elseif(!$only_basic_auth && isset($_COOKIE[self::EGW_SESSION_NAME]))
1204
		{
1205
			$sessionid = $_COOKIE[self::EGW_SESSION_NAME];
1206
		}
1207
		else
1208
		{
1209
			$sessionid = null;
1210
		}
1211
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."() _SERVER[REQUEST_URI]='$_SERVER[REQUEST_URI]' returning ".print_r($sessionid,true));
1212
		return $sessionid;
1213
	}
1214
1215
	/**
1216
	 * Get request or cookie variable with higher precedence to $_REQUEST then $_COOKIE
1217
	 *
1218
	 * In php < 5.3 that's identical to $_REQUEST[$name], but php5.3+ does no longer register cookied in $_REQUEST by default
1219
	 *
1220
	 * 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.
1221
	 *
1222
	 * @param string $name eg. 'kp3' or domain
1223
	 * @return mixed null if it's neither set in $_REQUEST or $_COOKIE
1224
	 */
1225
	static function get_request($name)
1226
	{
1227
		return isset($_REQUEST[$name]) ? $_REQUEST[$name] :
1228
			(isset($_COOKIE[$name]) ? $_COOKIE[$name] :
1229
			(isset($_COOKIE[$name=ucfirst($name)]) ? $_COOKIE[$name] : null));
1230
	}
1231
1232
	/**
1233
	 * Check to see if a session is still current and valid
1234
	 *
1235
	 * @param string $sessionid session id to be verfied
1236
	 * @param string $kp3 ?? to be verified
1237
	 * @return bool is the session valid?
1238
	 */
1239
	function verify($sessionid=null,$kp3=null)
1240
	{
1241
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$sessionid','$kp3') ".function_backtrace());
1242
1243
		$fill_egw_info_and_repositories = !$GLOBALS['egw_info']['flags']['restored_from_session'];
1244
1245
		if(!$sessionid)
1246
		{
1247
			$sessionid = self::get_sessionid();
1248
			$kp3       = self::get_request('kp3');
1249
		}
1250
1251
		$this->sessionid = $sessionid;
1252
		$this->kp3       = $kp3;
1253
1254
1255
		if (!$this->sessionid)
1256
		{
1257
			if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$sessionid') get_sessionid()='".self::get_sessionid()."' No session ID");
1258
			return false;
1259
		}
1260
1261
		switch (session_status())
1262
		{
1263
			case PHP_SESSION_DISABLED:
1264
				throw new ErrorException('EGroupware requires the PHP session extension!');
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\ErrorException was not found. Did you mean ErrorException? If so, make sure to prefix the type with \.
Loading history...
1265
			case PHP_SESSION_NONE:
1266
				session_name(self::EGW_SESSION_NAME);
1267
				session_id($this->sessionid);
1268
				self::cache_control();
1269
				session_start();
1270
				break;
1271
			case PHP_SESSION_ACTIVE:
1272
				// session already started eg. by managementserver_client
1273
		}
1274
1275
		// check if we have a eGroupware session --> return false if not (but dont destroy it!)
1276
		if (is_null($_SESSION) || !isset($_SESSION[self::EGW_SESSION_VAR]))
1277
		{
1278
			if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$sessionid') session does NOT exist!");
1279
			return false;
1280
		}
1281
		$session =& $_SESSION[self::EGW_SESSION_VAR];
1282
1283
		if ($session['session_dla'] <= time() - $GLOBALS['egw_info']['server']['sessions_timeout'])
1284
		{
1285
			if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$sessionid') session timed out!");
1286
			$this->destroy($sessionid,$kp3);
1287
			return false;
1288
		}
1289
1290
		$this->session_flags = $session['session_flags'];
1291
1292
		$this->split_login_domain($session['session_lid'],$this->account_lid,$this->account_domain);
1293
1294
		// This is to ensure that we authenticate to the correct domain (might not be default)
1295
		if($GLOBALS['egw_info']['user']['domain'] && $this->account_domain != $GLOBALS['egw_info']['user']['domain'])
1296
		{
1297
			return false;	// session not verified, domain changed
1298
		}
1299
		$GLOBALS['egw_info']['user']['kp3'] = $this->kp3;
1300
1301
		// allow xajax / notifications to not update the dla, so sessions can time out again
1302
		if (!isset($GLOBALS['egw_info']['flags']['no_dla_update']) || !$GLOBALS['egw_info']['flags']['no_dla_update'])
1303
		{
1304
			$this->update_dla(true);
1305
		}
1306
		elseif ($GLOBALS['egw_info']['flags']['currentapp'] == 'notifications')
1307
		{
1308
			$this->update_notification_heartbeat();
1309
		}
1310
		$this->account_id = $GLOBALS['egw']->accounts->name2id($this->account_lid,'account_lid','u');
1311
		if (!$this->account_id)
1312
		{
1313
			if (self::ERROR_LOG_DEBUG) error_log("*** Session::verify($sessionid) !accounts::name2id('$this->account_lid')");
1314
			return false;
1315
		}
1316
1317
		$GLOBALS['egw_info']['user']['account_id'] = $this->account_id;
1318
1319
		if ($fill_egw_info_and_repositories)
1320
		{
1321
			$GLOBALS['egw_info']['user'] = $this->read_repositories();
1322
		}
1323
		else
1324
		{
1325
			// restore apps to $GLOBALS['egw_info']['apps']
1326
			$GLOBALS['egw']->applications->read_installed_apps();
1327
1328
			// session only stores app-names, restore apps from egw_info[apps]
1329
			if (isset($GLOBALS['egw_info']['user']['apps'][0]))
1330
			{
1331
				$GLOBALS['egw_info']['user']['apps'] = array_intersect_key($GLOBALS['egw_info']['apps'], array_flip($GLOBALS['egw_info']['user']['apps']));
1332
			}
1333
1334
			// set prefs, they are no longer stored in session
1335
			$GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->read_repository();
1336
		}
1337
1338
		if ($GLOBALS['egw']->accounts->is_expired($GLOBALS['egw_info']['user']))
1339
		{
1340
			if (self::ERROR_LOG_DEBUG) error_log("*** Session::verify($sessionid) accounts is expired");
1341
			return false;
1342
		}
1343
		$this->passwd = base64_decode(Cache::getSession('phpgwapi', 'password'));
1344
		if ($fill_egw_info_and_repositories)
1345
		{
1346
			$GLOBALS['egw_info']['user']['session_ip'] = $session['session_ip'];
1347
			$GLOBALS['egw_info']['user']['passwd']     = $this->passwd;
1348
		}
1349
		if ($this->account_domain != $GLOBALS['egw_info']['user']['domain'])
1350
		{
1351
			if (self::ERROR_LOG_DEBUG) error_log("*** Session::verify($sessionid) wrong domain");
1352
			return false;
1353
		}
1354
1355
		if ($GLOBALS['egw_info']['server']['sessions_checkip'])
1356
		{
1357
			if (strtoupper(substr(PHP_OS,0,3)) != 'WIN' && (!$GLOBALS['egw_info']['user']['session_ip'] ||
1358
				$GLOBALS['egw_info']['user']['session_ip'] != $this->getuser_ip()))
1359
			{
1360
				if (self::ERROR_LOG_DEBUG) error_log("*** Session::verify($sessionid) wrong IP");
1361
				return false;
1362
			}
1363
		}
1364
1365
		if ($fill_egw_info_and_repositories)
1366
		{
1367
			$GLOBALS['egw']->acl->__construct($this->account_id);
1368
			$GLOBALS['egw']->preferences->__construct($this->account_id);
1369
			$GLOBALS['egw']->applications->__construct($this->account_id);
1370
		}
1371
		if (!$this->account_lid)
1372
		{
1373
			if (self::ERROR_LOG_DEBUG) error_log("*** Session::verify($sessionid) !account_lid");
1374
			return false;
1375
		}
1376
1377
		// query accesslog-id, if not set in session (session is made persistent after login!)
1378
		if (!$this->sessionid_access_log && $this->session_flags != 'A')
1379
		{
1380
			$this->sessionid_access_log = $GLOBALS['egw']->db->select(self::ACCESS_LOG_TABLE,'sessionid',array(
1381
				'session_php' => $this->sessionid,
1382
			),__LINE__,__FILE__)->fetchColumn();
1383
			//error_log(__METHOD__."() sessionid=$this->sessionid --> sessionid_access_log=$this->sessionid_access_log");
1384
		}
1385
1386
		// check if we use cookies for the session, but no cookie set
1387
		// happens eg. in sitemgr (when redirecting to a different domain) or with new java notification app
1388
		if ($GLOBALS['egw_info']['server']['usecookies'] && isset($_REQUEST[self::EGW_SESSION_NAME]) &&
1389
			$_REQUEST[self::EGW_SESSION_NAME] === $this->sessionid &&
1390
			(!isset($_COOKIE[self::EGW_SESSION_NAME]) || $_COOKIE[self::EGW_SESSION_NAME] !== $_REQUEST[self::EGW_SESSION_NAME]))
1391
		{
1392
			if (self::ERROR_LOG_DEBUG) error_log("--> Session::verify($sessionid) SUCCESS, but NO required cookies set --> setting them now");
1393
			self::egw_setcookie(self::EGW_SESSION_NAME,$this->sessionid);
1394
			self::egw_setcookie('kp3',$this->kp3);
1395
			self::egw_setcookie('domain',$this->account_domain);
1396
		}
1397
1398
		if (self::ERROR_LOG_DEBUG) error_log("--> Session::verify($sessionid) SUCCESS");
1399
1400
		return true;
1401
	}
1402
1403
	/**
1404
	 * Terminate a session
1405
	 *
1406
	 * @param int|string $sessionid nummeric or php session id of session to be terminated
1407
	 * @param string $kp3
1408
	 * @return boolean true on success, false on error
1409
	 */
1410
	function destroy($sessionid, $kp3='')
1411
	{
1412
		if (!$sessionid && $kp3)
1413
		{
1414
			return false;
1415
		}
1416
		$this->log_access($sessionid);	// log logout-time
1417
1418
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($sessionid,$kp3)");
1419
1420
		if (is_numeric($sessionid))	// do we have a access-log-id --> get PHP session id
1421
		{
1422
			$sessionid = $GLOBALS['egw']->db->select(self::ACCESS_LOG_TABLE,'session_php',array(
1423
					'sessionid' => $sessionid,
1424
				),__LINE__,__FILE__)->fetchColumn();
1425
		}
1426
1427
		Hooks::process(array(
1428
			'location'  => 'session_destroyed',
1429
			'sessionid' => $sessionid,
1430
		),'',true);	// true = run hooks from all apps, not just the ones the current user has perms to run
1431
1432
		// Only do the following, if where working with the current user
1433
		if (!$GLOBALS['egw_info']['user']['sessionid'] || $sessionid == $GLOBALS['egw_info']['user']['sessionid'])
1434
		{
1435
			if (self::ERROR_LOG_DEBUG) error_log(__METHOD__." ********* about to call session_destroy!");
1436
			session_unset();
1437
			@session_destroy();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for session_destroy(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

1437
			/** @scrutinizer ignore-unhandled */ @session_destroy();

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1438
			// we need to (re-)load the eGW session-handler, as session_destroy unloads custom session-handlers
1439
			if (function_exists('init_session_handler'))
1440
			{
1441
				init_session_handler();
1442
			}
1443
1444
			if ($GLOBALS['egw_info']['server']['usecookies'])
1445
			{
1446
				self::egw_setcookie(session_name());
1447
			}
1448
		}
1449
		else
1450
		{
1451
			$this->commit_session();	// close our own session
1452
1453
			session_id($sessionid);
1454
			if (session_start())
1455
			{
1456
				session_destroy();
1457
			}
1458
		}
1459
		return true;
1460
	}
1461
1462
	/**
1463
	 * Generate a url which supports url or cookies based sessions
1464
	 *
1465
	 * Please note, the values of the query get url encoded!
1466
	 *
1467
	 * @param string $url a url relative to the egroupware install root, it can contain a query too
1468
	 * @param array|string $extravars query string arguements as string or array (prefered)
1469
	 * 	if string is used ambersands in vars have to be already urlencoded as '%26', function ensures they get NOT double encoded
1470
	 * @return string generated url
1471
	 */
1472
	public static function link($url, $extravars = '')
1473
	{
1474
		//error_log(_METHOD__."(url='$url',extravars='".array2string($extravars)."')");
1475
1476
		if ($url[0] != '/')
1477
		{
1478
			$app = $GLOBALS['egw_info']['flags']['currentapp'];
1479
			if ($app != 'login' && $app != 'logout')
1480
			{
1481
				$url = $app.'/'.$url;
1482
			}
1483
		}
1484
1485
		// append the url to the webserver url, but avoid more then one slash between the parts of the url
1486
		$webserver_url = $GLOBALS['egw_info']['server']['webserver_url'];
1487
		// patch inspired by vladimir kolobkov -> we should not try to match the webserver url against the url without '/' as delimiter,
1488
		// as $webserver_url may be part of $url (as /egw is part of phpgwapi/js/egw_instant_load.html)
1489
		if (($url[0] != '/' || $webserver_url != '/') && (!$webserver_url || strpos($url, $webserver_url.'/') === false))
1490
		{
1491
			if($url[0] != '/' && substr($webserver_url,-1) != '/')
1492
			{
1493
				$url = $webserver_url .'/'. $url;
1494
			}
1495
			else
1496
			{
1497
				$url = $webserver_url . $url;
1498
			}
1499
		}
1500
1501
		if(isset($GLOBALS['egw_info']['server']['enforce_ssl']) && $GLOBALS['egw_info']['server']['enforce_ssl'])
1502
		{
1503
			if(substr($url ,0,4) != 'http')
1504
			{
1505
				$url = 'https://'.$_SERVER['HTTP_HOST'].$url;
1506
			}
1507
			else
1508
			{
1509
				$url = str_replace ( 'http:', 'https:', $url);
1510
			}
1511
		}
1512
		$vars = array();
1513
		// add session params if not using cookies
1514
		if (!$GLOBALS['egw_info']['server']['usecookies'])
1515
		{
1516
			$vars[self::EGW_SESSION_NAME] = $GLOBALS['egw']->session->sessionid;
1517
			$vars['kp3'] = $GLOBALS['egw']->session->kp3;
1518
			$vars['domain'] = $GLOBALS['egw']->session->account_domain;
1519
		}
1520
1521
		// check if the url already contains a query and ensure that vars is an array and all strings are in extravars
1522
		list($ret_url,$othervars) = explode('?', $url, 2);
1523
		if ($extravars && is_array($extravars))
1524
		{
1525
			$vars += $extravars;
1526
			$extravars = $othervars;
1527
		}
1528
		else
1529
		{
1530
			if ($othervars) $extravars .= ($extravars?'&':'').$othervars;
1531
		}
1532
1533
		// parse extravars string into the vars array
1534
		if ($extravars)
1535
		{
1536
			foreach(explode('&',$extravars) as $expr)
1537
			{
1538
				list($var,$val) = explode('=', $expr,2);
1539
				if (strpos($val,'%26') != false) $val = str_replace('%26','&',$val);	// make sure to not double encode &
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing strpos($val, '%26') of type integer to the boolean false. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
1540
				if (substr($var,-2) == '[]')
1541
				{
1542
					$vars[substr($var,0,-2)][] = $val;
1543
				}
1544
				else
1545
				{
1546
					$vars[$var] = $val;
1547
				}
1548
			}
1549
		}
1550
1551
		// if there are vars, we add them urlencoded to the url
1552
		if (count($vars))
1553
		{
1554
			$query = array();
1555
			foreach($vars as $key => $value)
1556
			{
1557
				if (is_array($value))
1558
				{
1559
					foreach($value as $val)
1560
					{
1561
						$query[] = $key.'[]='.urlencode($val);
1562
					}
1563
				}
1564
				else
1565
				{
1566
					$query[] = $key.'='.urlencode($value);
1567
				}
1568
			}
1569
			$ret_url .= '?' . implode('&',$query);
1570
		}
1571
		return $ret_url;
1572
	}
1573
1574
	/**
1575
	 * Regexp to validate IPv4 and IPv6
1576
	 */
1577
	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';
1578
1579
	/**
1580
	 * Get the ip address of current users
1581
	 *
1582
	 * We remove further private IPs (from proxys) as they invalidate user
1583
	 * sessions, when they change because of multiple proxys.
1584
	 *
1585
	 * @return string ip address
1586
	 */
1587
	public static function getuser_ip()
1588
	{
1589
		if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
1590
		{
1591
			$forwarded_for = preg_replace('/, *10\..*$/', '', $_SERVER['HTTP_X_FORWARDED_FOR']);
1592
			if (preg_match(self::IP_REGEXP, $forwarded_for))
1593
			{
1594
				return $forwarded_for;
1595
			}
1596
		}
1597
		return $_SERVER['REMOTE_ADDR'];
1598
	}
1599
1600
	/**
1601
	 * domain for cookies
1602
	 *
1603
	 * @var string
1604
	 */
1605
	private static $cookie_domain = '';
1606
1607
	/**
1608
	 * path for cookies
1609
	 *
1610
	 * @var string
1611
	 */
1612
	private static $cookie_path = '/';
1613
1614
	/**
1615
	 * iOS web-apps will loose cookie if set with a livetime of 0 / session-cookie
1616
	 *
1617
	 * Therefore we set a fixed lifetime of 24h from session-start instead.
1618
	 * Server-side session will timeout earliert anyway, if there's no activity.
1619
	 */
1620
	const IOS_SESSION_COOKIE_LIFETIME = 86400;
1621
1622
	/**
1623
	 * Set a cookie with eGW's cookie-domain and -path settings
1624
	 *
1625
	 * @param string $cookiename name of cookie to be set
1626
	 * @param string $cookievalue ='' value to be used, if unset cookie is cleared (optional)
1627
	 * @param int $cookietime =0 when cookie should expire, 0 for session only (optional)
1628
	 * @param string $cookiepath =null optional path (eg. '/') if the eGW install-dir should not be used
1629
	 */
1630
	public static function egw_setcookie($cookiename,$cookievalue='',$cookietime=0,$cookiepath=null)
1631
	{
1632
		if (empty(self::$cookie_domain) || empty(self::$cookie_path))
1633
		{
1634
			self::set_cookiedomain();
1635
		}
1636
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($cookiename,$cookievalue,$cookietime,$cookiepath,".self::$cookie_domain.")");
1637
1638
		// if we are installed in iOS as web-app, we must not set a cookietime==0 (session-cookie),
1639
		// as every change between apps will cause the cookie to get lost
1640
		static $is_iOS = null;
1641
		if (!$cookietime && !isset($is_iOS)) $is_iOS = (bool)preg_match('/^(iPhone|iPad|iPod)/i', Header\UserAgent::mobile());
1642
1643
		if(!headers_sent())	// gives only a warning, but can not send the cookie anyway
1644
		{
1645
			setcookie($cookiename, $cookievalue,
1646
				!$cookietime && $is_iOS ? time()+self::IOS_SESSION_COOKIE_LIFETIME : $cookietime,
1647
				is_null($cookiepath) ? self::$cookie_path : $cookiepath,self::$cookie_domain,
1648
				// if called via HTTPS, only send cookie for https and only allow cookie access via HTTP (true)
1649
				empty($GLOBALS['egw_info']['server']['insecure_cookies']) && Header\Http::schema() === 'https', true);
1650
		}
1651
	}
1652
1653
	/**
1654
	 * Set the domain and path used for cookies
1655
	 */
1656
	private static function set_cookiedomain()
1657
	{
1658
		if ($GLOBALS['egw_info']['server']['cookiedomain'])
1659
		{
1660
			// Admin set domain, eg. .domain.com to allow egw.domain.com and www.domain.com
1661
			self::$cookie_domain = $GLOBALS['egw_info']['server']['cookiedomain'];
1662
		}
1663
		else
1664
		{
1665
			// Use HTTP_X_FORWARDED_HOST if set, which is the case behind a none-transparent proxy
1666
			self::$cookie_domain = Header\Http::host();
1667
		}
1668
		// remove port from HTTP_HOST
1669
		$arr = null;
1670
		if (preg_match("/^(.*):(.*)$/",self::$cookie_domain,$arr))
1671
		{
1672
			self::$cookie_domain = $arr[1];
1673
		}
1674
		if (count(explode('.',self::$cookie_domain)) <= 1)
1675
		{
1676
			// setcookie dont likes domains without dots, leaving it empty, gets setcookie to fill the domain in
1677
			self::$cookie_domain = '';
1678
		}
1679
		if (!$GLOBALS['egw_info']['server']['cookiepath'] ||
1680
			!(self::$cookie_path = parse_url($GLOBALS['egw_info']['server']['webserver_url'],PHP_URL_PATH)))
1681
		{
1682
			self::$cookie_path = '/';
1683
		}
1684
1685
		session_set_cookie_params(0, self::$cookie_path, self::$cookie_domain,
1686
			// if called via HTTPS, only send cookie for https and only allow cookie access via HTTP (true)
1687
			empty($GLOBALS['egw_info']['server']['insecure_cookies']) && Header\Http::schema() === 'https', true);
1688
	}
1689
1690
	/**
1691
	 * Search the instance matching the request
1692
	 *
1693
	 * @param string $login on login $_POST['login'], $_SERVER['PHP_AUTH_USER'] or $_SERVER['REMOTE_USER']
1694
	 * @param string $domain_requested usually self::get_request('domain')
1695
	 * @param string &$default_domain usually $default_domain get's set eg. by sitemgr
1696
	 * @param string|array $server_names usually array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME'])
1697
	 * @param array $domains =null defaults to $GLOBALS['egw_domain'] from the header
1698
	 * @return string $GLOBALS['egw_info']['user']['domain'] set with the domain/instance to use
1699
	 */
1700
	public static function search_instance($login,$domain_requested,&$default_domain,$server_names,array $domains=null)
1701
	{
1702
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$login','$domain_requested',".array2string($default_domain).".".array2string($server_names).".".array2string($domains).")");
1703
1704
		if (is_null($domains)) $domains = $GLOBALS['egw_domain'];
1705
1706
		if (!isset($default_domain) || !isset($domains[$default_domain]))	// allow to overwrite the default domain
1707
		{
1708
			foreach((array)$server_names as $server_name)
1709
			{
1710
				list($server_name) = explode(':', $server_name);	// remove port from HTTP_HOST
1711
				if(isset($domains[$server_name]))
1712
				{
1713
					$default_domain = $server_name;
1714
					break;
1715
				}
1716
				else
1717
				{
1718
					$parts = explode('.', $server_name);
1719
					array_shift($parts);
1720
					$domain_part = implode('.', $parts);
1721
					if(isset($domains[$domain_part]))
1722
					{
1723
						$default_domain = $domain_part;
1724
						break;
1725
					}
1726
					else
1727
					{
1728
						reset($domains);
1729
						$default_domain = key($domains);
1730
					}
1731
					unset($domain_part);
1732
				}
1733
			}
1734
		}
1735
		if (isset($login))	// on login
1736
		{
1737
			if (strpos($login,'@') === false || count($domains) == 1)
1738
			{
1739
				$login .= '@' . (isset($_POST['logindomain']) ? $_POST['logindomain'] : $default_domain);
1740
			}
1741
			$parts = explode('@',$login);
1742
			$domain = array_pop($parts);
1743
			$GLOBALS['login'] = $login;
1744
		}
1745
		else	// on "normal" pageview
1746
		{
1747
			$domain = $domain_requested;
1748
		}
1749
		if (!isset($domains[$domain]))
1750
		{
1751
			$domain = $default_domain;
1752
		}
1753
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."() default_domain=".array2string($default_domain).', login='.array2string($login)." returning ".array2string($domain));
1754
1755
		return $domain;
1756
	}
1757
1758
	/**
1759
	 * Set action logged in access-log
1760
	 *
1761
	 * Non-ascii chars in $action get transliterate to ascii, as our session_action column allows only ascii.
1762
	 *
1763
	 * @param string $action
1764
	 */
1765
	public function set_action($action)
1766
	{
1767
		if (preg_match('/[^\x20-\x7f]/', $action))
1768
		{
1769
			$action = Translation::to_ascii($action);
1770
		}
1771
		$this->action = $action;
1772
	}
1773
1774
	/**
1775
	 * Ignore dla logging for a maximum of 900s = 15min
1776
	 */
1777
	const MAX_IGNORE_DLA_LOG = 900;
1778
1779
	/**
1780
	 * Update session_action and session_dla (session last used time)
1781
	 *
1782
	 * @param boolean $update_access_log =false false: dont update egw_access_log table, but set $this->action
1783
	 * @return string action as written to egw_access_log.session_action
1784
	 */
1785
	private function update_dla($update_access_log=false)
1786
	{
1787
		// This way XML-RPC users aren't always listed as xmlrpc.php
1788
		if (isset($_GET['menuaction']))
1789
		{
1790
			list(, $action) = explode('.ajax_exec.template.', $_GET['menuaction']);
1791
1792
			if (empty($action)) $action = $_GET['menuaction'];
1793
		}
1794
		else
1795
		{
1796
			$action = $_SERVER['PHP_SELF'];
1797
			// remove EGroupware path, if not installed in webroot
1798
			$egw_path = $GLOBALS['egw_info']['server']['webserver_url'];
1799
			if ($egw_path[0] != '/') $egw_path = parse_url($egw_path,PHP_URL_PATH);
1800
			if ($action == '/Microsoft-Server-ActiveSync')
1801
			{
1802
				$action .= '?Cmd='.$_GET['Cmd'].'&DeviceId='.$_GET['DeviceId'];
1803
			}
1804
			elseif ($egw_path)
1805
			{
1806
				list(,$action) = explode($egw_path,$action,2);
1807
			}
1808
		}
1809
		$this->set_action($action);
1810
1811
		// update dla in access-log table, if we have an access-log row (non-anonymous session)
1812
		if ($this->sessionid_access_log && $update_access_log &&
1813
			// ignore updates (session creation is written) of *dav, avatar and thumbnail, due to possible high volume of updates
1814
			(!preg_match('#^(/webdav|/groupdav|/api/avatar|/api/thumbnail)\.php#', $this->action) ||
1815
				(time() - $_SESSION[self::EGW_SESSION_VAR]['session_logged_dla']) > self::MAX_IGNORE_DLA_LOG) &&
1816
			is_object($GLOBALS['egw']->db))
1817
		{
1818
			$_SESSION[self::EGW_SESSION_VAR]['session_logged_dla'] = time();
1819
1820
			$GLOBALS['egw']->db->update(self::ACCESS_LOG_TABLE,array(
1821
				'session_dla' => time(),
1822
				'session_action' => $this->action,
1823
			) + ($this->action === '/logout.php' ? array() : array(
1824
				'lo' => null,	// just in case it was (automatic) timed out before
1825
			)),array(
1826
				'sessionid' => $this->sessionid_access_log,
1827
			),__LINE__,__FILE__);
1828
		}
1829
1830
		$_SESSION[self::EGW_SESSION_VAR]['session_dla'] = time();
1831
		$_SESSION[self::EGW_SESSION_VAR]['session_action'] = $this->action;
1832
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__.'() _SESSION['.self::EGW_SESSION_VAR.']='.array2string($_SESSION[self::EGW_SESSION_VAR]));
1833
1834
		return $this->action;
1835
	}
1836
1837
	/**
1838
	 * Update notification_heartbeat time of session
1839
	 */
1840
	private function update_notification_heartbeat()
1841
	{
1842
		// update dla in access-log table, if we have an access-log row (non-anonymous session)
1843
		if ($this->sessionid_access_log)
1844
		{
1845
			$GLOBALS['egw']->db->update(self::ACCESS_LOG_TABLE,array(
1846
				'notification_heartbeat' => time(),
1847
			),array(
1848
				'sessionid' => $this->sessionid_access_log,
1849
				'lo IS NULL',
1850
			),__LINE__,__FILE__);
1851
		}
1852
	}
1853
1854
	/**
1855
	 * Read the diverse repositories / init classes with data from the just loged in user
1856
	 *
1857
	 * @return array used to assign to $GLOBALS['egw_info']['user']
1858
	 */
1859
	public function read_repositories()
1860
	{
1861
		$GLOBALS['egw']->acl->__construct($this->account_id);
1862
		$GLOBALS['egw']->preferences->__construct($this->account_id);
1863
		$GLOBALS['egw']->applications->__construct($this->account_id);
1864
1865
		$user = $GLOBALS['egw']->accounts->read($this->account_id);
1866
		// set homedirectory from auth_ldap or auth_ads, to be able to use it in vfs
1867
		if (!isset($user['homedirectory']))
1868
		{
1869
			// authentication happens in login.php, which does NOT yet create egw-object in session
1870
			// --> need to store homedirectory in session
1871
			if(isset($GLOBALS['auto_create_acct']['homedirectory']))
1872
			{
1873
				Cache::setSession(__CLASS__, 'homedirectory',
1874
					$user['homedirectory'] = $GLOBALS['auto_create_acct']['homedirectory']);
1875
			}
1876
			else
1877
			{
1878
				$user['homedirectory'] = Cache::getSession(__CLASS__, 'homedirectory');
1879
			}
1880
		}
1881
		$user['preferences'] = $GLOBALS['egw']->preferences->read_repository();
1882
		if (is_object($GLOBALS['egw']->datetime))
1883
		{
1884
			$GLOBALS['egw']->datetime->__construct();		// to set tz_offset from the now read prefs
1885
		}
1886
		$user['apps']        = $GLOBALS['egw']->applications->read_repository();
1887
		$user['domain']      = $this->account_domain;
1888
		$user['sessionid']   = $this->sessionid;
1889
		$user['kp3']         = $this->kp3;
1890
		$user['session_ip']  = $this->getuser_ip();
1891
		$user['session_lid'] = $this->account_lid.'@'.$this->account_domain;
1892
		$user['account_id']  = $this->account_id;
1893
		$user['account_lid'] = $this->account_lid;
1894
		$user['userid']      = $this->account_lid;
1895
		$user['passwd']      = $this->passwd;
1896
1897
		return $user;
1898
	}
1899
1900
	/**
1901
	 * Splits a login-name into account_lid and eGW-domain/-instance
1902
	 *
1903
	 * @param string $login login-name (ie. user@default)
1904
	 * @param string &$account_lid returned account_lid (ie. user)
1905
	 * @param string &$domain returned domain (ie. domain)
1906
	 */
1907
	private function split_login_domain($login,&$account_lid,&$domain)
1908
	{
1909
		$parts = explode('@',$login);
1910
1911
		//conference - for strings like [email protected]@default ,
1912
		//allows that user have a login that is his e-mail. (viniciuscb)
1913
		if (count($parts) > 1)
1914
		{
1915
			$probable_domain = array_pop($parts);
1916
			//Last part of login string, when separated by @, is a domain name
1917
			if (in_array($probable_domain,$this->egw_domains))
1918
			{
1919
				$got_login = true;
1920
				$domain = $probable_domain;
1921
				$account_lid = implode('@',$parts);
1922
			}
1923
		}
1924
1925
		if (!$got_login)
1926
		{
1927
			$domain = $GLOBALS['egw_info']['server']['default_domain'];
1928
			$account_lid = $login;
1929
		}
1930
	}
1931
1932
	/**
1933
	 * Create a hash from user and pw
1934
	 *
1935
	 * Can be used to check setup config user/password inside egroupware:
1936
	 *
1937
	 * if (Api\Session::user_pw_hash($user,$pw) === $GLOBALS['egw_info']['server']['config_hash'])
1938
	 *
1939
	 * @param string $user username
1940
	 * @param string $password password or md5 hash of password if $allow_password_md5
1941
	 * @param boolean $allow_password_md5 =false can password alread be an md5 hash
1942
	 * @return string
1943
	 */
1944
	static function user_pw_hash($user,$password,$allow_password_md5=false)
1945
	{
1946
		$password_md5 = $allow_password_md5 && preg_match('/^[a-f0-9]{32}$/',$password) ? $password : md5($password);
1947
1948
		$hash = sha1(strtolower($user).$password_md5);
1949
1950
		return $hash;
1951
	}
1952
1953
	/**
1954
	 * Initialise the used session handler
1955
	 *
1956
	 * @return boolean true if we have a session, false otherwise
1957
	 * @throws \ErrorException if there is no PHP session support
1958
	 */
1959
	public static function init_handler()
1960
	{
1961
		switch(session_status())
1962
		{
1963
			case PHP_SESSION_DISABLED:
1964
				throw new \ErrorException('EGroupware requires PHP session extension!');
1965
			case PHP_SESSION_NONE:
1966
				ini_set('session.use_cookies',0);	// disable the automatic use of cookies, as it uses the path / by default
1967
				session_name(self::EGW_SESSION_NAME);
1968
				if (($sessionid = self::get_sessionid()))
1969
				{
1970
					session_id($sessionid);
1971
					self::cache_control();
1972
					$ok = session_start();
1973
					self::decrypt();
1974
					if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."() sessionid=$sessionid, _SESSION[".self::EGW_SESSION_VAR.']='.array2string($_SESSION[self::EGW_SESSION_VAR]));
1975
					return $ok;
1976
				}
1977
				break;
1978
			case PHP_SESSION_ACTIVE:
1979
				return true;	// session created by MServer
1980
		}
1981
		if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."() no active session!");
1982
1983
		return false;
1984
	}
1985
1986
	/**
1987
	 * Controling caching and expires header
1988
	 *
1989
	 * Headers are send based on given parameters or $GLOBALS['egw_info']['flags']['nocachecontrol']:
1990
	 * - not set of false --> no caching (default)
1991
	 * - true --> private caching by browser (no expires header)
1992
	 * - "public" or integer --> public caching with given cache_expire in minutes or php.ini default session_cache_expire
1993
	 *
1994
	 * @param int $expire =null expiration time in seconds, default $GLOBALS['egw_info']['flags']['nocachecontrol'] or php.ini session.cache_expire
1995
	 * @param int $private =null allows to set private caching with given expiration time, by setting it to true
1996
	 */
1997
	public static function cache_control($expire=null, $private=null)
1998
	{
1999
		if (is_null($expire) && isset($GLOBALS['egw_info']['flags']['nocachecontrol']) && is_int($GLOBALS['egw_info']['flags']['nocachecontrol']))
2000
		{
2001
			$expire = $GLOBALS['egw_info']['flags']['nocachecontrol'];
2002
		}
2003
		// session not yet started: use PHP session_cache_limiter() and session_cache_expires() functions
2004
		if (!isset($_SESSION))
2005
		{
2006
			// controling caching and expires header
2007
			if(!isset($expire) && (!isset($GLOBALS['egw_info']['flags']['nocachecontrol']) ||
2008
				!$GLOBALS['egw_info']['flags']['nocachecontrol']))
2009
			{
2010
				session_cache_limiter('nocache');
2011
			}
2012
			elseif (isset($expire) || $GLOBALS['egw_info']['flags']['nocachecontrol'] === 'public' || is_int($GLOBALS['egw_info']['flags']['nocachecontrol']))
2013
			{
2014
				// allow public caching: proxys, cdns, ...
2015
				if (isset($expire))
2016
				{
2017
					session_cache_expire((int)ceil($expire/60));	// in minutes
2018
				}
2019
				session_cache_limiter($private ? 'private' : 'public');
2020
			}
2021
			else
2022
			{
2023
				// allow caching by browser
2024
				session_cache_limiter('private_no_expire');
2025
			}
2026
		}
2027
		// session already started
2028
		if (isset($_SESSION))
2029
		{
2030
			if ($expire && (session_cache_limiter() !== ($expire===true?'private_no_expire':'public') ||
2031
				is_int($expire) && $expire/60 !== session_cache_expire()))
2032
			{
2033
				$file = $line = null;
2034
				if (headers_sent($file, $line))
2035
				{
2036
					error_log(__METHOD__."($expire) called, but header already sent in $file: $line");
2037
					return;
2038
				}
2039
				if($expire === true)	// same behavior as session_cache_limiter('private_no_expire')
2040
				{
2041
					header('Cache-Control: private, max-age='.(60*session_cache_expire()));
2042
					header_remove('Expires');
2043
				}
2044
				elseif ($private)
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...
2045
				{
2046
					header('Cache-Control: private, max-age='.$expire);
2047
					header('Expires: ' . gmdate('D, d M Y H:i:s', time()+$expire) . ' GMT');
2048
				}
2049
				else
2050
				{
2051
					header('Cache-Control: public, max-age='.$expire);
2052
					header('Expires: ' . gmdate('D, d M Y H:i:s', time()+$expire) . ' GMT');
2053
				}
2054
				// remove Pragma header, might be set by old header
2055
				if (function_exists('header_remove'))	// PHP 5.3+
2056
				{
2057
					header_remove('Pragma');
2058
				}
2059
				else
2060
				{
2061
					header('Pragma:');
2062
				}
2063
			}
2064
		}
2065
	}
2066
2067
	/**
2068
	 * Get a session list (of the current instance)
2069
	 *
2070
	 * @param int $start
2071
	 * @param string $sort ='DESC' ASC or DESC
2072
	 * @param string $order ='session_dla' session_lid, session_id, session_started, session_logintime, session_action, or (default) session_dla
2073
	 * @param boolean $all_no_sort =False skip sorting and limiting to maxmatchs if set to true
2074
	 * @param array $filter =array() extra filter for sessions
2075
	 * @return array with sessions (values for keys as in $sort)
2076
	 */
2077
	public static function session_list($start,$sort='DESC',$order='session_dla',$all_no_sort=False,array $filter=array())
2078
	{
2079
		$sessions = array();
2080
		if (!preg_match('/^[a-z0-9_ ,]+$/i',$order_by=$order.' '.$sort) || $order_by == ' ')
2081
		{
2082
			$order_by = 'session_dla DESC';
2083
		}
2084
		$filter['lo'] = null;
2085
		$filter[] = 'account_id>0';
2086
		$filter[] = 'session_dla > '.(int)(time() - $GLOBALS['egw_info']['server']['sessions_timeout']);
2087
		$filter[] = '(notification_heartbeat IS NULL OR notification_heartbeat > '.self::heartbeat_limit().')';
2088
		foreach($GLOBALS['egw']->db->select(self::ACCESS_LOG_TABLE, '*', $filter, __LINE__, __FILE__,
2089
			$all_no_sort ? false : $start, 'ORDER BY '.$order_by) as $row)
2090
		{
2091
			$sessions[$row['sessionid']] = $row;
2092
		}
2093
		return $sessions;
2094
	}
2095
2096
	/**
2097
	 * Query number of sessions (not more then once every N secs)
2098
	 *
2099
	 * @param array $filter =array() extra filter for sessions
2100
	 * @return int number of active sessions
2101
	 */
2102
	public static function session_count(array $filter=array())
2103
	{
2104
		$filter['lo'] = null;
2105
		$filter[] = 'account_id>0';
2106
		$filter[] = 'session_dla > '.(int)(time() - $GLOBALS['egw_info']['server']['sessions_timeout']);
2107
		$filter[] = '(notification_heartbeat IS NULL OR notification_heartbeat > '.self::heartbeat_limit().')';
2108
		return $GLOBALS['egw']->db->select(self::ACCESS_LOG_TABLE, 'COUNT(*)', $filter, __LINE__, __FILE__)->fetchColumn();
2109
	}
2110
2111
	/**
2112
	 * Get limit / latest time of heartbeat for session to be active
2113
	 *
2114
	 * @return int TS in server-time
2115
	 */
2116
	public static function heartbeat_limit()
2117
	{
2118
		static $limit=null;
2119
2120
		if (is_null($limit))
2121
		{
2122
			$config = Config::read('notifications');
2123
			if (!($popup_poll_interval  = $config['popup_poll_interval']))
2124
			{
2125
				$popup_poll_interval = 60;
2126
			}
2127
			$limit = (int)(time() - $popup_poll_interval-10);	// 10s grace periode
2128
		}
2129
		return $limit;
2130
	}
2131
2132
	/**
2133
	 * Check if given user can be reached via notifications
2134
	 *
2135
	 * Checks if notifications callback checked in not more then heartbeat_limit() seconds ago
2136
	 *
2137
	 * @param int $account_id
2138
	 * @param int number of active sessions of given user with notifications running
2139
	 */
2140
	public static function notifications_active($account_id)
2141
	{
2142
		return $GLOBALS['egw']->db->select(self::ACCESS_LOG_TABLE, 'COUNT(*)', array(
2143
				'lo' => null,
2144
				'session_dla > '.(int)(time() - $GLOBALS['egw_info']['server']['sessions_timeout']),
2145
				'account_id' => $account_id,
2146
				'notification_heartbeat > '.self::heartbeat_limit(),
2147
		), __LINE__, __FILE__)->fetchColumn();
2148
	}
2149
}
2150