diff --git a/LibreNMS/Data/Store/Rrd.php b/LibreNMS/Data/Store/Rrd.php index 14df167ba8..6613902f4f 100644 --- a/LibreNMS/Data/Store/Rrd.php +++ b/LibreNMS/Data/Store/Rrd.php @@ -561,9 +561,9 @@ class Rrd extends BaseDatastore * * @throws \LibreNMS\Exceptions\RrdGraphException */ - public function graph(string $options): string + public function graph(string $options, array $env = null): string { - $process = new Process([Config::get('rrdtool', 'rrdtool'), '-'], $this->rrd_dir); + $process = new Process([Config::get('rrdtool', 'rrdtool'), '-'], $this->rrd_dir, $env); $process->setTimeout(300); $process->setIdleTimeout(300); diff --git a/app/Http/Controllers/Ajax/TimezoneController.php b/app/Http/Controllers/Ajax/TimezoneController.php new file mode 100644 index 0000000000..25c2d0191f --- /dev/null +++ b/app/Http/Controllers/Ajax/TimezoneController.php @@ -0,0 +1,55 @@ +. + * + * @link https://www.librenms.org + * + * @copyright 2021 Steven Wilton + * @author Steven Wilton + */ + +namespace App\Http\Controllers\Ajax; + +use App\Http\Controllers\Controller; +use Illuminate\Http\Request; + +class TimezoneController extends Controller +{ + public function set(Request $request): string + { + session([ + 'timezone_static' => $request->boolean('static'), + ]); + + // laravel session + if ($request->timezone) { + // Only accept valid timezones + if (! in_array($request->timezone, timezone_identifiers_list())) { + return session('timezone'); + } + + session([ + 'timezone' => $request->timezone, + ]); + + return $request->timezone; + } + + return session('timezone'); + } +} diff --git a/app/Http/Controllers/Device/Tabs/LatencyController.php b/app/Http/Controllers/Device/Tabs/LatencyController.php index 2249d9c0ab..4fa5fbdcd3 100644 --- a/app/Http/Controllers/Device/Tabs/LatencyController.php +++ b/app/Http/Controllers/Device/Tabs/LatencyController.php @@ -58,10 +58,13 @@ class LatencyController implements DeviceTab public function data(Device $device): array { - $from = Request::get('dtpickerfrom', Carbon::now()->subDays(2)->format(Config::get('dateformat.byminute'))); - $to = Request::get('dtpickerto', Carbon::now()->format(Config::get('dateformat.byminute'))); + $from = Request::get('dtpickerfrom', Carbon::now(session('timezone'))->subDays(2)->format(Config::get('dateformat.byminute'))); + $to = Request::get('dtpickerto', Carbon::now(session('timezone'))->format(Config::get('dateformat.byminute'))); - $perf = $this->fetchPerfData($device, $from, $to); + $dbfrom = Carbon::createFromFormat(Config::get('dateformat.byminute'), $from)->setTimezone(date_default_timezone_get())->format(Config::get('dateformat.byminute')); + $dbto = Carbon::createFromFormat(Config::get('dateformat.byminute'), $to)->setTimezone(date_default_timezone_get())->format(Config::get('dateformat.byminute')); + + $perf = $this->fetchPerfData($device, $dbfrom, $dbto); $duration = $perf && $perf->isNotEmpty() ? abs(strtotime($perf->first()->date) - strtotime($perf->last()->date)) * 1000 @@ -91,7 +94,7 @@ class LatencyController implements DeviceTab return DB::table('device_perf') ->where('device_id', $device->device_id) ->whereBetween('timestamp', [$from, $to]) - ->select(DB::raw("DATE_FORMAT(timestamp, '%Y-%m-%d %H:%i') date,xmt,rcv,loss,min,max,avg")) + ->selectRaw("DATE_FORMAT(IFNULL(CONVERT_TZ(timestamp, @@global.time_zone, ?), timestamp), '%Y-%m-%d %H:%i') date,xmt,rcv,loss,min,max,avg", [session('timezone')]) ->get(); } diff --git a/app/Http/Controllers/Table/EventlogController.php b/app/Http/Controllers/Table/EventlogController.php index 50b0c807eb..264ee7260f 100644 --- a/app/Http/Controllers/Table/EventlogController.php +++ b/app/Http/Controllers/Table/EventlogController.php @@ -117,7 +117,7 @@ class EventlogController extends TableController $output = ""; - $output .= (new Carbon($eventlog->datetime))->format(Config::get('dateformat.compact')); + $output .= (new Carbon($eventlog->datetime))->setTimezone(session('timezone'))->format(Config::get('dateformat.compact')); $output .= ''; return $output; diff --git a/app/Http/Controllers/Table/OutagesController.php b/app/Http/Controllers/Table/OutagesController.php index e46eceda0b..b12997c241 100644 --- a/app/Http/Controllers/Table/OutagesController.php +++ b/app/Http/Controllers/Table/OutagesController.php @@ -64,10 +64,10 @@ class OutagesController extends TableController return DeviceOutage::hasAccess($request->user()) ->with('device') ->when($request->from, function ($query) use ($request) { - $query->where('going_down', '>=', strtotime($request->from)); + $query->where('going_down', '>=', Carbon::parse($request->from, session('timezone'))->getTimestamp()); }) ->when($request->to, function ($query) use ($request) { - $query->where('going_down', '<=', strtotime($request->to)); + $query->where('going_down', '<=', Carbon::parse($request->to, session('timezone'))->getTimestamp()); }); } @@ -113,7 +113,7 @@ class OutagesController extends TableController } $output = ""; - $output .= Carbon::createFromTimestamp($timestamp)->format(Config::get('dateformat.compact')); // Convert epoch to local time + $output .= Carbon::createFromTimestamp($timestamp, session('timezone'))->format(Config::get('dateformat.compact')); // Convert epoch to local time $output .= ''; return $output; diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 427d9bfe0b..ace4bfe76d 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -80,6 +80,7 @@ class UserController extends Controller 'user' => $tmp_user, 'dashboard' => null, 'dashboards' => Dashboard::allAvailable($tmp_user)->get(), + 'timezone' => 'default', ]); } @@ -100,6 +101,7 @@ class UserController extends Controller $user->setPassword($request->new_password); $user->auth_id = (string) LegacyAuth::get()->getUserid($user->username) ?: $user->user_id; $this->updateDashboard($user, $request->get('dashboard')); + $this->updateTimezone($user, $request->get('timezone')); if ($user->save()) { $flasher->addSuccess(__('User :username created', ['username' => $user->username])); @@ -143,6 +145,7 @@ class UserController extends Controller 'user' => $user, 'dashboard' => UserPref::getPref($user, 'dashboard'), 'dashboards' => Dashboard::allAvailable($user)->get(), + 'timezone' => UserPref::getPref($user, 'timezone') ?: 'default', ]; if (Config::get('twofactor')) { @@ -186,6 +189,14 @@ class UserController extends Controller $flasher->addSuccess(__('Updated dashboard for :username', ['username' => $user->username])); } + if ($request->has('timezone') && $this->updateTimezone($user, $request->get('timezone'))) { + if ($request->get('timezone') != 'default') { + $flasher->addSuccess(__('Updated timezone for :username', ['username' => $user->username])); + } else { + $flasher->addSuccess(__('Cleared timezone for :username', ['username' => $user->username])); + } + } + if ($user->save()) { $flasher->addSuccess(__('User :username updated', ['username' => $user->username])); @@ -233,6 +244,35 @@ class UserController extends Controller return false; } + /** + * @param User $user + * @param string $timezone + * @return bool + */ + protected function updateTimezone(User $user, $timezone) + { + $existing = UserPref::getPref($user, 'timezone'); + if ($timezone != 'default') { + if (! in_array($timezone, timezone_identifiers_list())) { + return false; + } + + if ($timezone != $existing) { + UserPref::setPref($user, 'timezone', $timezone); + + return true; + } + } else { + if ($existing != '') { + UserPref::forgetPref($user, 'timezone'); + + return true; + } + } + + return false; + } + public function authlog() { $this->authorize('manage', User::class); diff --git a/app/Http/Controllers/UserPreferencesController.php b/app/Http/Controllers/UserPreferencesController.php index 0bb015ac03..e77eaf0b9e 100644 --- a/app/Http/Controllers/UserPreferencesController.php +++ b/app/Http/Controllers/UserPreferencesController.php @@ -73,6 +73,7 @@ class UserPreferencesController extends Controller 'site_style' => UserPref::getPref($user, 'site_style'), 'site_style_default' => $styles[$default_style] ?? $default_style, 'site_styles' => $styles, + 'timezone' => UserPref::getPref($user, 'timezone'), 'hide_dashboard_editor' => UserPref::getPref($user, 'hide_dashboard_editor') ?? 0, ]; @@ -110,6 +111,10 @@ class UserPreferencesController extends Controller 'required', Rule::in(array_merge(['default'], array_keys($this->getValidStyles()))), ], + 'timezone' => [ + 'required', + Rule::in(array_merge(['default'], timezone_identifiers_list())), + ], 'hide_dashboard_editor' => 'required|integer', ]; diff --git a/app/Providers/LegacyUserProvider.php b/app/Providers/LegacyUserProvider.php index bf060ba4ca..11f58aa5d8 100644 --- a/app/Providers/LegacyUserProvider.php +++ b/app/Providers/LegacyUserProvider.php @@ -26,6 +26,7 @@ namespace App\Providers; use App\Models\User; +use App\Models\UserPref; use DB; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\UserProvider; @@ -130,6 +131,13 @@ class LegacyUserProvider implements UserProvider throw new AuthenticationException(); } + if ($tz = UserPref::getPref($user, 'timezone')) { + session([ + 'timezone' => $tz, + 'timezone_static' => true, + ]); + } + return true; } catch (AuthenticationException $ae) { $auth_message = $ae->getMessage(); diff --git a/html/js/librenms.js b/html/js/librenms.js index a7accc248f..01e16c9bc8 100644 --- a/html/js/librenms.js +++ b/html/js/librenms.js @@ -72,6 +72,22 @@ function submitCustomRange(frmdata) { return true; } +function updateTimezone(tz, static) +{ + $.post(ajax_url + '/set_timezone', + { + timezone: tz, + static: static + }, + function(data) { + if(data === tz) { + location.reload(); + } + }, + 'text' + ); +} + function updateResolution(refresh) { $.post(ajax_url + '/set_resolution', diff --git a/includes/html/graphs/graph.inc.php b/includes/html/graphs/graph.inc.php index 03c3358e01..49caed7d55 100644 --- a/includes/html/graphs/graph.inc.php +++ b/includes/html/graphs/graph.inc.php @@ -33,6 +33,11 @@ $nototal = ! $graph_params->visible('total'); $nodetails = ! $graph_params->visible('details'); $noagg = ! $graph_params->visible('aggregate'); $rrd_options = ''; +$env = []; + +if (session('timezone')) { + $env['TZ'] = session('timezone'); +} require Config::get('install_dir') . "/includes/html/graphs/$type/auth.inc.php"; @@ -62,7 +67,7 @@ if (! empty($command_only)) { echo escapeshellcmd('rrdtool ' . Rrd::buildCommand('graph', Config::get('temp_dir') . '/' . strgen(), $rrd_options)); echo ''; try { - Rrd::graph($rrd_options); + Rrd::graph($rrd_options, $env); } catch (\LibreNMS\Exceptions\RrdGraphException $e) { echo "

RRDTool Output

"; echo "
";
@@ -82,7 +87,7 @@ if (empty($rrd_options)) {
 
 // Generating the graph!
 try {
-    $image_data = Rrd::graph($rrd_options);
+    $image_data = Rrd::graph($rrd_options, $env);
 
     // output the graph
     if (\LibreNMS\Util\Debug::isEnabled()) {
diff --git a/includes/html/pages/outages.inc.php b/includes/html/pages/outages.inc.php
index 1f9d8d7b5a..f238437915 100644
--- a/includes/html/pages/outages.inc.php
+++ b/includes/html/pages/outages.inc.php
@@ -78,7 +78,7 @@ $pagetitle[] = 'Outages';
                 clear: 'fa fa-trash-o',
                 close: 'fa fa-close'
             },
-            defaultDate: 'subMonth()->format(Config::get('dateformat.byminute', 'Y-m-d H:i')); ?>'
+            defaultDate: 'subMonth()->format(Config::get('dateformat.byminute', 'Y-m-d H:i')); ?>'
         });
         $("#dtpickerfrom").on("dp.change", function (e) {
             $("#dtpickerto").data("DateTimePicker").minDate(e.date);
@@ -95,7 +95,7 @@ $pagetitle[] = 'Outages';
                 clear: 'fa fa-trash-o',
                 close: 'fa fa-close'
             },
-            defaultDate: 'format(Config::get('dateformat.byminute', 'Y-m-d H:i')); ?>'
+            defaultDate: 'format(Config::get('dateformat.byminute', 'Y-m-d H:i')); ?>'
         });
         $("#dtpickerto").on("dp.change", function (e) {
             $("#dtpickerfrom").data("DateTimePicker").maxDate(e.date);
@@ -106,7 +106,7 @@ $pagetitle[] = 'Outages';
         if ($("#dtpickerto").val() != "") {
             $("#dtpickerfrom").data("DateTimePicker").maxDate($("#dtpickerto").val());
         } else {
-            $("#dtpickerto").data("DateTimePicker").maxDate('format(Config::get('dateformat.byminute', 'Y-m-d H:i')); ?>');
+            $("#dtpickerto").data("DateTimePicker").maxDate('format(Config::get('dateformat.byminute', 'Y-m-d H:i')); ?>');
         }
     });
 
