System: Configuration: History - refactor using MVC components.

When \Deciso\OPNcentral\Central exists, there might be multiple providers to select from, so we can easily reuse the same component in both versions.

closes https://github.com/opnsense/core/issues/6828
This commit is contained in:
Ad Schellevis 2023-09-09 13:05:59 +02:00
parent d9fcc0bbbf
commit f75ec9688a
7 changed files with 532 additions and 330 deletions

4
plist
View File

@ -269,10 +269,12 @@
/usr/local/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/SessionController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/VoucherController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/forms/dialogZone.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/Api/BackupController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/Api/FirmwareController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/Api/MenuController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/Api/ServiceController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/Api/SystemController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/BackupController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/FirmwareController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/HaltController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/IndexController.php
@ -729,6 +731,7 @@
/usr/local/opnsense/mvc/app/views/OPNsense/CaptivePortal/clients.volt
/usr/local/opnsense/mvc/app/views/OPNsense/CaptivePortal/index.volt
/usr/local/opnsense/mvc/app/views/OPNsense/CaptivePortal/vouchers.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Core/backup_history.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Core/firmware.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Core/halt.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Core/license.volt
@ -1983,7 +1986,6 @@
/usr/local/www/csrf.inc
/usr/local/www/diag_authentication.php
/usr/local/www/diag_backup.php
/usr/local/www/diag_confbak.php
/usr/local/www/diag_defaults.php
/usr/local/www/diag_logs_settings.php
/usr/local/www/fbegin.inc

View File

