Warning, /plasma/plasma-firewall/kcm/ui/main.qml is written in an unsupported language. File is not indexed.

0001 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0002 // SPDX-FileCopyrightText: 2018 Alexis Lopes Zubeta <contact@azubieta.net>
0003 // SPDX-FileCopyrightText: 2020 Tomaz Canabrava <tcanabrava@kde.org>
0004 // SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
0005 
0006 import QtQuick
0007 import QtQuick.Layouts 1.3
0008 import QtQuick.Controls as QQC2
0009 import Qt.labs.qmlmodels as Labs
0010 
0011 import org.kde.kcmutils as KCMUtils
0012 
0013 import org.kcm.firewall 1.0
0014 
0015 import org.kde.kirigami 2.14 as Kirigami
0016 
0017 KCMUtils.ScrollViewKCM {
0018     id: root
0019 
0020     implicitHeight: Kirigami.Units.gridUnit * 25
0021     implicitWidth: Kirigami.Units.gridUnit * 44
0022 
0023     property var policyChoices : [
0024         {text: i18n("Allow"), data: "allow", tooltip: i18n("Allow all connections")},
0025         {text: i18n("Ignore"), data: "deny", tooltip: i18n("Keeps the program waiting until the connection attempt times out, some short time later.")},
0026         {text: i18n("Reject"), data: "reject", tooltip: i18n("Produces an immediate and very informative 'Connection refused' message")}
0027     ]
0028 
0029     Kirigami.OverlaySheet {
0030         id: drawer
0031 
0032         parent: root.QQC2.Overlay.overlay
0033 
0034         onVisibleChanged: {
0035             if (visible) {
0036                 ruleEdit.forceActiveFocus();
0037             } else {
0038                 // FIXME also reset rule
0039                 ruleEditMessage.visible = false;
0040             }
0041         }
0042 
0043         title: ruleEdit.newRule ? i18n("Create A New Firewall Rule") : i18n("Edit Firewall Rule")
0044 
0045         ColumnLayout {
0046             spacing: Kirigami.Units.largeSpacing
0047 
0048             Kirigami.InlineMessage {
0049                 id: ruleEditMessage
0050                 type: Kirigami.MessageType.Error
0051                 Layout.fillWidth: true
0052             }
0053 
0054             RuleEdit {
0055                 id: ruleEdit
0056                 client: kcm.client
0057                 height: childrenRect.height
0058                 implicitWidth: 30 * Kirigami.Units.gridUnit
0059 
0060                 Keys.onEnterPressed: event => accept()
0061                 Keys.onReturnPressed: event => accept()
0062 
0063                 function accept() {
0064                     var job = kcm.client[newRule ? "addRule" : "updateRule"](rule);
0065                     if (!job) {
0066                         ruleEditMessage.text = i18n("Please restart plasma firewall, the backend disconnected.");
0067                         ruleEditMessage.visible = true;
0068                         return;
0069                     }
0070 
0071                     busy = true;
0072                     kcm.needsSave = true;
0073                     job.result.connect(() => {
0074                         busy = false;
0075 
0076                         if (job.error) {
0077                             // don't show an error when user canceled…
0078                             if (job.error !== 4) { // FIXME magic number
0079                                 if (newRule) {
0080                                     ruleEditMessage.text = i18n("Error creating rule: %1", job.errorString);
0081                                 } else {
0082                                     ruleEditMessage.text = i18n("Error updating rule: %1", job.errorString);
0083                                 }
0084                                 ruleEditMessage.visible = true;
0085 
0086                             }
0087                             // …but also don't close in this case!
0088                             return;
0089                         }
0090 
0091                         drawer.close();
0092                     });
0093                 }
0094             }
0095 
0096             InlineBusyIndicator {
0097                 Layout.alignment: Qt.AlignHCenter
0098                 running: ruleEdit.busy
0099                 visible: running
0100             }
0101         }
0102 
0103         footer: QQC2.DialogButtonBox {
0104             enabled: ruleEdit.ready
0105 
0106             QQC2.Button {
0107                 text: ruleEdit.newRule ? i18n("Create") : i18n("Save")
0108                 icon.name: ruleEdit.newRule ? "document-new" : "document-save"
0109                 QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
0110             }
0111 
0112             onAccepted: {
0113                 if (ruleEdit.simple.index > -1) {
0114                     ruleEdit.rule.sourceApplication = ruleEdit.simple.service[ruleEdit.simple.index]
0115                 }
0116                 ruleEdit.accept()
0117             }
0118         }
0119     }
0120 
0121     header: ColumnLayout {
0122         id: columnLayout
0123 
0124         Kirigami.InlineMessage {
0125             id: firewallInlineErrorMessage
0126             Layout.fillWidth: true
0127             type: Kirigami.MessageType.Error
0128         }
0129         Kirigami.FormLayout {
0130             RowLayout {
0131                 Kirigami.FormData.label: i18n("Firewall Status:")
0132                 QQC2.CheckBox {
0133                     id: enabledCheckBox
0134                     property QtObject activeJob: null
0135                     text: {
0136                         if (kcm.client.enabled) {
0137                             return activeJob ? i18n("Disabling…") : i18n("Enabled")
0138                         } else {
0139                             return activeJob ? i18n("Enabling…") : i18n("Disabled")
0140                         }
0141                     }
0142                     enabled: !activeJob && !connectEnableTimer.running
0143 
0144                     function bindCurrent() {
0145                         checked = Qt.binding(function() {
0146                             return kcm.client.enabled;
0147                         });
0148                     }
0149                     Component.onCompleted: bindCurrent()
0150 
0151                     // FirewallD has a delay after the request to disable, to accept
0152                     // enable actions, but the delay does not return with the job result
0153                     // this is an ugly hack.
0154                     Timer {
0155                         id: connectEnableTimer
0156                         interval: 4000
0157                         repeat: false
0158                     }
0159 
0160                     onToggled: {
0161                         const enable = checked; // store the state on job begin, not when it finished
0162 
0163                         const job = kcm.client.setEnabled(checked);
0164                         if (job === null) {
0165                             firewallInlineErrorMessage.text = i18n("The firewall application, please install %1", kcm.client.name);
0166                             firewallInlineErrorMessage.visible = true;
0167                             return;
0168                         }
0169                         enabledCheckBox.activeJob = job;
0170                         job.result.connect(function () {
0171                             enabledCheckBox.activeJob = null; // need to explicitly unset since gc will clear it non-deterministic
0172                             bindCurrent();
0173 
0174                             if (job.error && job.error !== 4) {
0175                                 console.log(job.errorString);
0176                                 var errorString = job.errorString;
0177                                 // Firewalld is sending a typo to us.
0178                                 if (errorString.indexOf("Permission denied") !== -1) {
0179                                     errorString = i18n("Permission denied");
0180                                 }
0181 
0182                                 if (errorString.indexOf("unable to initialize table") !== -1) {
0183                                     firewallInlineErrorMessage.text = i18n("You recently updated your kernel. Iptables is failing to initialize, please reboot.")
0184                                 } else {
0185                                     firewallInlineErrorMessage.text = enabled
0186                                         ? i18n("Error enabling firewall: %1", errorString)
0187                                         : i18n("Error disabling firewall: %1", errorString)
0188                                 }
0189                                 firewallInlineErrorMessage.visible = true;
0190                             }
0191                             if (!enable && !job.error) {
0192                                 connectEnableTimer.start();
0193                             }
0194                         });
0195                         job.start();
0196                     }
0197                 }
0198 
0199                 InlineBusyIndicator {
0200                     Layout.fillHeight: true
0201                     running: enabledCheckBox.activeJob !== null || connectEnableTimer.running
0202                 }
0203             }
0204 
0205             Repeater {
0206                 model: [
0207                     {label: i18n("Default Incoming Policy:"), key: "Incoming"},
0208                     {label: i18n("Default Outgoing Policy:"), key: "Outgoing"}
0209                 ]
0210 
0211                 RowLayout {
0212                     Kirigami.FormData.label: modelData.label
0213 
0214                     QQC2.ComboBox {
0215                         id: policyCombo
0216 
0217                         property QtObject activeJob: null
0218                         // TODO currentValue
0219                         readonly property string currentPolicy: policyChoices[currentIndex].data
0220 
0221                         model: policyChoices
0222                         textRole: "text"
0223                         enabled: !activeJob && kcm.client.enabled
0224                         QQC2.ToolTip.text: policyChoices[currentIndex].tooltip
0225                         QQC2.ToolTip.delay: 1000
0226                         QQC2.ToolTip.timeout: 5000
0227                         QQC2.ToolTip.visible: hovered
0228 
0229                         Binding { // :(
0230                             target: ruleEdit
0231                             property: "default" + modelData.key + "PolicyRule"
0232                             value: policyCombo.currentPolicy
0233                         }
0234 
0235                         function bindCurrent() {
0236                             currentIndex = Qt.binding(function() {
0237                                 return policyChoices.findIndex((choice) => choice.data === kcm.client["default" + modelData.key + "Policy"]);
0238                             });
0239                         }
0240                         Component.onCompleted: bindCurrent()
0241 
0242                         onActivated: {
0243                             const job = kcm.client["setDefault" + modelData.key + "Policy"](currentPolicy)
0244                             if (!job) {
0245                                 firewallInlineErrorMessage.text = i18n("Please restart plasma firewall, the backend disconnected.");
0246                                 firewallInlineErrorMessage.visible = true;
0247                                 return;
0248                            }
0249                             policyCombo.activeJob = job;
0250                             job.result.connect(function () {
0251                                 policyCombo.activeJob = null;
0252                                 bindCurrent();
0253 
0254                                 if (job.error && job.error !== 4) { // TODO magic number
0255                                     firewallInlineErrorMessage.text = i18n("Error changing policy: %1", job.errorString)
0256                                     firewallInlineErrorMessage.visible = true;
0257                                 }
0258                             });
0259                         }
0260                     }
0261 
0262                     InlineBusyIndicator {
0263                         Layout.fillHeight: true
0264                         running: policyCombo.activeJob !== null
0265                     }
0266                 }
0267             }
0268         }
0269     }
0270 
0271     QQC2.HorizontalHeaderView {
0272         id: horizontalHeader
0273         syncView: tableView
0274         visible: tableView.rows > 0
0275         selectionModel: ItemSelectionModel{}
0276     }
0277 
0278     view: TableView {
0279         id: tableView
0280         anchors.fill: parent
0281         topMargin: horizontalHeader.height
0282         resizableColumns: true
0283         alternatingRows: true
0284 
0285         property int currentHoveredRow: -1
0286 
0287         selectionModel: ItemSelectionModel {}
0288         selectionMode: TreeView.SelectRows
0289         function selectRelative(delta) {
0290             var nextRow = selectionModel.currentIndex.row + delta
0291             if (nextRow < 0) {
0292                 nextRow = 0
0293             }
0294             if (nextRow >= rows) {
0295                 nextRow = rows - 1
0296             }
0297             var index = model.index(nextRow, selectionModel.currentIndex.column)
0298             selectionModel.setCurrentIndex(index, ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows)
0299         }
0300         Keys.onUpPressed: selectRelative(-1)
0301         Keys.onDownPressed: selectRelative(1)
0302 
0303         function editRule(row) {
0304             ruleEdit.rule = kcm.client.ruleAt(row);
0305             ruleEdit.newRule = false;
0306             drawer.open();
0307         }
0308         Keys.onEnterPressed: Keys.returnPressed(event)
0309         Keys.onReturnPressed: {
0310             if (selectionModel.currentIndex) {
0311                 editRule(selectionModel.currentIndex.row)
0312             }
0313         }
0314 
0315         HoverHandler {
0316             onPointChanged: {
0317                 view.currentHoveredRow = view.cellAtPosition(point.position).y
0318             }
0319         }
0320 
0321         model: kcm.client.rulesModel
0322         columnWidthProvider: (column) => {
0323             let explicitWidth = explicitColumnWidth(column)
0324             if (explicitWidth > 0) {
0325                 return explicitWidth
0326             }
0327             const columnWidths = [];
0328             columnWidths[RuleListModel.ActionColumn] =  Kirigami.Units.gridUnit * 8
0329             columnWidths[RuleListModel.FromColumn] =  Kirigami.Units.gridUnit * 10
0330             columnWidths[RuleListModel.ToColumn] =  Kirigami.Units.gridUnit * 10
0331             columnWidths[RuleListModel.Ipv6Column] = Kirigami.Units.gridUnit * 4
0332             columnWidths[RuleListModel.LoggingColumn] = Kirigami.Units.gridUnit * 5
0333             columnWidths[RuleListModel.EditColumn] = Kirigami.Units.gridUnit * 6
0334             return columnWidths[column]
0335         }
0336         delegate: Labs.DelegateChooser {
0337             Labs.DelegateChoice {
0338                 column: RuleListModel.EditColumn
0339                 RowLayout {
0340                     id: ruleActionsRow
0341                     required property var model
0342                     required property bool current
0343                     required property bool selected
0344                     property QtObject activeJob: null
0345                     spacing: 0
0346                     // TODO InlineBusyIndicator?
0347                     enabled: !activeJob
0348                     visible: tableView.currentHoveredRow === model.row || selected
0349 
0350                     Item {
0351                         Layout.fillWidth: true
0352                     }
0353 
0354                     QQC2.ToolButton {
0355                         Layout.fillHeight: true
0356                         icon.name: "edit-entry"
0357                         visible: kcm.client.supportsRuleUpdate
0358                         onClicked: tableView.editRule(model.row)
0359                         QQC2.ToolTip {
0360                             text: i18nc("@info:tooltip", "Edit Rule")
0361                         }
0362                     }
0363                     QQC2.ToolButton {
0364                         Layout.fillHeight: true
0365                         icon.name: "edit-delete"
0366                         onClicked: {
0367                             const job = kcm.client.removeRule(model.row);
0368                             if (!job) {
0369                                 firewallInlineErrorMessage.text = i18n("Please restart plasma firewall, the backend disconnected.");
0370                                 firewallInlineErrorMessage.visible = true;
0371                                 return;
0372                             }
0373 
0374                             ruleActionsRow.activeJob = job;
0375                             kcm.needsSave = true;
0376                             job.result.connect(function () {
0377                                 ruleActionsRow.activeJob = null;
0378 
0379                                 if (job.error && job.error !== 4) { // TODO magic number
0380                                     firewallInlineErrorMessage.text = i18n("Error removing rule: %1", job.errorString);
0381                                     firewallInlineErrorMessage.visible = true;
0382                                 }
0383 
0384                             });
0385                         }
0386                         QQC2.ToolTip {
0387                             text: i18nc("@info:tooltip", "Remove Rule")
0388                         }
0389                     }
0390                 }
0391             }
0392             Labs.DelegateChoice {
0393                 QQC2.ItemDelegate {
0394                     required property var model
0395                     required property bool current
0396                     required property bool selected
0397                     text: model.display
0398                     highlighted: selected || current
0399                     onClicked: {
0400                         tableView.selectionModel.setCurrentIndex(tableView.model.index(model.row, model.column), ItemSelectionModel.Rows | ItemSelectionModel.ClearAndSelect)
0401                     }
0402                 }
0403             }
0404         }
0405 
0406         Kirigami.PlaceholderMessage {
0407             parent: tableView.parent
0408             anchors.centerIn: parent
0409             width: tableView.width - (Kirigami.Units.largeSpacing * 12)
0410             visible: tableView.rows === 0
0411             text: !kcm.client.enabled ? i18n("Firewall is disabled") : i18n("No firewall rules have been added")
0412             explanation: kcm.client.enabled ?
0413                 xi18nc("@info", "Click the <interface>Add Rule…</interface> button below to add one") :
0414                 xi18nc("@info", "Enable the firewall with the <interface>Firewall Status</interface> checkbox above, and then click the <interface>Add Rule…</interface> button below to add one")
0415         }
0416     }
0417 
0418     footer: RowLayout {
0419         QQC2.Button {
0420             text: i18nc("'view' is being used as a verb here", "View Connections")
0421             icon.name: "network-connect"
0422             onClicked: kcm.push("ConnectionsView.qml");
0423         }
0424         QQC2.Button {
0425             text: i18nc("'view' is being used as a verb here", "View Logs")
0426             icon.name: "viewlog"
0427             onClicked: kcm.push("LogsView.qml");
0428         }
0429         Item {
0430             Layout.fillWidth: true
0431         }
0432 
0433         QQC2.Button {
0434             enabled: !kcm.client.busy && kcm.client.enabled
0435             icon.name: "list-add"
0436             text: i18n("Add Rule…")
0437             onClicked: {
0438                 ruleEdit.newRule = true
0439                 drawer.open()
0440             }
0441         }
0442 
0443         QQC2.Button {
0444             icon.name: "help-about"
0445             text: i18n("About")
0446             onClicked: root.showAboutView()
0447         }
0448 
0449     }
0450     Component.onCompleted: {
0451         if (kcm.client.name === "") {
0452             firewallInlineErrorMessage.text = i18n("Please install a firewall, such as ufw or firewalld");
0453             firewallInlineErrorMessage.visible = true;
0454             enabledCheckBox.enabled = false;
0455         } else {
0456             // Initialize the client's status.
0457             kcm.client.refresh();
0458         }
0459     }
0460 
0461     function showAboutView() {
0462         const sheet = aboutComponent.createObject(root.QQC2.Overlay.overlay, {name: kcm.client.name, version: kcm.client.version()});
0463         sheet.open();
0464     }
0465 
0466     Component {
0467         id: aboutComponent
0468         About {
0469             onClosed: destroy()
0470         }
0471     }
0472 }