Session   F
last analyzed

Complexity

Total Complexity 367

Size/Duplication

Total Lines 2114
Duplicated Lines 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 861
c 7
b 0
f 0
dl 0
loc 2114
rs 1.739
wmc 367

39 Methods

Rating   Name   Duplication   Size   Complexity  
A decrypt() 0 13 5
A log_session_usage() 0 18 6
A __wakeup() 0 7 3
A commit_session() 0 6 2
B init_crypt() 0 33 10
D __construct() 0 69 11
A encrypt() 0 19 5
A __destruct() 0 3 1
A getuser_ip() 0 11 3
A removeRememberMeTokenOnLogout() 0 5 3
C checkMultifactorAuth() 0 67 15
F create() 0 234 51
B log_access() 0 69 11
A register_session() 0 19 3
B egw_setcookie() 0 20 11
B destroy() 0 50 11
B skipPasswordAuth() 0 27 8
A rememberMeTokenLifetime() 0 25 5
A checkOpenIDconfigured() 0 29 4
B checkSetRememberMeToken() 0 25 9
C login_blocked() 0 88 12
C get_sessionid() 0 41 14
A get_request() 0 5 4
F link() 0 100 26
F verify() 0 162 44
A set_action() 0 7 2
A update_notification_heartbeat() 0 11 2
B set_cookiedomain() 0 34 8
B init_handler() 0 25 7
A user_pw_hash() 0 7 3
D cache_control() 0 65 23
A split_login_domain() 0 22 4
A notifications_active() 0 8 1
A heartbeat_limit() 0 14 3
A session_count() 0 7 1
D search_instance() 0 56 14
A session_list() 0 17 5
C update_dla() 0 50 13
A read_repositories() 0 39 4

How to fix   Complexity   

Complex Class

Complex classes like Session often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Session, and based on these observations, apply Extract Interface, too.

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
				if (session_status() !== PHP_SESSION_ACTIVE)	// gives warning including password
554
				{
555
					session_id($this->sessionid);
556
				}
557
			}
558
			else
559
			{
560
				self::cache_control();
561
				session_start();
562
				// set a new session-id, if not syncml (already done in Horde code and can NOT be changed)
563
				if (!$no_session && $GLOBALS['egw_info']['flags']['currentapp'] != 'syncml')
564
				{
565
					session_regenerate_id(true);
566
				}
567
				$this->sessionid = session_id();
568
			}
569
			$this->kp3       = Auth::randomstring(24);
570
571
			$GLOBALS['egw_info']['user'] = $this->read_repositories();
572
			if ($GLOBALS['egw']->accounts->is_expired($GLOBALS['egw_info']['user']))
573
			{
574
				$this->reason = 'account is expired';
575
				$this->cd_reason = self::CD_ACCOUNT_EXPIRED;
576
577
				if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) UNSUCCESSFULL ($this->reason)");
578
				return false;
579
			}
580
581
			Cache::setSession('phpgwapi', 'password', base64_encode($this->passwd));
582
583
			// if we have a second factor, check it before forced password change
584
			if ($check_2fa !== false)
585
			{
586
				try {
587
					$this->checkMultifactorAuth($check_2fa, $_COOKIE[self::REMEMBER_ME_COOKIE]);
588
				}
589
				catch(\Exception $e) {
590
					$this->cd_reason = $e->getCode();
591
					$this->reason = $e->getMessage();
592
					$this->log_access($this->reason, $login, $user_ip, 0);	// log unsuccessfull login
593
					if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check,$fail_on_forced_password_change,'$check_2fa') UNSUCCESSFULL ($this->reason)");
594
					return false;
595
				}
596
			}
597
598
			if ($fail_on_forced_password_change && Auth::check_password_change($this->reason) === false)
599
			{
600
				$this->cd_reason = self::CD_FORCE_PASSWORD_CHANGE;
601
				return false;
602
			}
603
604
			if ($GLOBALS['egw']->acl->check('anonymous',1,'phpgwapi'))
605
			{
606
				$this->session_flags = 'A';
607
			}
608
			else
609
			{
610
				$this->session_flags = 'N';
611
			}
612
613
			if (($hook_result = Hooks::process(array(
614
				'location'       => 'session_creation',
615
				'sessionid'      => $this->sessionid,
616
				'session_flags'  => $this->session_flags,
617
				'account_id'     => $this->account_id,
618
				'account_lid'    => $this->account_lid,
619
				'passwd'         => $this->passwd,
620
				'account_domain' => $this->account_domain,
621
				'user_ip'        => $user_ip,
622
			),'',true)))	// true = run hooks from all apps, not just the ones the current user has perms to run
623
			{
624
				foreach($hook_result as $reason)
625
				{
626
					if ($reason)	// called hook requests to deny the session
627
					{
628
						$this->reason = $this->cd_reason = $reason;
629
						$this->log_access($this->reason,$login,$user_ip,0);		// log unsuccessfull login
630
						if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) UNSUCCESSFULL ($this->reason)");
631
						return false;
632
					}
633
				}
634
			}
635
			$GLOBALS['egw']->db->transaction_begin();
636
			$this->register_session($this->login,$user_ip,$now,$this->session_flags);