@ -0,0 +1,214 @@
<?php
/*
* Copyright (C) 2023 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Core\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\ACL;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
/**
* Class BackupController
* @package OPNsense\Core\Api
*/
class BackupController extends ApiControllerBase
{
/**
* when the user-config-readonly privilege is set, raise an error
*/
private function throwReadOnly()
{
if ((new ACL())->hasPrivilege($this->getUserName(), 'user-config-readonly')) {
throw new UserException(
sprintf("User %s denied for write access (user-config-readonly set)", $this->getUserName())
);
}
}
/**
* return available providers and their backup locations
* @return array
*/
private function providers()
{
$result = [];
$result['this'] = ['description' => gettext('This Firewall'), 'dirname' => '/conf/backup'];
if (class_exists('\Deciso\OPNcentral\Central')) {
$central = new \Deciso\OPNcentral\Central();
$central->setUserScope($this->getUserName());
$ctrHosts = [];
foreach ($central->hosts->host->getNodes() as $itemid => $item) {
$ctrHosts[$itemid] = ['description' => $item['description']];
}
foreach (glob('/conf/remote.backups/*') as $filename) {
$dirname = basename($filename);
if (isset($ctrHosts[$dirname])) {
$result[$dirname] = $ctrHosts[$dirname];
$result[$dirname]['dirname'] = $filename;
}
}
}
return $result;
}
/**
* list available providers
* @return array
*/
public function providersAction()
{
return ['items' => $this->providers()];
}
/**
* list available backups for selected host
*/
public function backupsAction($host)
{
$result = ['items' => []];
$providers = $this->providers();
if (!empty($providers[$host])) {
foreach (glob($providers[$host]['dirname'] . "/config-*.xml") as $filename) {
$xmlNode = @simplexml_load_file($filename, "SimpleXMLElement", LIBXML_NOERROR | LIBXML_ERR_NONE);
if (isset($xmlNode->revision)) {
$cfg_item = [
'time' => (string)$xmlNode->revision->time,
'time_iso' => date('c', (string)$xmlNode->revision->time),
'description' => (string)$xmlNode->revision->description,
'username' => (string)$xmlNode->revision->username,
'filesize' => filesize($filename),
'id' => basename($filename)
] ;
$result['items'][] = $cfg_item;
}
}
// sort newest first
usort($result['items'], function ($item1, $item2) {
return $item1['time'] < $item2['time'];
});
}
return $result;
}
/**
* diff two backups for selected host
*/
public function diffAction($host, $backup1, $backup2)
{
$result = ['items' => []];
$providers = $this->providers();
if (!empty($providers[$host])) {
$bckfilename1 = null;
$bckfilename2 = null;
foreach (glob($providers[$host]['dirname'] . "/config-*.xml") as $filename) {
$bckfilename = basename($filename);
if ($backup1 == $bckfilename) {
$bckfilename1 = $filename;
} elseif ($backup2 == $bckfilename) {
$bckfilename2 = $filename;
}
}
if (!empty($bckfilename1) && !empty($bckfilename2)) {
$diff = [];
exec("/usr/bin/diff -u " . escapeshellarg($bckfilename1) . " " . escapeshellarg($bckfilename2), $diff);
if (!empty($diff)) {
foreach ($diff as $line) {
$result['items'][] = htmlspecialchars($line, ENT_QUOTES | ENT_HTML401);
}
}
}
}
return $result;
}
/**
* delete local backup
*/
public function deleteBackupAction($backup)
{
$this->throwReadOnly();
foreach (glob("/conf/backup/config-*.xml") as $filename) {
$bckfilename = basename($filename);
if ($backup === $bckfilename) {
@unlink($filename);
return ['status' => 'deleted'];
}
}
return ['status' => 'not_found'];
}
/**
* revert to local backup from history
*/
public function revertBackupAction($backup)
{
$this->throwReadOnly();
foreach (glob("/conf/backup/config-*.xml") as $filename) {
$bckfilename = basename($filename);
if ($backup === $bckfilename) {
$cnf = Config::getInstance();
$cnf->backup();
$cnf->restoreBackup($filename);
$cnf->save();
return ['status' => 'reverted'];
}
}
return ['status' => 'not_found'];
}
/**
* download specified backup
*/
public function downloadAction($host, $backup)
{
$providers = $this->providers();
if (!empty($providers[$host])) {
foreach (glob($providers[$host]['dirname'] . "/config-*.xml") as $filename) {
if ($backup == basename($filename)) {
$payload = @simplexml_load_file($filename);
$hostname = '';
if ($payload !== false && isset($payload->system) && isset($payload->system->hostname)) {
$hostname = $payload->system->hostname . "." .$payload->system->domain;
}
$target_filename = urlencode('config-' . $hostname . '-' . explode('/config-', $filename, 2)[1]);
$this->response->setContentType('application/octet-stream');
$this->response->setRawHeader("Content-Disposition: attachment; filename=" . $target_filename);
$this->response->setRawHeader("Content-length: " . filesize($filename));
$this->response->setRawHeader("Pragma: no-cache");
$this->response->setRawHeader("Expires: 0");
ob_clean();
flush();
readfile($filename);
break;
}
}
}
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* Copyright (C) 2023 Deciso B.V.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*/
namespace OPNsense\Core;
/**
* Class BackupController
* @package OPNsense\Core
*/
class BackupController extends \OPNsense\Base\IndexController
{
public function historyAction($selected_host=null)
{
$this->view->selected_host = $selected_host;
$this->view->pick('OPNsense/Core/backup_history');
}
}

View File

@ -80,7 +80,8 @@
<page-diagnostics-configurationhistory>
<name>Diagnostics: Configuration History</name>
<patterns>
<pattern>diag_confbak.php*</pattern>
<pattern>ui/core/backup/history*</pattern>
<pattern>api/core/backup/*</pattern>
</patterns>
</page-diagnostics-configurationhistory>
<page-diagnostics-factorydefaults>

View File

@ -34,9 +34,7 @@
<Configuration cssClass="fa fa-history fa-fw">
<Backups url="/diag_backup.php"/>
<Defaults url="/diag_defaults.php"/>
<History url="/diag_confbak.php">
<none url="/diag_confbak.php*" visibility="hidden"/>
</History>
<History url="/ui/core/backup/history/this"/>
</Configuration>
<Firmware cssClass="fa fa-building-o fa-fw">
<Status order="10" url="/ui/core/firmware#status"/>

View File

@ -0,0 +1,268 @@
{#
# Copyright (c) 2023 Deciso B.V.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#}
<style>
.diff_record {
white-space: pre-wrap;
font-family: monospace;
}
</style>
<script>
'use strict';
function get_backups(){
let provider = $("#providers").val();
$("#host_processing").show();
ajaxGet('/api/core/backup/backups/' + provider, {}, function(data, status){
let target1 = $("#backups1").empty();
let target2 = $("#backups2").empty();
if (data.items && Object.keys(data.items).length > 1) {
Object.keys(data.items).forEach(function(key) {
let record = data.items[key];
let payload = $("<div>");
payload.append(record.time_iso, "&nbsp;", record.username, "<br/>");
payload.append($("<small/>").text(record.description));
target1.append($("<option/>").attr('value', record.id).attr('data-content', payload.html()));
target2.append($("<option/>").attr('value', record.id).attr('data-content', payload.html()));
});
}
$(".backups").selectpicker('refresh');
$("#backups1").change();
$("#host_processing").hide();
if ($("#backups1 option").length === 0) {
$("#diff_tfoot").show();
} else {
$("#diff_tfoot").hide();
}
});
if (provider === 'this' ) {
$(".only_local").show();
} else {
$(".only_local").hide();
}
}
$( document ).ready(function () {
ajaxGet('/api/core/backup/providers', {}, function(data, status){
let selected_host = "{{selected_host}}";
if (data.items && Object.keys(data.items).length > 1) {
let target = $("#providers").empty();
Object.keys(data.items).forEach(function(key) {
let opt = $("<option/>").attr('value', key).text(data.items[key].description);
if (selected_host == key) {
opt.attr('selected', 'selected');
}
target.append(opt);
});
target.selectpicker('refresh');
}
$("#providers").change(get_backups);
$("#providers").change();
if ($("#providers > option").length > 1) {
$("#providers_pane").show();
}
});
$("#backups1").change(function(){
let target = $("#backups2");
target.val($("#backups1").val());
if ($('#backups2 option').length > target[0].selectedIndex) {
target[0].selectedIndex = target[0].selectedIndex + 1;
}
target.selectpicker('refresh');
target.change();
});
$("#backups2").change(function(){
let provider = $("#providers").val();
let url = '/api/core/backup/diff/'+provider+'/'+$("#backups1").val()+'/'+$("#backups2").val();
ajaxGet(url, {}, function(data, status){
let target = $("#diff_records").empty();
if (data.items && data.items.length > 1) {
for (let i=0 ; i < data.items.length ; ++i) {
let $tr = $("<tr/>");
let $td = $("<td class='diff_record'/>").append($("<div>").html(data.items[i]).text());
let color = '#000000';
switch (data.items[i][0]) {
case '+':
color = '#3bbb33';
break;
case '-':
color = '#c13928';
break;
case '@':
color = '#3bb9c3';
break;
}
$td.css('color', color);
target.append($tr.append($td));
}
}
});
});
$("#act_revert").click(function(event){
event.preventDefault();
if (!$("#backups1").val()) {
return;
}
BootstrapDialog.show({
type: BootstrapDialog.TYPE_INFO,
title: "{{ lang._('Action') }}",
message: "{{ lang._('Restore from Configuration Backup') }} <br/>" + $("#backups1 option:selected").data('content'),
buttons: [{
label: "{{ lang._('No') }}",
action: function(dialogRef) {
dialogRef.close();
}}, {
label: "{{ lang._('Yes') }}",
action: function(dialogRef) {
ajaxCall("/api/core/backup/revert_backup/" + $("#backups1").val(),{}, function(){
dialogRef.close();
get_backups();
});
}
}]
});
});
$("#act_remove").click(function(event){
event.preventDefault();
if (!$("#backups1").val()) {
return;
}
BootstrapDialog.show({
type: BootstrapDialog.TYPE_DANGER,
title: "{{ lang._('Action') }}",
message: "{{ lang._('Remove Configuration Backup') }} <br/>" + $("#backups1 option:selected").data('content'),
buttons: [{
label: "{{ lang._('No') }}",
action: function(dialogRef) {
dialogRef.close();
}}, {
label: "{{ lang._('Yes') }}",
action: function(dialogRef) {
ajaxCall("/api/core/backup/delete_backup/" + $("#backups1").val(),{}, function(){
dialogRef.close();
});
}
}]
});
});
$("#act_download").click(function(event){
event.preventDefault();
if (!$("#backups1").val()) {
return;
}
let url = '/api/core/backup/download/' + $("#providers").val() + '/' + $("#backups1").val();
let link = document.createElement("a");
$(link).click(function(e) {
e.preventDefault();
window.location.href = url;
});
$(link).click();
});
});
</script>
<div class="tab-content content-box __mb" id="providers_pane" style="display: none;">
<div class="row">
<div class="col-xs-12">
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>{{ lang._('Host')}}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<select id="providers" class="selectpicker">
<option value="this" selected>{{ lang._('This Firewall')}}</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="tab-content content-box __mb">
<div class="row">
<div class="col-xs-12">
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>
{{ lang._('Backups (compare)')}}
<i id="host_processing" class="fa fa-fw fa-spinner fa-pulse" style="display: none;"></i>
</th>
</tr>
</thead>
<tbody>
<tr style="height: 70px;">
<td>
<select class="selectpicker backups" id="backups1"></select>
<select class="selectpicker backups" id="backups2"></select>
<div>
<a id="act_revert" class="only_local btn btn-default btn-xs" data-toggle="tooltip" title="{{ lang._('Revert to this configuration')}}">
<i class="fa fa-sign-in fa-fw"></i>
</a>
<a id="act_remove" class="only_local btn btn-default btn-xs" data-toggle="tooltip" title="{{ lang._('Remove this backup')}}">
<i class="fa fa-trash fa-fw"></i>
</a>
<a id="act_download" class="btn btn-default btn-xs" data-toggle="tooltip" title="{{ lang._('Download this backup')}}">
<i class="fa fa-download fa-fw"></i>
</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="tab-content content-box __mb">
<div class="row">
<div class="col-xs-12">
<table class="table table-condensed">
<thead>
<tr>
<th >{{ lang._('Changes between selected versions')}}</th>
</tr>
</thead>
<tbody id="diff_records">
</tbody>
<tfoot id="diff_tfoot" style="display: none;">
<tr>
<td>{{ lang._('No backups available')}}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>

View File

@ -1,325 +0,0 @@
<?php
/*
* Copyright (C) 2014 Deciso B.V.
* Copyright (C) 2005 Colin Smith <ethethlay@gmail.com>
* Copyright (C) 2010 Jim Pingle <jimp@pfsense.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
require_once("guiconfig.inc");
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$pconfig['backupcount'] = isset($config['system']['backupcount']) ? $config['system']['backupcount'] : null;
$cnf = OPNsense\Core\Config::getInstance();
$confvers = $cnf->getBackups(true);
array_shift($confvers);
if (!empty($_GET['getcfg'])) {
foreach ($confvers as $filename => $revision) {
if ($revision['time'] == $_GET['getcfg']) {
$exp_name = urlencode("config-{$config['system']['hostname']}.{$config['system']['domain']}-{$_GET['getcfg']}.xml");
$exp_data = file_get_contents($filename);
$exp_size = strlen($exp_data);
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename={$exp_name}");
header("Content-Length: $exp_size");
echo $exp_data;
exit;
}
}
}
$oldfile = '';
$newfile = '';
$diff = '';
if (!empty($_GET['diff']) && isset($_GET['oldtime']) && isset($_GET['newtime'])
&& is_numeric($_GET['oldtime']) && (is_numeric($_GET['newtime']) || ($_GET['newtime'] == 'current'))) {
foreach ($confvers as $filename => $revision) {
if ($revision['time'] == $_GET['oldtime']) {
$oldfile = $filename;
}
if ($revision['time'] == $_GET['newtime']) {
$newfile = $filename;
}
}
$oldtime = $_GET['oldtime'];
$oldcheck = $oldtime;
if ($_GET['newtime'] == 'current') {
$newfile = '/conf/config.xml';
$newtime = $config['revision']['time'];
} else {
$newtime = $_GET['newtime'];
$newcheck = $newtime;
}
} elseif (count($confvers)) {
$files = array_keys($confvers);
$newfile = '/conf/config.xml';
$newtime = $config['revision']['time'];
$oldfile = $files[0];
$oldtime = $confvers[$oldfile]['time'];
}
if (file_exists($oldfile) && file_exists($newfile)) {
exec("/usr/bin/diff -u " . escapeshellarg($oldfile) . " " . escapeshellarg($newfile), $diff);
}
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input_errors = array();
$pconfig = $_POST;
$cnf = OPNsense\Core\Config::getInstance();
$confvers = $cnf->getBackups(true);
array_shift($confvers);
$user = getUserEntry($_SESSION['Username']);
$readonly = userHasPrivilege($user, 'user-config-readonly');
if (!empty($_POST['act']) && $_POST['act'] == 'revert') {
foreach ($confvers as $filename => $revision) {
if (isset($revision['time']) && $revision['time'] == $_POST['time']) {
if (!$readonly && config_restore($filename) == 0) {
$savemsg = sprintf(gettext('Successfully reverted to timestamp %s with description "%s".'), date(gettext("n/j/y H:i:s"), $revision['time']), $revision['description']);
} else {
$savemsg = gettext("Unable to revert to the selected configuration.");
}
break;
}
}
} elseif (!empty($_POST['act']) && $_POST['act'] == "delete") {
foreach ($confvers as $filename => $revision) {
if (isset($revision['time']) && $revision['time'] == $_POST['time']) {
if (!$readonly && file_exists($filename)) {
$savemsg = sprintf(gettext('Deleted backup with timestamp %s and description "%s".'), date(gettext("n/j/y H:i:s"), $revision['time']), $revision['description']);
unset($confvers[$filename]);
@unlink($filename);
} else {
$savemsg = gettext("Unable to delete the selected configuration.");
}
break;
}
}
}
}
include("head.inc");
?>
<script>
//<![CDATA[
$(document).ready(function () {
// revert config dialog
$(".act_revert").click(function () {
var id = $(this).data('id');
BootstrapDialog.show({
type: BootstrapDialog.TYPE_INFO,
title: "<?= html_safe(gettext('Action')) ?>",
message: "<?= html_safe(gettext('Restore from Configuration Backup')) ?> <br/> <?= html_safe(gettext('Version')) ?>: " + id,
buttons: [{
label: "<?= html_safe(gettext('No')) ?>",
action: function (dialogRef) {
dialogRef.close();
}
}, {
label: "<?= html_safe(gettext('Yes')) ?>",
action: function (dialogRef) {
$("#time").val(id);
$("#action").val("revert");
$("#iform").submit()
}
}]
});
});
// delete backup dialog
$(".act_delete").click(function () {
var id = $(this).data('id');
BootstrapDialog.show({
type: BootstrapDialog.TYPE_DANGER,
title: "<?= html_safe(gettext('Action')) ?>",
message: "<?= html_safe(gettext('Remove Configuration Backup')) ?> <br/> <?= html_safe(gettext('Version')) ?>: " + id,
buttons: [{
label: "<?= html_safe(gettext('No')) ?>",
action: function (dialogRef) {
dialogRef.close();
}
}, {
label: "<?= html_safe(gettext('Yes')) ?>",
action: function (dialogRef) {
$("#time").val(id);
$("#action").val("delete");
$("#iform").submit()
}
}]
});
});
});
//]]>
</script>
<body>
<?php
include("fbegin.inc");
?>
<section class="page-content-main">
<form method="post" id="iform">
<input type="hidden" id="time" name="time" value="" />
<input type="hidden" id="action" name="act" value="" />
</form>
<div class="container-fluid">
<div class="row">
<?php if (isset($input_errors) && count($input_errors) > 0) print_input_errors($input_errors); ?>
<?php if ($savemsg) print_info_box($savemsg); ?>
<section class="col-xs-12">
<?php if ($diff): ?>
<div class="content-box tab-content table-responsive __mb" style="overflow: scroll;">
<table class="table table-striped">
<tbody>
<tr>
<td colspan="2">
<strong><?= sprintf(
gettext('Configuration diff from %s to %s'),
date(gettext('n/j/y H:i:s'), $oldtime),
date(gettext('n/j/y H:i:s'), $newtime)
) ?></strong>
</td>
</tr>
<tr>
<td>
<?php
foreach ($diff as $line):
switch (substr($line, 0, 1)) {
case '+':
$color = '#3bbb33';
break;
case '-':
$color = '#c13928';
break;
case '@':
$color = '#3bb9c3';
break;
default:
$color = '#000000';
} ?>
<span style="color: <?= $color; ?>; white-space: pre-wrap; font-family: monospace;"><?= html_safe($line) ?></span>
<br/>
<?php endforeach; ?>
</td>
</tr>
</tbody>
</table>
</div>
<?php endif ?>
<?php if (count($confvers)): ?>
<form method="get">
<div class="content-box tab-content table-responsive">
<table class="table table-striped">
<tbody>
<tr>
<td colspan="2"><strong><?= gettext('History') ?></strong></td>
</tr>
<tr>
<td>
<button type="submit" name="diff" class="btn btn-primary pull-left" value="Diff">
<?= gettext('View differences'); ?>
</button>
</td>
<td>
<?= gettext("To view the differences between an older configuration and a newer configuration, select the older configuration using the left column of radio options and select the newer configuration in the right column, then press the button."); ?>
</td>
</tr>
</tbody>
</table>
<table class="table table-striped">
<tbody>
<tr>
<th colspan="2"><?= gettext("Diff"); ?></th>
<th><?= gettext("Date"); ?></th>
<th><?= gettext("Size"); ?></th>
<th><?= gettext("Configuration Change"); ?></th>
<th class="text-nowrap"></th>
</tr>
<tr>
<td></td>
<td>
<input type="radio" name="newtime" value="current" <?= !isset($newcheck) || $newcheck == 'current' ? 'checked="checked"' : '' ?>/>
</td>
<td><?= date(gettext("n/j/y H:i:s"), $config['revision']['time']) ?></td>
<td><?= format_bytes(filesize("/conf/config.xml")) ?></td>
<td><?= html_safe($config['revision']['username'])?>: <?=html_safe($config['revision']['description']); ?></td>
<td class="text-nowrap"><strong><?= gettext("Current"); ?></strong></td>
</tr>
<?php $last = count($confvers); $curr = 1; foreach ($confvers as $version): ?>
<tr>
<td>
<input type="radio" name="oldtime"
value="<?= $version['time']; ?>" <?= (!isset($oldcheck) && $curr == 1) || (isset($oldcheck) && $oldcheck == $version['time']) ? 'checked="checked"' : '' ?>/>
</td>
<td>
<?php if ($curr != $last): ?>
<input type="radio" name="newtime" value="<?= $version['time']; ?>" <?= isset($newcheck) && $newcheck == $version['time'] ? 'checked="checked"' : '' ?>/>
<?php endif ?>
</td>
<td><?= date(gettext("n/j/y H:i:s"), $version['time']) ?></td>
<td><?= format_bytes($version['filesize']) ?></td>
<td><?= html_safe($version['username']);?>: <?=html_safe($version['description']);?></td>
<td class="text-nowrap">
<a data-id="<?= $version['time']; ?>" href="#"
class="act_revert btn btn-default btn-xs" data-toggle="tooltip"
title="<?= html_safe(gettext('Revert to this configuration')) ?>">
<i class="fa fa-sign-in fa-fw"></i>
</a>
<a data-id="<?= $version['time']; ?>" href="#"
class="act_delete btn btn-default btn-xs" data-toggle="tooltip"
title="<?= html_safe(gettext('Remove this backup')) ?>">
<i class="fa fa-trash fa-fw"></i>
</a>
<a href="diag_confbak.php?getcfg=<?= $version['time']; ?>"
class="btn btn-default btn-xs" data-toggle="tooltip"
title="<?= html_safe(gettext('Download this backup')) ?>">
<i class="fa fa-download fa-fw"></i>
</a>
</td>
</tr>
<?php $curr++; endforeach ?>
</tbody>
</table>
</div>
</form>
<?php endif ?>
</section>
</div>
</div>
</section>
<?php
include 'foot.inc';