Rerun of connect_db_mysqli.inc.
[fa-stable.git] / includes / session.inc
1 <?php
2 /**********************************************************************
3         Copyright (C) FrontAccounting, LLC.
4         Released under the terms of the GNU General Public License, GPL,
5         as published by the Free Software Foundation, either version 3
6         of the License, or (at your option) any later version.
7         This program is distributed in the hope that it will be useful,
8         but WITHOUT ANY WARRANTY; without even the implied warranty of
9         MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10         See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
11 ***********************************************************************/
12 define('VARLIB_PATH', $path_to_root.'/tmp');
13 define('VARLOG_PATH', $path_to_root.'/tmp');
14 define('SECURE_ONLY', true); // if you really need also http (unsecure) access allowed, you can set this to NULL
15
16 class SessionManager
17 {
18         function sessionStart($name, $limit = 0, $path = '/', $domain = null, $secure = null)
19         {
20                 // Set the cookie name
21                 session_name($name);
22
23                 // Set SSL level
24                 $https = isset($secure) ? $secure : (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
25
26                 // Set session cookie options
27                 if (version_compare(PHP_VERSION, '5.2', '<')) // avoid failure on older php versions
28                         session_set_cookie_params($limit, $path, $domain, $https);
29                 else
30                         session_set_cookie_params($limit, $path, $domain, $https, true);
31
32                 session_start();
33
34                 // Make sure the session hasn't expired, and destroy it if it has
35                 if ($this->validateSession())
36                 {
37                         // Check to see if the session is new or a hijacking attempt
38                         if(!$this->preventHijacking())
39                         {
40                                 // Reset session data and regenerate id
41                                 $_SESSION = array();
42                                 $_SESSION['IPaddress'] = $_SERVER['REMOTE_ADDR'];
43                                 $_SESSION['userAgent'] = @$_SERVER['HTTP_USER_AGENT'];
44                                 $this->regenerateSession();
45
46                         // Give a 5% chance of the session id changing on any request
47                         }
48                         elseif (rand(1, 100) <= 5)
49                         {
50                                 $this->regenerateSession();
51                         }
52                 }
53                 else
54                 {
55                         $_SESSION = array();
56                         session_destroy();
57                         session_start();
58                 }
59         }
60
61         function preventHijacking()
62         {
63                 if (!isset($_SESSION['IPaddress']) || !isset($_SESSION['userAgent']))
64                         return false;
65
66                 if ($_SESSION['IPaddress'] != $_SERVER['REMOTE_ADDR'])
67                         return false;
68
69                 if ( $_SESSION['userAgent'] != @$_SERVER['HTTP_USER_AGENT'])
70                         return false;
71
72                 return true;
73         }
74
75         function regenerateSession()
76         {
77                 // If this session is obsolete it means there already is a new id
78                 if (isset($_SESSION['OBSOLETE']) && ($_SESSION['OBSOLETE'] == true))
79                         return;
80
81                 // Set current session to expire in 10 seconds
82                 $_SESSION['OBSOLETE'] = true;
83                 $_SESSION['EXPIRES'] = time() + 10;
84
85                 // Create new session without destroying the old one
86                 session_regenerate_id();
87                 // Grab current session ID and close both sessions to allow other scripts to use them
88                 $newSession = session_id();
89                 session_write_close();
90                 // Set session ID to the new one, and start it back up again
91
92                 session_id($newSession);
93                 session_start();
94                 
95                 // Now we unset the obsolete and expiration values for the session we want to keep
96                 unset($_SESSION['OBSOLETE']);
97                 unset($_SESSION['EXPIRES']);
98         }
99
100         function validateSession()
101         {
102                 if (isset($_SESSION['OBSOLETE']) && !isset($_SESSION['EXPIRES']) )
103                         return false;
104
105                 if (isset($_SESSION['EXPIRES']) && $_SESSION['EXPIRES'] < time())
106                         return false;
107
108                 return true;
109         }
110 }
111
112 function output_html($text)
113 {
114         global $before_box, $Ajax, $messages;
115         // Fatal errors are not send to error_handler,
116         // so we must check the output
117         if ($text && preg_match('/\bFatal error(<.*?>)?:(.*)/i', $text, $m)) {
118                 $Ajax->aCommands = array();  // Don't update page via ajax on errors
119                 $text = preg_replace('/\bFatal error(<.*?>)?:(.*)/i','', $text);
120                 $messages[] = array(E_ERROR, $m[2], null, null);
121         }
122         $Ajax->run();
123         return  in_ajax() ? fmt_errors() : ($before_box.fmt_errors().$text);
124 }
125 //----------------------------------------------------------------------------------------
126
127 function kill_login()
128 {
129         session_unset();
130         session_destroy();
131 }
132 //----------------------------------------------------------------------------------------
133
134 function login_fail()
135 {
136         global $path_to_root;
137
138         header("HTTP/1.1 401 Authorization Required");
139         echo "<center><br><br><font size='5' color='red'><b>" . _("Incorrect Password") . "<b></font><br><br>";
140         echo "<b>" . _("The user and password combination is not valid for the system.") . "<b><br><br>";
141         echo _("If you are not an authorized user, please contact your system administrator to obtain an account to enable you to use the system.");
142         echo "<br><a href='$path_to_root/index.php'>" . _("Try again") . "</a>";
143         echo "</center>";
144         kill_login();
145         die();
146 }
147
148 function password_reset_fail()
149 {
150         global $path_to_root;
151         
152         echo "<center><br><br><font size='5' color='red'><b>" . _("Incorrect Email") . "<b></font><br><br>";
153         echo "<b>" . _("The email address does not exist in the system, or is used by more than one user.") . "<b><br><br>";
154
155         echo _("Plase try again or contact your system administrator to obtain new password.");
156         echo "<br><a href='$path_to_root/index.php?reset=1'>" . _("Try again") . "</a>";
157         echo "</center>";
158
159         kill_login();
160         die();
161 }
162
163 function password_reset_success()
164 {
165         global $path_to_root;
166
167         echo "<center><br><br><font size='5' color='green'><b>" . _("New password sent") . "<b></font><br><br>";
168         echo "<b>" . _("A new password has been sent to your mailbox.") . "<b><br><br>";
169
170         echo "<br><a href='$path_to_root/index.php'>" . _("Login here") . "</a>";
171         echo "</center>";
172         
173         kill_login();
174         die();
175 }
176
177 function check_faillog()
178 {
179         global $SysPrefs, $login_faillog;
180
181         $user = $_SESSION["wa_current_user"]->user;
182
183         $_SESSION["wa_current_user"]->login_attempt++;
184         if (@$SysPrefs->login_delay && (@$login_faillog[$user][$_SERVER['REMOTE_ADDR']] >= @$SysPrefs->login_max_attempts) && (time() < $login_faillog[$user]['last'] + $SysPrefs->login_delay))
185                 return true;
186
187         return false;
188 }
189
190 /*
191         Ensure file is re-read on next request if php caching is active
192 */
193 function cache_invalidate($filename)
194 {
195         if (function_exists('opcache_invalidate'))      // OpCode extension
196                 opcache_invalidate($filename);
197 }
198
199 /*
200         Simple brute force attack detection is performed before connection to company database is open. Therefore access counters have to be stored in file.
201         Login attempts counter is created for every new user IP, which partialy prevent DOS attacks.
202 */
203 function write_login_filelog($login, $result)
204 {
205         global $login_faillog, $SysPrefs, $path_to_root;
206
207         $user = $_SESSION["wa_current_user"]->user;
208
209         $ip = $_SERVER['REMOTE_ADDR'];
210
211         if (!isset($login_faillog[$user][$ip]) || $result) // init or reset on successfull login
212                 $login_faillog[$user] = array($ip => 0, 'last' => '');
213
214         if (!$result)
215         {
216                 if ($login_faillog[$user][$ip] < @$SysPrefs->login_max_attempts) {
217
218                         $login_faillog[$user][$ip]++;
219                 } else {
220                         $login_faillog[$user][$ip] = 0; // comment out to restart counter only after successfull login.
221                         error_log(sprintf(_("Brute force attack on account '%s' detected. Access for non-logged users temporarily blocked."     ), $login));
222                 }
223                 $login_faillog[$user]['last'] = time();
224         }
225
226         $msg = "<?php\n";
227         $msg .= "/*\n";
228         $msg .= "Login attempts info.\n";
229         $msg .= "*/\n";
230         $msg .= "\$login_faillog = " .var_export($login_faillog, true). ";\n";
231
232         $filename = VARLIB_PATH."/faillog.php";
233
234         if ((!file_exists($filename) && is_writable(VARLIB_PATH)) || is_writable($filename))
235         {
236                 file_put_contents($filename, $msg);
237                 cache_invalidate($filename);
238         }
239 }
240
241 //----------------------------------------------------------------------------------------
242
243 function check_page_security($page_security)
244 {
245         global $SysPrefs;
246         
247         $msg = '';
248         
249         if (!$_SESSION["wa_current_user"]->check_user_access())
250         {
251                 // notification after upgrade from pre-2.2 version
252                 $msg = $_SESSION["wa_current_user"]->old_db ?
253                          _("Security settings have not been defined for your user account.")
254                                 . "<br>" . _("Please contact your system administrator.")       
255                         : _("Please remove \$security_groups and \$security_headings arrays from config.php file!");
256         } elseif (!$SysPrefs->db_ok && !$_SESSION["wa_current_user"]->can_access('SA_SOFTWAREUPGRADE')) 
257         {
258                 $msg = _('Access to application has been blocked until database upgrade is completed by system administrator.');
259         }
260         
261         if ($msg){
262                 display_error($msg);
263                 end_page(@$_REQUEST['popup']);
264                 kill_login();
265                 exit;
266         }
267
268         if (!$_SESSION["wa_current_user"]->can_access_page($page_security))
269         {
270
271                 echo "<center><br><br><br><b>";
272                 echo _("The security settings on your account do not permit you to access this function");
273                 echo "</b>";
274                 echo "<br><br><br><br></center>";
275                 end_page(@$_REQUEST['popup']);
276                 exit;
277         }
278         if (!$SysPrefs->db_ok 
279                 && !in_array($page_security, array('SA_SOFTWAREUPGRADE', 'SA_OPEN', 'SA_BACKUP')))
280         {
281                 display_error(_('System is blocked after source upgrade until database is updated on System/Software Upgrade page'));
282                 end_page();
283                 exit;
284         }
285
286 }
287 /*
288         Helper function for setting page security level depeding on 
289         GET start variable and/or some value stored in session variable.
290         Before the call $page_security should be set to default page_security value.
291 */
292 function set_page_security($value=null, $trans = array(), $gtrans = array())
293 {
294         global $page_security;
295
296         // first check is this is not start page call
297         foreach($gtrans as $key => $area)
298                 if (isset($_GET[$key])) {
299                         $page_security = $area;
300                         return;
301                 }
302
303         // then check session value
304         if (isset($trans[$value])) {
305                 $page_security = $trans[$value];
306                 return;
307         }
308 }
309
310 //-----------------------------------------------------------------------------
311 //      Removing magic quotes from nested arrays/variables
312 //
313 function strip_quotes($data)
314 {
315         if(version_compare(phpversion(), '5.4', '<') && get_magic_quotes_gpc()) {
316                 if(is_array($data)) {
317                         foreach($data as $k => $v) {
318                                 $data[$k] = strip_quotes($data[$k]);
319                         }
320                 } else
321                         return stripslashes($data);
322         }
323         return $data;
324 }
325
326 /*
327         htmlspecialchars does not support certain encodings.
328         ISO-8859-2 fortunately has the same special characters positions as 
329         ISO-8859-1, so fix is easy. If any other unsupported encoding is used,
330         add workaround here.
331 */
332 function html_specials_encode($str)
333 {
334         return htmlspecialchars($str, ENT_QUOTES, $_SESSION['language']->encoding=='iso-8859-2' ?
335                  'ISO-8859-1' : $_SESSION['language']->encoding);
336 }
337
338 function html_cleanup(&$parms)
339 {
340         foreach($parms as $name => $value) {
341                 if (is_array($value))
342                         html_cleanup($parms[$name]);
343                 else
344                         $parms[$name] = html_specials_encode($value);
345         }
346         reset($parms); // needed for direct key() usage later throughout the sources
347 }
348
349 //============================================================================
350 //
351 //
352 function login_timeout()
353 {
354         // skip timeout on logout page
355         if ($_SESSION["wa_current_user"]->logged) {
356                 $tout = $_SESSION["wa_current_user"]->timeout;
357                 if ($tout && (time() > $_SESSION["wa_current_user"]->last_act + $tout))
358                 {
359                         $_SESSION["wa_current_user"]->logged = false;
360                 }
361                 $_SESSION["wa_current_user"]->last_act = time();
362         }
363 }
364 //============================================================================
365 if (!isset($path_to_root))
366 {
367         $path_to_root = ".";
368 }
369
370 // Prevent register_globals vulnerability
371 if (isset($_GET['path_to_root']) || isset($_POST['path_to_root']))
372         die("Restricted access");
373
374 include_once($path_to_root . "/includes/errors.inc");
375 // colect all error msgs
376 set_error_handler('error_handler' /*, errtypes */);
377 set_exception_handler('exception_handler');
378
379 include_once($path_to_root . "/includes/current_user.inc");
380 include_once($path_to_root . "/frontaccounting.php");
381 include_once($path_to_root . "/admin/db/security_db.inc");
382 include_once($path_to_root . "/includes/lang/language.inc");
383 include_once($path_to_root . "/config_db.php");
384 include_once($path_to_root . "/includes/ajax.inc");
385 include_once($path_to_root . "/includes/ui/ui_msgs.inc");
386 include_once($path_to_root . "/includes/prefs/sysprefs.inc");
387
388 include_once($path_to_root . "/includes/hooks.inc");
389 //
390 // include all extensions hook files.
391 //
392 foreach ($installed_extensions as $ext)
393 {
394         if (file_exists($path_to_root.'/'.$ext['path'].'/hooks.php'))
395                 include_once($path_to_root.'/'.$ext['path'].'/hooks.php');
396 }
397
398 ini_set('session.gc_maxlifetime', 36000); // moved from below.
399
400 $Session_manager = new SessionManager();
401 $Session_manager->sessionStart('FA'.md5(dirname(__FILE__)), 0, '/', null, SECURE_ONLY);
402
403 $_SESSION['SysPrefs'] = new sys_prefs();
404
405 $SysPrefs = &$_SESSION['SysPrefs'];
406
407 //----------------------------------------------------------------------------------------
408 // set to reasonable values if not set in config file (pre-2.3.12 installations)
409
410 if ((!isset($SysPrefs->login_delay)) || ($SysPrefs->login_delay < 0))
411     $SysPrefs->login_delay = 10;
412
413 if ((!isset($SysPrefs->login_max_attempts)) || ($SysPrefs->login_max_attempts < 0))
414     $SysPrefs->login_max_attempts = 3; 
415
416 if ($SysPrefs->go_debug > 0)
417         $cur_error_level = -1;
418 else
419         $cur_error_level = E_USER_WARNING|E_USER_ERROR|E_USER_NOTICE;
420
421 error_reporting($cur_error_level);
422 ini_set("display_errors", "On");
423
424 if ($SysPrefs->error_logfile != '') {
425         ini_set("error_log", $SysPrefs->error_logfile);
426         ini_set("ignore_repeated_errors", "On");
427         ini_set("log_errors", "On");
428 }
429
430 /*
431         Uncomment the setting below when using FA on shared hosting
432         to avoid unexpeced session timeouts.
433         Make sure this directory exists and is writable!
434 */
435 // ini_set('session.save_path', VARLIB_PATH.'/');
436
437 // ini_set('session.gc_maxlifetime', 36000); // 10hrs - moved to before session_manager
438
439 hook_session_start(@$_POST["company_login_name"]);
440
441 // this is to fix the "back-do-you-want-to-refresh" issue - thanx PHPFreaks
442 header("Cache-control: private");
443
444 get_text_init();
445
446 if ($SysPrefs->login_delay > 0 && file_exists(VARLIB_PATH."/faillog.php"))
447         include_once(VARLIB_PATH."/faillog.php");
448
449 // Page Initialisation
450 if (isset($dflt_lang) && isset($installed_languages))
451 {
452         if (!isset($_SESSION['wa_current_user']) || !$_SESSION['wa_current_user']->logged_in()
453                 || !isset($_SESSION['language']) || !method_exists($_SESSION['language'], 'set_language'))
454         {
455                 $l = array_search_value($dflt_lang, $installed_languages,  'code');
456                 $_SESSION['language'] = new language($l['name'], $l['code'], $l['encoding'],
457                 (isset($l['rtl']) && $l['rtl'] === true) ? 'rtl' : 'ltr');
458         }
459
460         $_SESSION['language']->set_language($_SESSION['language']->code);
461 }
462
463 include_once($path_to_root . "/includes/access_levels.inc");
464 include_once($path_to_root . "/version.php");
465 include_once($path_to_root . "/includes/main.inc");
466 include_once($path_to_root . "/includes/app_entries.inc");
467
468 // Ajax communication object
469 $Ajax = new Ajax();
470
471 // js/php validation rules container
472 $Validate = array();
473 // bindings for editors
474 $Editors = array();
475 // page help. Currently help for function keys.
476 $Pagehelp = array();
477
478 $Refs = new references();
479
480 // intercept all output to destroy it in case of ajax call
481 register_shutdown_function('end_flush');
482 ob_start('output_html',0);
483
484 if (!isset($_SESSION["wa_current_user"]))
485         $_SESSION["wa_current_user"] = new current_user();
486
487 html_cleanup($_GET);
488 html_cleanup($_POST);
489 html_cleanup($_REQUEST);
490 html_cleanup($_SERVER);
491
492 // logout.php is the only page we should have always 
493 // accessable regardless of access level and current login status.
494 if (!defined('FA_LOGOUT_PHP_FILE')){
495
496         login_timeout();
497
498         if (!$_SESSION["wa_current_user"]->old_db && file_exists($path_to_root . '/company/'.user_company().'/installed_extensions.php'))
499                 include($path_to_root . '/company/'.user_company().'/installed_extensions.php');
500
501         install_hooks();
502
503         if (!$_SESSION["wa_current_user"]->logged_in())
504         {
505       if (@$SysPrefs->allow_password_reset && !$SysPrefs->allow_demo_mode
506         && (isset($_GET['reset']) || isset($_POST['email_entry_field']))) {
507                   if (!isset($_POST["email_entry_field"])) {
508         include($path_to_root . "/access/password_reset.php");
509         exit();
510       }
511       else {
512         if (isset($_POST["company_login_nickname"]) && !isset($_POST["company_login_name"])) {
513           for ($i = 0; $i < count($db_connections); $i++) {
514             if ($db_connections[$i]["name"] == $_POST["company_login_nickname"]) {
515               $_POST["company_login_name"] = $i;
516               unset($_POST["company_login_nickname"]);
517               break 1; // cannot pass variables to break from PHP v5.4 onwards
518             }
519           }
520         }
521         $_succeed = isset($db_connections[$_POST["company_login_name"]]) &&
522           $_SESSION["wa_current_user"]->reset_password($_POST["company_login_name"],
523           $_POST["email_entry_field"]);
524         if ($_succeed)
525         {
526           password_reset_success();
527         }
528
529         password_reset_fail();
530       }
531     }
532                 // Show login screen
533                 if (!isset($_POST["user_name_entry_field"]) or $_POST["user_name_entry_field"] == "")
534                 {
535                         // strip ajax marker from uri, to force synchronous page reload
536                         $_SESSION['timeout'] = array( 'uri'=>preg_replace('/JsHttpRequest=(?:(\d+)-)?([^&]+)/s',
537                                         '', html_specials_encode($_SERVER['REQUEST_URI'])),
538                                 'post' => $_POST);
539                 if (in_ajax())
540                         $Ajax->popup($path_to_root ."/access/timeout.php");
541                 else
542                         include($path_to_root . "/access/login.php");
543                         exit;
544                 } else {
545                         if (isset($_POST["company_login_nickname"]) && !isset($_POST["company_login_name"])) {
546                                 for ($i = 0; $i < count($db_connections); $i++) {
547                                         if ($db_connections[$i]["name"] == $_POST["company_login_nickname"]) {
548                                                 $_POST["company_login_name"] = $i;
549                                                 unset($_POST["company_login_nickname"]);
550                                                 break 1; // cannot pass variables to break from PHP v5.4 onwards
551                                         }
552                                 }
553                         }
554                         $succeed = isset($db_connections[$_POST["company_login_name"]]) &&
555                                 $_SESSION["wa_current_user"]->login($_POST["company_login_name"],
556                                 $_POST["user_name_entry_field"], $_POST["password"]);
557                         // select full vs fallback ui mode on login
558                         $_SESSION["wa_current_user"]->ui_mode = $_POST['ui_mode'];
559                         if (!$succeed)
560                         {
561                         // Incorrect password
562                                 if (isset($_SESSION['timeout'])) {
563                                         include($path_to_root . "/access/login.php");
564                                         exit;
565                                 } else
566                                         login_fail();
567                         }
568                         elseif(isset($_SESSION['timeout']) && !$_SESSION['timeout']['post'])
569                         {
570                                 // in case of GET request redirect to avoid confirmation dialog 
571                                 // after return from menu option
572                                 header("HTTP/1.1 307 Temporary Redirect");
573                                 header("Location: ".$_SESSION['timeout']['uri']);
574                                 exit();
575                         }
576                         $lang = &$_SESSION['language'];
577                         $lang->set_language($_SESSION['language']->code);
578                 }
579         } else
580         {
581                 set_global_connection();
582
583                 if (db_fixed())
584                         db_set_encoding($_SESSION['language']->encoding);
585
586                 $SysPrefs->refresh();
587         }
588         if (!isset($_SESSION["App"])) {
589                 $_SESSION["App"] = new front_accounting();
590                 $_SESSION["App"]->init();
591         }
592 }
593
594 // POST vars cleanup needed for direct reuse.
595 // We quote all values later with db_escape() before db update.
596 $_POST = strip_quotes($_POST);