From f75ec9688a033799d05238fb0e0bf02b27e4d05e Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Sat, 9 Sep 2023 13:05:59 +0200 Subject: [PATCH] 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 --- plist | 4 +- .../OPNsense/Core/Api/BackupController.php | 214 ++++++++++++ .../OPNsense/Core/BackupController.php | 44 +++ .../mvc/app/models/OPNsense/Core/ACL/ACL.xml | 3 +- .../app/models/OPNsense/Core/Menu/Menu.xml | 4 +- .../views/OPNsense/Core/backup_history.volt | 268 +++++++++++++++ src/www/diag_confbak.php | 325 ------------------ 7 files changed, 532 insertions(+), 330 deletions(-) create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Core/Api/BackupController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Core/BackupController.php create mode 100644 src/opnsense/mvc/app/views/OPNsense/Core/backup_history.volt delete mode 100644 src/www/diag_confbak.php diff --git a/plist b/plist index 51eea4fd4..befb91d7a 100644 --- a/plist +++ b/plist @@ -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 diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/BackupController.php b/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/BackupController.php new file mode 100644 index 000000000..6451d49d7 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/BackupController.php @@ -0,0 +1,214 @@ +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; + } + } + } + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Core/BackupController.php b/src/opnsense/mvc/app/controllers/OPNsense/Core/BackupController.php new file mode 100644 index 000000000..fa50a9802 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Core/BackupController.php @@ -0,0 +1,44 @@ +view->selected_host = $selected_host; + $this->view->pick('OPNsense/Core/backup_history'); + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml index 8b0c7abcb..3fd2cc90a 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml @@ -80,7 +80,8 @@ Diagnostics: Configuration History - diag_confbak.php* + ui/core/backup/history* + api/core/backup/* diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml index 578a1920f..5d4812793 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml @@ -34,9 +34,7 @@ - - - + diff --git a/src/opnsense/mvc/app/views/OPNsense/Core/backup_history.volt b/src/opnsense/mvc/app/views/OPNsense/Core/backup_history.volt new file mode 100644 index 000000000..8a44d3157 --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/Core/backup_history.volt @@ -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. +#} + + + + + + +
+
+
+ + + + + + + + + + + +
+ {{ lang._('Backups (compare)')}} + +
+ + + +
+
+
+
+ +
+
+
+ + + + + + + + + + + + + +
{{ lang._('Changes between selected versions')}}
+
+
+
diff --git a/src/www/diag_confbak.php b/src/www/diag_confbak.php deleted file mode 100644 index a67f8d0ff..000000000 --- a/src/www/diag_confbak.php +++ /dev/null @@ -1,325 +0,0 @@ - - * Copyright (C) 2010 Jim Pingle - * 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"); -?> - - - - - - -
-
- - -
-
-
- 0) print_input_errors($input_errors); ?> - -
- -
- - - - - - - - - -
- -
- - -
- -
-
- - -
-
- - - - - - - - - - -
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- /> - :
- /> - - - /> - - : - - - - - - - - - -
-
-
- -
-
-
-
-