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