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 }