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