diff --git a/includes/html/table/alertlog.inc.php b/includes/html/table/alertlog.inc.php
index 7c8359cd0d..b74252ebba 100644
--- a/includes/html/table/alertlog.inc.php
+++ b/includes/html/table/alertlog.inc.php
@@ -82,7 +82,12 @@ if ($rowCount != -1) {
     $sql .= " LIMIT $limit_low,$limit_high";
 }
 
-$sql = "SELECT R.severity, D.device_id,name AS alert,rule_id, state,time_logged,DATE_FORMAT(time_logged, '" . \LibreNMS\Config::get('dateformat.mysql.compact') . "') as humandate,details $sql";
+if (session('timezone')) {
+    $sql = "SELECT R.severity, D.device_id,name AS alert,rule_id,state,time_logged,DATE_FORMAT(IFNULL(CONVERT_TZ(time_logged, @@global.time_zone, ?),time_logged), '" . \LibreNMS\Config::get('dateformat.mysql.compact') . "') as humandate,details $sql";
+    $param = array_merge([session('timezone')], $param);
+} else {
+    $sql = "SELECT R.severity, D.device_id,name AS alert,rule_id,state,time_logged,DATE_FORMAT(time_logged, '" . \LibreNMS\Config::get('dateformat.mysql.compact') . "') as humandate,details $sql";
+}
 
 $rulei = 0;
 foreach (dbFetchRows($sql, $param) as $alertlog) {
diff --git a/includes/html/table/alerts.inc.php b/includes/html/table/alerts.inc.php
index dbf94f1c8f..b9a40b46f9 100644
--- a/includes/html/table/alerts.inc.php
+++ b/includes/html/table/alerts.inc.php
@@ -113,7 +113,12 @@ if ($rowCount != -1) {
     $sql .= " LIMIT $limit_low,$limit_high";
 }
 
-$sql = "SELECT `alerts`.*, `devices`.`hostname`, `devices`.`sysName`, `devices`.`display`, `devices`.`os`, `devices`.`hardware`, `locations`.`location`, `alert_rules`.`rule`, `alert_rules`.`name`, `alert_rules`.`severity` $sql";
+if (session('timezone')) {
+    $sql = "SELECT `alerts`.*, IFNULL(CONVERT_TZ(`alerts`.`timestamp`, @@global.time_zone, ?),`alerts`.`timestamp`) AS timestamp_display, `devices`.`hostname`, `devices`.`sysName`, `devices`.`display`, `devices`.`os`, `devices`.`hardware`, `locations`.`location`, `alert_rules`.`rule`, `alert_rules`.`name`, `alert_rules`.`severity` $sql";
+    $param = array_merge([session('timezone')], $param);
+} else {
+    $sql = "SELECT `alerts`.*, `alerts`.`timestamp` AS timestamp_display, `devices`.`hostname`, `devices`.`sysName`, `devices`.`display`, `devices`.`os`, `devices`.`hardware`, `locations`.`location`, `alert_rules`.`rule`, `alert_rules`.`name`, `alert_rules`.`severity` $sql";
+}
 
 $rulei = 0;
 $format = $vars['format'];
@@ -190,7 +195,7 @@ foreach (dbFetchRows($sql, $param) as $alert) {
         'verbose_details' => "",
         'hostname' => $hostname,
         'location' => generate_link($alert['location'], ['page' => 'devices', 'location' => $alert['location']]),
-        'timestamp' => ($alert['timestamp'] ? $alert['timestamp'] : 'N/A'),
+        'timestamp' => ($alert['timestamp_display'] ? $alert['timestamp_display'] : 'N/A'),
         'severity' => $severity_ico,
         'state' => $alert['state'],
         'alert_id' => $alert['id'],
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 0b4ace5751..cf09cd450e 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -359,3 +359,8 @@ parameters:
 			message: "#^Parameter \\#1 \\$new_location of method App\\\\Models\\\\Device\\:\\:setLocation\\(\\) expects App\\\\Models\\\\Location\\|string, null given\\.$#"
 			count: 1
 			path: tests/Unit/LocationTest.php
+
+		-
+			message: "#^Parameter \\#1 \\$user of static method App\\\\Models\\\\UserPref\\:\\:getPref\\(\\) expects App\\\\Models\\\\User, Illuminate\\\\Contracts\\\\Auth\\\\Authenticatable given\\.$#"
+			count: 1
+			path: app/Providers/LegacyUserProvider.php
diff --git a/resources/views/layouts/librenmsv1.blade.php b/resources/views/layouts/librenmsv1.blade.php
index 6d824f9f44..255325a7ab 100644
--- a/resources/views/layouts/librenmsv1.blade.php
+++ b/resources/views/layouts/librenmsv1.blade.php
@@ -76,7 +76,7 @@
         });
         var ajax_url = "{{ url('/ajax') }}";
     