637
			if ($this->session_flags != 'A')		// dont log anonymous sessions
638
			{
639
				$this->sessionid_access_log = $this->log_access($this->sessionid,$login,$user_ip,$this->account_id);
640
				// We do NOT log anonymous sessions to not block website and also to cope with
641
				// high rate anon endpoints might be called creating a bottleneck in the egw_accounts table.
642
				Cache::setSession('phpgwapi', 'account_previous_login', $GLOBALS['egw']->auth->previous_login);
643
				$GLOBALS['egw']->accounts->update_lastlogin($this->account_id,$user_ip);
644
			}
645
			$GLOBALS['egw']->db->transaction_commit();
646
647
			if ($GLOBALS['egw_info']['server']['usecookies'] && !$no_session)
648
			{
649
				self::egw_setcookie(self::EGW_SESSION_NAME,$this->sessionid);
650
				self::egw_setcookie('kp3',$this->kp3);
651
				self::egw_setcookie('domain',$this->account_domain);
652
			}
653
			if ($GLOBALS['egw_info']['server']['usecookies'] && !$no_session || isset($_COOKIE['last_loginid']))
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...
654
			{
655
				self::egw_setcookie('last_loginid', $this->account_lid ,$now+1209600); /* For 2 weeks */
656
				self::egw_setcookie('last_domain',$this->account_domain,$now+1209600);
657
			}
658
659
			// set new remember me token/cookie, if requested and necessary
660
			$expiration = null;
661
			if (($token = $this->checkSetRememberMeToken($remember_me, $_COOKIE[self::REMEMBER_ME_COOKIE], $expiration)))
662
			{
663
				self::egw_setcookie(self::REMEMBER_ME_COOKIE, $token, $expiration);
664
			}
665
666
			if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) successfull sessionid=$this->sessionid");
667
668
			// hook called once session is created
669
			Hooks::process(array(
670
				'location'       => 'session_created',
671
				'sessionid'      => $this->sessionid,
672
				'session_flags'  => $this->session_flags,
673
				'account_id'     => $this->account_id,
674
				'account_lid'    => $this->account_lid,
675
				'passwd'         => $this->passwd,
676
				'account_domain' => $this->account_domain,
677
				'user_ip'        => $user_ip,
678
				'session_type'   => Session\Type::get($_SERVER['REQUEST_URI'],
679
					$GLOBALS['egw_info']['flags']['current_app'],
680
					true),	// true return WebGUI instead of login, as we are logged in now
681
			),'',true);
682
683
			return $this->sessionid;
684
		}
685
		// catch all exceptions, as their (allways logged) trace (eg. on a database error) would contain the user password
686
		catch(Exception $e) {
687
			$this->reason = $this->cd_reason = is_a($e, Db\Exception::class) ?
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...
688
				// do not output specific database error, eg. invalid SQL statement
689
				lang('Database Error!') : $e->getMessage();
690
			error_log(__METHOD__."('$login', ".array2string(str_repeat('*', strlen($passwd))).
691
				", '$passwd_type', no_session=".array2string($no_session).
692
				", auth_check=".array2string($auth_check).
693
				", fail_on_forced_password_change=".array2string($fail_on_forced_password_change).
694
				") Exception ".$e->getMessage());
695
			return false;
696
		}
697
	}
698
699
	/**
700
	 * Check if password authentication is required or given token is sufficient
701
	 *
702
	 * Token is only checked for 'remember_me_token' === 'always', not for default of only for 2FA!
703
	 *
704
	 * Password auth is also required if 2FA is not disabled and either required or configured by user.
705
	 *
706
	 * @param string $token value of token
707
	 * @param int& $account_id =null account_id of token-owner to limit check on that user, on return account_id of token owner
708
	 * @return boolean false: if further auth check is required, true: if token is sufficient for authentication
709
	 */
710
	public function skipPasswordAuth($token, &$account_id=null)
711
	{
712
		// if token is empty or disabled --> password authentication required
713
		if (empty($token) || $GLOBALS['egw_info']['server']['remember_me_token'] !== 'always' ||
714
			!($client = $this->checkOpenIDconfigured()))
715
		{
716
			return false;
717
		}
718
719
		// check if token exists and is (still) valid
720
		$tokenRepo = new OpenID\Repositories\AccessTokenRepository();
721
		if (!($access_token = $tokenRepo->findToken($client, $account_id, 'PT1S', $token)))
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

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

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

835
				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...
836
				{
837
					return null;	// token still valid, no need to set it again
838
				}
839
			}
840
			$lifetime = $this->rememberMeTokenLifetime(is_string($remember_me) ? $remember_me : null);
0 ignored issues
show
introduced by
The condition is_string($remember_me) is always true.
Loading history...
841
			$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...
842
843
			$tokenFactory = new OpenID\Token();
844
			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

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

895
			$client->/** @scrutinizer ignore-call */ 
896
            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...
896
			$client->setName(lang('Remember me token'));
897
			$client->setAccessTokenTTL($this->rememberMeTokenLifetime());
898
			$client->setRefreshTokenTTL('P0S');	// no refresh token
899
			$client->setRedirectUri($GLOBALS['egw_info']['server']['webserver_url'].'/');
900
			$clients->persistNewClient($client);
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

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

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

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