Added support for user timezones and user browser timezone by default (incomplete) (#13626)

* Added support for user timezones and user browser timezone byt default

* Formatting fixes

* Use the timezone for alert log display

also added validation for the timezone because it's being used in SQL.

* Formatting fixes

* Added return type

* Formatting"

* Update the latency graphs to use the user timezone

* Simplify the web routes config

* Update phpstan to ignore type error

* Fixed up the phpstan config

* Reverse phpstan change

* Re-apply phpstan override

* Remove the option to unset the session timezone

* Formatting fix

* Update outge and event logs to use session timezone

* Fix route for the timezone control

* Made the timezone more dynamic

* Fix a logic error that was stopping the timezone from being set automatically on login

* Prevent getPref from being called twice

* again prevent getPref double call

* getPref double call

* Fixed typo made during merge

* Fixed merge error in phpstan-baseline.neon

* Change spaces to tabs in phpstan-baseline.neon

* Update error count

---------

Co-authored-by: Tony Murray <murraytony@gmail.com>
This commit is contained in:
eskyuu
2023-05-09 23:24:48 +08:00
committed by GitHub
parent 56e9fa3742
commit ce0734ff5d
19 changed files with 216 additions and 20 deletions

View File

@@ -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);

View File

@@ -0,0 +1,55 @@
<?php
/**
* TimezoneController.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*
* @copyright 2021 Steven Wilton
* @author Steven Wilton <swilton@fluentit.com.au>
*/
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');
}
}

View File

@@ -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();
}

View File

@@ -117,7 +117,7 @@ class EventlogController extends TableController
$output = "<span class='alert-status ";
$output .= $this->severityLabel($eventlog->severity);
$output .= " eventlog-status'></span><span style='display:inline;'>";
$output .= (new Carbon($eventlog->datetime))->format(Config::get('dateformat.compact'));
$output .= (new Carbon($eventlog->datetime))->setTimezone(session('timezone'))->format(Config::get('dateformat.compact'));
$output .= '</span>';
return $output;

View File

@@ -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 = "<span style='display:inline;'>";
$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 .= '</span>';
return $output;

View File

@@ -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);

View File

@@ -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',
];

View File

@@ -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();

View File

@@ -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',

View File

@@ -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 '</pre>';
try {
Rrd::graph($rrd_options);
Rrd::graph($rrd_options, $env);
} catch (\LibreNMS\Exceptions\RrdGraphException $e) {
echo "<p style='font-size: 16px; font-weight: bold;'>RRDTool Output</p>";
echo "<pre class='rrd-pre'>";
@@ -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()) {

View File

@@ -78,7 +78,7 @@ $pagetitle[] = 'Outages';
clear: 'fa fa-trash-o',
close: 'fa fa-close'
},
defaultDate: '<?php echo Carbon::now()->subMonth()->format(Config::get('dateformat.byminute', 'Y-m-d H:i')); ?>'
defaultDate: '<?php echo Carbon::now(session('timezone'))->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: '<?php echo Carbon::now()->format(Config::get('dateformat.byminute', 'Y-m-d H:i')); ?>'
defaultDate: '<?php echo Carbon::now(session('timezone'))->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('<?php echo Carbon::now()->format(Config::get('dateformat.byminute', 'Y-m-d H:i')); ?>');
$("#dtpickerto").data("DateTimePicker").maxDate('<?php echo Carbon::now(session('timezone'))->format(Config::get('dateformat.byminute', 'Y-m-d H:i')); ?>');
}
});

View File

@@ -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) {

View File

@@ -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' => "<button type='button' class='btn btn-alert-details command-alert-details' aria-label='Details' id='alert-details' data-alert_log_id='{$alert_log_id}'><i class='fa-solid fa-circle-info'></i></button>",
'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'],

View File

@@ -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

View File

@@ -76,7 +76,7 @@
});
var ajax_url = "{{ url('/ajax') }}";
</script>
<script src="{{ asset('js/librenms.js?ver=10272021') }}"></script>
<script src="{{ asset('js/librenms.js?ver=01112022') }}"></script>
<script type="text/javascript" src="{{ asset('js/overlib_mini.js') }}"></script>
<script type="text/javascript" src="{{ asset('js/flasher.min.js?ver=0.6.1') }}"></script>
<script type="text/javascript" src="{{ asset('js/toastr.min.js?ver=05072021') }}"></script>
@@ -89,6 +89,16 @@
document.documentElement.classList.remove('tw-dark')
}
</script>
@if(session('timezone_static') == null || ! session('timezone_static'))
<script>
$(document).ready(function() {
var tz = window.Intl.DateTimeFormat().resolvedOptions().timeZone;
if(tz !== '{{ session('timezone') }}') {
updateTimezone(tz, false);
}
});
</script>
@endif
@auth
<script src="{{ asset('js/register-service-worker.js') }}" defer></script>
@endauth

View File

@@ -86,6 +86,19 @@
</div>
@endif
<div class="form-group @if($errors->has('timezone')) has-error @endif">
<label for="timezone" class="control-label col-sm-3">{{ __('Timezone') }}</label>
<div class="col-sm-9">
<select id="timezone" name="timezone" class="form-control">
<option value="default">Browser Timezone</option>
@foreach(timezone_identifiers_list() as $tz)
<option value="{{ $tz }}" @if(old('timezone', $timezone) == $tz) selected @endif>{{ $tz }}</option>
@endforeach
</select>
<span class="help-block">{{ $errors->first('timezone') }}</span>
</div>
</div>
<script>
$("[type='checkbox']").bootstrapSwitch();
</script>

View File

@@ -18,7 +18,8 @@
<th data-column-id="realname" data-formatter="text">{{ __('Real Name') }}</th>
<th data-column-id="level" data-formatter="level" data-type="numeric">{{ __('Access') }}</th>
<th data-column-id="auth_type" data-visible="{{ $multiauth ? 'true' : 'false' }}">{{ __('auth.title') }}</th>
<th data-column-id="email" data-formatter="text">{{ __('Email') }}</th>
<th data-column-id="email">{{ __('Email') }}</th>
<th data-column-id="timezone">{{ __('Timezone') }}</th>
@if(\LibreNMS\Authentication\LegacyAuth::getType() == 'mysql')
<th data-column-id="enabled" data-formatter="enabled">{{ __('Enabled') }}</th>
@endif
@@ -38,6 +39,7 @@
<td>{{ $user->level }}</td>
<td>{{ $user->auth_type }}</td>
<td>{{ $user->email }}</td>
<td>{{ \App\Models\UserPref::getPref($user, 'timezone') ?: "Browser Timezone" }}</td>
@if(\LibreNMS\Authentication\LegacyAuth::getType() == 'mysql')
<td>{{ $user->enabled }}</td>
@endif

View File

@@ -92,6 +92,17 @@
<small>* {{ __('Translation not fully supported') }}</small>
</div>
</div>
<div class="form-group">
<label for="timezone" class="col-sm-4 control-label">{{ __('Timezone') }}</label>
<div class="col-sm-4">
<select class="form-control ajax-select" name="timezone" data-pref="timezone" data-previous="{{ $timezone }}">
<option value="default">Browser Timezone</option>
@foreach(timezone_identifiers_list() as $tz)
<option value="{{ $tz }}" @if($timezone == $tz) selected @endif>{{ $tz }}</option>
@endforeach
</select>
</div>
</div>
<div class="form-group">
<label for="notetodevice" class="col-sm-4 control-label">{{ __('Add schedule notes to devices notes') }}</label>
<div class="col-sm-4">
@@ -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');

View File

@@ -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');