-    
+    
     
     
     
@@ -89,6 +89,16 @@
             document.documentElement.classList.remove('tw-dark')
         }
     
+    @if(session('timezone_static') == null || ! session('timezone_static'))
+    
+    @endif
     @auth
         
     @endauth
diff --git a/resources/views/user/form.blade.php b/resources/views/user/form.blade.php
index c3aa052e3f..f62fea8025 100644
--- a/resources/views/user/form.blade.php
+++ b/resources/views/user/form.blade.php
@@ -86,6 +86,19 @@
 
 @endif
 
+
+ +
+ + {{ $errors->first('timezone') }} +
+
+ diff --git a/resources/views/user/index.blade.php b/resources/views/user/index.blade.php index 9ea3fd51f0..9b49f9d6f9 100644 --- a/resources/views/user/index.blade.php +++ b/resources/views/user/index.blade.php @@ -18,7 +18,8 @@ {{ __('Real Name') }} {{ __('Access') }} {{ __('auth.title') }} - {{ __('Email') }} + {{ __('Email') }} + {{ __('Timezone') }} @if(\LibreNMS\Authentication\LegacyAuth::getType() == 'mysql') {{ __('Enabled') }} @endif @@ -38,6 +39,7 @@ {{ $user->level }} {{ $user->auth_type }} {{ $user->email }} + {{ \App\Models\UserPref::getPref($user, 'timezone') ?: "Browser Timezone" }} @if(\LibreNMS\Authentication\LegacyAuth::getType() == 'mysql') {{ $user->enabled }} @endif diff --git a/resources/views/user/preferences.blade.php b/resources/views/user/preferences.blade.php index 49607a2e33..9b6cf08921 100644 --- a/resources/views/user/preferences.blade.php +++ b/resources/views/user/preferences.blade.php @@ -92,6 +92,17 @@ * {{ __('Translation not fully supported') }} +
+ +
+ +
+
@@ -238,6 +249,14 @@ if (pref === 'site_style') { location.reload(); } + if (pref === 'timezone') { + if(value === 'default') { + var tz = window.Intl.DateTimeFormat().resolvedOptions().timeZone; + updateTimezone(tz, false); + } else { + updateTimezone(value, true); + } + } $this.data('previous', value); $this.closest('.form-group').addClass('has-success'); diff --git a/routes/web.php b/routes/web.php index b3475b6a1c..dfb451d71f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -230,6 +230,11 @@ Route::middleware(['auth'])->group(function () { Route::permanentRedirect('demo', '/'); }); +// routes that don't need authentication +Route::group(['prefix' => 'ajax', 'namespace' => 'Ajax'], function () { + Route::post('set_timezone', 'TimezoneController@set'); +}); + // installation routes Route::prefix('install')->namespace('Install')->group(function () { Route::get('/', 'InstallationController@redirectToFirst')->name('install');