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