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