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