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