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