diff --git a/net/firewall/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php b/net/firewall/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php index d2ed30745..4c06d3e39 100644 --- a/net/firewall/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php +++ b/net/firewall/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php @@ -29,6 +29,7 @@ namespace OPNsense\Firewall\Api; use OPNsense\Base\ApiMutableModelControllerBase; use OPNsense\Core\Backend; +use OPNsense\Core\Config; class FilterController extends ApiMutableModelControllerBase { @@ -73,4 +74,37 @@ class FilterController extends ApiMutableModelControllerBase return array("status" => "error"); } } + + public function savepointAction() + { + if ($this->request->isPost()) { + // trigger a save, so we know revision->time matches our running config + Config::getInstance()->save(); + return array( + "status" => "ok", + "retention" => (string)Config::getInstance()->backupCount(), + "revision" => (string)Config::getInstance()->object()->revision->time + ); + } else { + return array("status" => "error"); + } + } + + public function revertAction($revision) + { + if ($this->request->isPost()) { + Config::getInstance()->lock(); + $filename = Config::getInstance()->getBackupFilename($revision); + if (!$filename) { + Config::getInstance()->unlock(); + return ["status" => gettext("unknown (or removed) savepoint")]; + } + $this->getModel()->rollback($revision); + Config::getInstance()->unlock(); + (new Backend())->configdRun('filter reload'); + return ["status" => "ok"]; + } else { + return array("status" => "error"); + } + } } diff --git a/net/firewall/src/opnsense/mvc/app/models/OPNsense/Firewall/Filter.php b/net/firewall/src/opnsense/mvc/app/models/OPNsense/Firewall/Filter.php index d47492863..5294bb4d4 100644 --- a/net/firewall/src/opnsense/mvc/app/models/OPNsense/Firewall/Filter.php +++ b/net/firewall/src/opnsense/mvc/app/models/OPNsense/Firewall/Filter.php @@ -28,6 +28,7 @@ namespace OPNsense\Firewall; +use OPNsense\Core\Config; use Phalcon\Validation\Message; use OPNsense\Base\BaseModel; use OPNsense\Firewall\Util; @@ -82,4 +83,26 @@ class Filter extends BaseModel } return $messages; } + + /** + * Rollback this model to a previous version. + * Make sure to remove this object afterwards, since its contents won't be updated. + * @param $revision float|string revision number + */ + public function rollback($revision) + { + $filename = Config::getInstance()->getBackupFilename($revision); + if ($filename) { + // fiddle with the dom, copy OPNsense->Firewall->FilterRule from backup to current config + $sourcexml = simplexml_load_file($filename); + if ($sourcexml->OPNsense->Firewall->FilterRule) { + $sourcedom = dom_import_simplexml($sourcexml->OPNsense->Firewall->FilterRule); + $targetxml = Config::getInstance()->object(); + $targetdom = dom_import_simplexml($targetxml->OPNsense->Firewall->FilterRule); + $node = $targetdom->ownerDocument->importNode($sourcedom, TRUE); + $targetdom->parentNode->replaceChild($node, $targetdom); + Config::getInstance()->save(); + } + } + } } \ No newline at end of file diff --git a/net/firewall/src/opnsense/mvc/app/views/OPNsense/Firewall/filter.volt b/net/firewall/src/opnsense/mvc/app/views/OPNsense/Firewall/filter.volt index 1e3389e9f..e6191dba6 100644 --- a/net/firewall/src/opnsense/mvc/app/views/OPNsense/Firewall/filter.volt +++ b/net/firewall/src/opnsense/mvc/app/views/OPNsense/Firewall/filter.volt @@ -21,6 +21,47 @@ } $("#reconfigureAct").SimpleActionButton(); + $("#savepointAct").SimpleActionButton({ + onAction: function(data, status){ + stdDialogInform( + "{{ lang._('Savepoint created') }}", + data['revision'], + "{{ lang._('Close') }}" + ); + } + }); + + $("#revertAction").on('click', function(){ + BootstrapDialog.show({ + type: BootstrapDialog.TYPE_DEFAULT, + title: "{{ lang._('Revert to savepoint') }}", + message: "

{{ lang._('Enter a savepoint to rollback to.') }}

" + + '
' + + '' + + ''+ + '
', + buttons: [{ + label: "{{ lang._('Revert') }}", + cssClass: 'btn-primary', + action: function(dialogRef) { + ajaxCall("/api/firewall/filter/revert/" + $("#revertToTime").val(), {}, function (data, status) { + if (data.status !== "ok") { + $("#revertToTime").parent().addClass("has-error"); + $("#revertToTimeError").html(data.status); + } else { + std_bootgrid_reload("grid-rules"); + dialogRef.close(); + } + }); + } + }], + onshown: function(dialogRef) { + $("#revertToTime").parent().removeClass("has-error"); + $("#revertToTimeError").html(""); + $("#revertToTime").val(""); + } + }); + }); }); @@ -64,6 +105,18 @@ data-error-title="{{ lang._('Filter load error') }}" type="button" > + +
+ + +