Merged changes from stable branch up to 2.3.23.
[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.") . "<b><br><br>";
153
154   echo _("If you are not an authorized user, please contact your system administrator to obtain an account to enable you to use the system.");
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         Simple brute force attack detection is performed before connection to company database is open. Therefore access counters have to be stored in file.
189         Login attempts counter is created for every new user IP, which partialy prevent DOS attacks.
190 */
191 function write_login_filelog($login, $result)
192 {
193         global $login_faillog, $SysPrefs, $path_to_root;
194
195         $user = $_SESSION["wa_current_user"]->user;
196
197         $ip = $_SERVER['REMOTE_ADDR'];
198
199         if (!isset($login_faillog[$user][$ip]) || $result) // init or reset on successfull login
200                 $login_faillog[$user] = array($ip => 0, 'last' => '');
201
202         if (!$result)
203         {
204                 if ($login_faillog[$user][$ip] < @$SysPrefs->login_max_attempts) {
205
206                         $login_faillog[$user][$ip]++;
207                 } else {
208                         $login_faillog[$user][$ip] = 0; // comment out to restart counter only after successfull login.
209                         error_log(sprintf(_("Brute force attack on account '%s' detected. Access for non-logged users temporarily blocked."     ), $login));
210                 }
211                 $login_faillog[$user]['last'] = time();
212         }
213
214         $msg = "<?php\n";
215         $msg .= "/*\n";
216         $msg .= "Login attempts info.\n";
217         $msg .= "*/\n";
218         $msg .= "\$login_faillog = " .var_export($login_faillog, true). ";\n";
219
220         $filename = $path_to_root."/tmp/faillog.php";
221
222         if ((!file_exists($filename) && is_writable($path_to_root.'/tmp')) || is_writable($filename))
223         {
224                 file_put_contents($filename, $msg);
225         }
226 }
227
228 //----------------------------------------------------------------------------------------
229
230 function check_page_security($page_security)
231 {
232         global $SysPrefs;
233         
234         $msg = '';
235         
236         if (!$_SESSION["wa_current_user"]->check_user_access())
237         {
238                 // notification after upgrade from pre-2.2 version
239                 $msg = $_SESSION["wa_current_user"]->old_db ?
240                          _("Security settings have not been defined for your user account.")
241                                 . "<br>" . _("Please contact your system administrator.")       
242                         : _("Please remove \$security_groups and \$security_headings arrays from config.php file!");
243         } elseif (!$SysPrefs->db_ok && !$_SESSION["wa_current_user"]->can_access('SA_SOFTWAREUPGRADE')) 
244         {
245                 $msg = _('Access to application has been blocked until database upgrade is completed by system administrator.');
246         }
247         
248         if ($msg){
249                 display_error($msg);
250                 end_page(@$_REQUEST['popup']);
251                 kill_login();
252                 exit;
253         }
254
255         if (!$_SESSION["wa_current_user"]->can_access_page($page_security))
256         {
257
258                 echo "<center><br><br><br><b>";
259                 echo _("The security settings on your account do not permit you to access this function");
260                 echo "</b>";
261                 echo "<br><br><br><br></center>";
262                 end_page(@$_REQUEST['popup']);
263                 exit;
264         }
265         if (!$SysPrefs->db_ok 
266                 && !in_array($page_security, array('SA_SOFTWAREUPGRADE', 'SA_OPEN', 'SA_BACKUP')))
267         {
268                 display_error(_('System is blocked after source upgrade until database is updated on System/Software Upgrade page'));
269                 end_page();
270                 exit;
271         }
272
273 }
274 /*
275         Helper function for setting page security level depeding on 
276         GET start variable and/or some value stored in session variable.
277         Before the call $page_security should be set to default page_security value.
278 */
279 function set_page_security($value=null, $trans = array(), $gtrans = array())
280 {
281         global $page_security;
282
283         // first check is this is not start page call
284         foreach($gtrans as $key => $area)
285                 if (isset($_GET[$key])) {
286                         $page_security = $area;
287                         return;
288                 }
289
290         // then check session value
291         if (isset($trans[$value])) {
292                 $page_security = $trans[$value];
293                 return;
294         }
295 }
296
297 //-----------------------------------------------------------------------------
298 //      Removing magic quotes from nested arrays/variables
299 //
300 function strip_quotes($data)
301 {
302         if(get_magic_quotes_gpc()) {
303                 if(is_array($data)) {
304                         foreach($data as $k => $v) {
305                                 $data[$k] = strip_quotes($data[$k]);
306                         }
307                 } else
308                         return stripslashes($data);
309         }
310         return $data;
311 }
312
313 /*
314         htmlspecialchars does not support certain encodings.
315         ISO-8859-2 fortunately has the same special characters positions as 
316         ISO-8859-1, so fix is easy. If any other unsupported encoding is used,
317         add workaround here.
318 */
319 function html_specials_encode($str)
320 {
321         return htmlspecialchars($str, ENT_QUOTES, $_SESSION['language']->encoding=='iso-8859-2' ?
322                  'ISO-8859-1' : $_SESSION['language']->encoding);
323 }
324
325 function html_cleanup(&$parms)
326 {
327         foreach($parms as $name => $value) {
328                 if (is_array($value))
329                         html_cleanup($parms[$name]);
330                 else
331                         $parms[$name] = html_specials_encode($value);
332         }
333         reset($parms); // needed for direct key() usage later throughout the sources
334 }
335
336 //============================================================================
337 //
338 //
339 function login_timeout()
340 {
341         // skip timeout on logout page
342         if ($_SESSION["wa_current_user"]->logged) {
343                 $tout = $_SESSION["wa_current_user"]->timeout;
344                 if ($tout && (time() > $_SESSION["wa_current_user"]->last_act + $tout))
345                 {
346                         $_SESSION["wa_current_user"]->logged = false;
347                 }
348                 $_SESSION["wa_current_user"]->last_act = time();
349         }
350 }
351 //============================================================================
352 if (!isset($path_to_root))
353 {
354         $path_to_root = ".";
355 }
356
357 // Prevent register_globals vulnerability
358 if (isset($_GET['path_to_root']) || isset($_POST['path_to_root']))
359         die("Restricted access");
360
361 include_once($path_to_root . "/includes/errors.inc");
362 // colect all error msgs
363 set_error_handler('error_handler' /*, errtypes */);
364
365 include_once($path_to_root . "/includes/current_user.inc");
366 include_once($path_to_root . "/frontaccounting.php");
367 include_once($path_to_root . "/admin/db/security_db.inc");
368 include_once($path_to_root . "/includes/lang/language.inc");
369 include_once($path_to_root . "/config_db.php");
370 include_once($path_to_root . "/includes/ajax.inc");
371 include_once($path_to_root . "/includes/ui/ui_msgs.inc");
372 include_once($path_to_root . "/includes/prefs/sysprefs.inc");
373
374 include_once($path_to_root . "/includes/hooks.inc");
375 //
376 // include all extensions hook files.
377 //
378 foreach ($installed_extensions as $ext)
379 {
380         if (file_exists($path_to_root.'/'.$ext['path'].'/hooks.php'))
381                 include_once($path_to_root.'/'.$ext['path'].'/hooks.php');
382 }
383 $Session_manager = new SessionManager();
384 $Session_manager->sessionStart('FA'.md5(dirname(__FILE__)));
385
386 $_SESSION['SysPrefs'] = new sys_prefs();
387
388 $SysPrefs = &$_SESSION['SysPrefs'];
389
390 //----------------------------------------------------------------------------------------
391 // set to reasonable values if not set in config file (pre-2.3.12 installations)
392
393 if ((!isset($SysPrefs->login_delay)) || ($SysPrefs->login_delay < 0))
394     $SysPrefs->login_delay = 10;
395
396 if ((!isset($SysPrefs->login_max_attempts)) || ($SysPrefs->login_max_attempts < 0))
397     $SysPrefs->login_max_attempts = 3; 
398
399 if ($SysPrefs->go_debug > 0)
400         error_reporting(-1);
401 else
402         error_reporting(E_USER_WARNING|E_USER_ERROR|E_USER_NOTICE);
403 ini_set("display_errors", "On");
404
405 if ($SysPrefs->error_logfile != '') {
406         ini_set("error_log", $SysPrefs->error_logfile);
407         ini_set("ignore_repeated_errors", "On");
408         ini_set("log_errors", "On");
409 }
410
411
412 /*
413         Uncomment the setting below when using FA on shared hosting
414         to avoid unexpeced session timeouts.
415         Make sure this directory exists and is writable!
416 */
417 // ini_set('session.save_path', dirname(__FILE__).'/../tmp/');
418
419 ini_set('session.gc_maxlifetime', 36000); // 10hrs
420
421 hook_session_start(@$_POST["company_login_name"]);
422
423 // this is to fix the "back-do-you-want-to-refresh" issue - thanx PHPFreaks
424 header("Cache-control: private");
425
426 get_text_init();
427
428 if ($SysPrefs->login_delay > 0)
429         @include_once($path_to_root . "/tmp/faillog.php");
430
431 // Page Initialisation
432 if (!isset($_SESSION['wa_current_user']) || !$_SESSION['wa_current_user']->logged_in()
433         || !isset($_SESSION['language']) || !method_exists($_SESSION['language'], 'set_language'))
434 {
435         $l = array_search_value($dflt_lang, $installed_languages,  'code');
436         $_SESSION['language'] = new language($l['name'], $l['code'], $l['encoding'],
437          (isset($l['rtl']) && $l['rtl'] === true) ? 'rtl' : 'ltr');
438 }
439
440 $_SESSION['language']->set_language($_SESSION['language']->code);
441
442
443 include_once($path_to_root . "/includes/access_levels.inc");
444 include_once($path_to_root . "/version.php");
445 include_once($path_to_root . "/includes/main.inc");
446 include_once($path_to_root . "/includes/app_entries.inc");
447
448 // Ajax communication object
449 $Ajax = new Ajax();
450
451 // js/php validation rules container
452 $Validate = array();
453 // bindings for editors
454 $Editors = array();
455 // page help. Currently help for function keys.
456 $Pagehelp = array();
457
458 $Refs = new references();
459
460 // intercept all output to destroy it in case of ajax call
461 register_shutdown_function('end_flush');
462 ob_start('output_html',0);
463
464 if (!isset($_SESSION["wa_current_user"]))
465         $_SESSION["wa_current_user"] = new current_user();
466
467 html_cleanup($_GET);
468 html_cleanup($_POST);
469 html_cleanup($_REQUEST);
470 html_cleanup($_SERVER);
471
472 // logout.php is the only page we should have always 
473 // accessable regardless of access level and current login status.
474 if (!defined('FA_LOGOUT_PHP_FILE')){
475
476         login_timeout();
477
478         if (!$_SESSION["wa_current_user"]->old_db)
479                 include_once($path_to_root . '/company/'.user_company().'/installed_extensions.php');
480
481         install_hooks();
482
483         if (!$_SESSION["wa_current_user"]->logged_in())
484         {
485       if (@$SysPrefs->allow_password_reset && !$SysPrefs->allow_demo_mode
486         && (isset($_GET['reset']) || isset($_POST['email_entry_field']))) {
487                   if (!isset($_POST["email_entry_field"])) {
488         include($path_to_root . "/access/password_reset.php");
489         exit();
490       }
491       else {
492         if (isset($_POST["company_login_nickname"]) && !isset($_POST["company_login_name"])) {
493           for ($i = 0; $i < count($db_connections); $i++) {
494             if ($db_connections[$i]["name"] == $_POST["company_login_nickname"]) {
495               $_POST["company_login_name"] = $i;
496               unset($_POST["company_login_nickname"]);
497               break 1; // cannot pass variables to break from PHP v5.4 onwards
498             }
499           }
500         }
501         $_succeed = isset($db_connections[$_POST["company_login_name"]]) &&
502           $_SESSION["wa_current_user"]->reset_password($_POST["company_login_name"],
503           $_POST["email_entry_field"]);
504         if ($_succeed)
505         {
506           password_reset_success();
507         }
508
509         password_reset_fail();
510       }
511     }
512                 // Show login screen
513                 if (!isset($_POST["user_name_entry_field"]) or $_POST["user_name_entry_field"] == "")
514                 {
515                         // strip ajax marker from uri, to force synchronous page reload
516                         $_SESSION['timeout'] = array( 'uri'=>preg_replace('/JsHttpRequest=(?:(\d+)-)?([^&]+)/s',
517                                         '', html_specials_encode($_SERVER['REQUEST_URI'])),
518                                 'post' => $_POST);
519
520                         include($path_to_root . "/access/login.php");
521                         if (in_ajax())
522                                 $Ajax->activate('_page_body');
523                         exit;
524                 } else {
525                         if (isset($_POST["company_login_nickname"]) && !isset($_POST["company_login_name"])) {
526                                 for ($i = 0; $i < count($db_connections); $i++) {
527                                         if ($db_connections[$i]["name"] == $_POST["company_login_nickname"]) {
528                                                 $_POST["company_login_name"] = $i;
529                                                 unset($_POST["company_login_nickname"]);
530                                                 break 1; // cannot pass variables to break from PHP v5.4 onwards
531                                         }
532                                 }
533                         }
534                         $succeed = isset($db_connections[$_POST["company_login_name"]]) &&
535                                 $_SESSION["wa_current_user"]->login($_POST["company_login_name"],
536                                 $_POST["user_name_entry_field"], $_POST["password"]);
537                         // select full vs fallback ui mode on login
538                         $_SESSION["wa_current_user"]->ui_mode = $_POST['ui_mode'];
539                         if (!$succeed)
540                         {
541                         // Incorrect password
542                                 login_fail();
543                         }
544                         elseif(isset($_SESSION['timeout']) && !$_SESSION['timeout']['post'])
545                         {
546                                 // in case of GET request redirect to avoid confirmation dialog 
547                                 // after return from menu option
548                                 header("HTTP/1.1 303 See Other");
549                                 header("Location: ".$_SESSION['timeout']['uri']);
550                                 exit();
551                         }
552                         $lang = &$_SESSION['language'];
553                         $lang->set_language($_SESSION['language']->code);
554                 }
555         } else
556         {
557                 set_global_connection();
558
559                 if (db_fixed())
560                         db_set_encoding($_SESSION['language']->encoding);
561
562                 $SysPrefs->refresh_company_prefs();
563         }
564         if (!isset($_SESSION["App"])) {
565                 $_SESSION["App"] = new front_accounting();
566                 $_SESSION["App"]->init();
567         }
568 }
569
570 // POST vars cleanup needed for direct reuse.
571 // We quote all values later with db_escape() before db update.
572 $_POST = strip_quotes($_POST);