Blob Blame History Raw
#!/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2010-2015 Red Hat, Inc.
#
# Authors:
# Thomas Woerner <twoerner@redhat.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import sys
from PyQt5 import QtGui, QtCore, QtWidgets

import gi
gi.require_version('Notify', '0.7')
from gi.repository import Notify

import os
from dbus.mainloop.pyqt5 import DBusQtMainLoop
import functools

from firewall import config
from firewall.core.fw_nm import nm_is_imported, nm_get_zone_of_connection, \
                                nm_set_zone_of_connection, \
                                nm_get_dbus_interface, \
                                nm_get_connections
from firewall.core.watcher import Watcher
from firewall.client import FirewallClient
import slip.dbus
import dbus
import signal

import gettext
gettext.textdomain(config.DOMAIN)
_ = gettext.gettext

PATH = [ ]
for p in os.getenv("PATH").split(":"):
    if p not in PATH:
        PATH.append(p)

def search_app(app):
    for p in PATH:
        _app = "%s/%s" % (p, app)
        if os.path.exists(_app):
            return _app
    return None

NM_CONNECTION_EDITOR = ""
for binary in [ "/usr/bin/nm-connection-editor",
                    "/bin/nm-connection-editor",
                "/usr/bin/kde5-nm-connection-editor",
                    "/bin/kde5-nm-connection-editor",
                "/usr/bin/kde-nm-connection-editor",
                    "/bin/kde-nm-connection-editor" ]:
    if os.path.exists(binary):
        NM_CONNECTION_EDITOR = binary
        break

PY2 = sys.version < '3'

def escape(text):
    text = text.replace('&', '&amp;')
    text = text.replace('>', '&gt;')
    text = text.replace('<', '&lt;')
    return text

def fromUTF8(text):
    if PY2 and QtCore.QT_VERSION < 0x050000:
        return QtCore.QString.fromUtf8(text)
    return text

# ZoneInterfaceEditor #########################################################

class ZoneInterfaceEditor(QtWidgets.QDialog):
    def __init__(self, fw, interface, zone):
        self.fw = fw
        self.interface = interface
        self.zone = None
        self.title = _("Select zone for interface '%s'") % self.interface

        QtWidgets.QDialog.__init__(self)
        self.create_ui(zone)

    def create_ui(self, zone):
        self.setWindowTitle(fromUTF8(escape(self.title)))
        self.rejected.connect(self.hide)

        self.resize(100, 50)

        vbox = QtWidgets.QVBoxLayout()
        vbox.setSpacing(6)

        label = QtWidgets.QLabel(fromUTF8(escape(self.title)))
        vbox.addWidget(label)

        self.combo = QtWidgets.QComboBox()
        self.fill_zone_combo()
        vbox.addWidget(self.combo)

        buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok
                                           | QtWidgets.QDialogButtonBox.Cancel)
        self.ok_button = buttonBox.button(QtWidgets.QDialogButtonBox.Ok)
        buttonBox.accepted.connect(self.ok)
        buttonBox.rejected.connect(self.hide)
        vbox.addWidget(buttonBox)
        self.ok_button.setDisabled(True)

        self.combo.activated.connect(self.combo_changed)
        self.setLayout(vbox)
        self.set_zone(zone)

    def combo_changed(self):
        self.ok_button.setDisabled(self.get_zone() == self.zone)

    def set_zone(self, zone):
        self.zone = zone
        if zone == "":
            self.combo.setCurrentIndex(self.combo.findText(
                escape(_("Default Zone"))))
        else:
            self.combo.setCurrentIndex(self.combo.findText(self.zone))
        self.combo_changed()

    def get_zone(self):
        text = str(self.combo.currentText())
        if text == escape(_("Default Zone")):
            text = ""
        return text

    def fill_zone_combo(self):
        self.combo.clear()
        self.combo.addItem(fromUTF8(escape(_("Default Zone"))))
        for z in self.fw.getZones():
            self.combo.addItem(z)

    def zones_changed(self):
        zone = self.get_zone()
        self.fill_zone_combo()
        self.set_zone(zone)

    def ok(self):
        self.fw.changeZoneOfInterface(self.get_zone(), self.interface)
        self.hide()

# ZoneConnectionEditor ########################################################

class ZoneConnectionEditor(ZoneInterfaceEditor):
    def __init__(self, fw, connection, connection_name, zone):
        self.fw = fw
        self.connection = connection
        self.connection_name = connection_name
        self.zone = None
        self.title = _("Select zone for connection '%s'") % self.connection_name

        QtWidgets.QDialog.__init__(self)
        self.create_ui(zone)

    def ok(self):
        # apply changes
        try:
            nm_set_zone_of_connection(self.get_zone(), self.connection)
        except Exception:
            text = _("Failed to set zone {zone} for connection {connection_name}")
            QtWidgets.QMessageBox.warning(None, fromUTF8(escape(self.title)),
                                      escape(text.format(
                                          zone=self.get_zone(),
                                          connection_name=self.connection_name)))
        self.hide()

# ZoneSourceEditor ############################################################

class ZoneSourceEditor(ZoneInterfaceEditor):
    def __init__(self, fw, source, zone):
        self.fw = fw
        self.source = source
        self.zone = None
        self.title = _("Select zone for source '%s'") % self.source

        QtWidgets.QDialog.__init__(self)
        self.create_ui(zone)

    def ok(self):
        self.fw.changeZoneOfSource(self.get_zone(), self.source)
        self.hide()

# ShieldsEditor #########################################################

class ShieldsEditor(QtWidgets.QDialog):
    def __init__(self, fw, settings, shields_up, shields_down):
        self.fw = fw
        self.settings = settings
        self.shields_up = shields_up
        self.shields_down = shields_down
        self.title = _("Configure Shields Up/Down Zones")

        QtWidgets.QDialog.__init__(self)
        self.create_ui()

    def create_ui(self):
        self.setWindowTitle(fromUTF8(escape(self.title)))
        self.rejected.connect(self.hide)

        vbox = QtWidgets.QVBoxLayout()
        vbox.setSpacing(6)

        label = QtWidgets.QLabel(fromUTF8(escape(
            _("Here you can select the zones used for Shields Up and "
              "Shields Down."))))
        label.setWordWrap(True)
        vbox.addWidget(label)

        label = QtWidgets.QLabel(fromUTF8(escape(
            _("This feature is useful for people using the default zones "
              "mostly. For users, that are changing zones of connections, it "
              "might be of limited use."))))
        label.setWordWrap(True)
        vbox.addWidget(label)

        grid = QtWidgets.QGridLayout()
        grid.setSpacing(6)

        label = QtWidgets.QLabel(fromUTF8(escape(_("Shields Up Zone:"))))
        label.setWordWrap(True)
        grid.addWidget(label, 0, 0, 1, 1)

        self.shields_up_combo = QtWidgets.QComboBox()
        #self.fill_combo(self.shields_up_combo)
        #self.set_shields_up(self.shields_up)
        grid.addWidget(self.shields_up_combo, 0, 1, 1, 1)

        button = QtWidgets.QPushButton(_("Reset To Default"))
        button.clicked.connect(self.reset_shields_up)
        grid.addWidget(button, 0, 2, 1, 1)

        label = QtWidgets.QLabel(fromUTF8(escape(_("Shields Down Zone:"))))
        label.setWordWrap(True)
        grid.addWidget(label, 1, 0, 1, 1)

        self.shields_down_combo = QtWidgets.QComboBox()
        #self.fill_combo(self.shields_down_combo)
        #self.set_shields_down(self.shields_down)
        grid.addWidget(self.shields_down_combo, 1, 1, 1, 1)

        button = QtWidgets.QPushButton(_("Reset To Default"))
        button.clicked.connect(self.reset_shields_down)
        grid.addWidget(button, 1, 2, 1, 1)

        vbox.addLayout(grid)

        buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok
                                           | QtWidgets.QDialogButtonBox.Cancel)
        self.ok_button = buttonBox.button(QtWidgets.QDialogButtonBox.Ok)
        buttonBox.accepted.connect(self.ok)
        buttonBox.rejected.connect(self.hide)
        vbox.addWidget(buttonBox)
        self.ok_button.setDisabled(True)

        self.shields_up_combo.activated.connect(self.shields_combo_changed)
        self.shields_down_combo.activated.connect(self.shields_combo_changed)

        self.setLayout(vbox)

    def shields_combo_changed(self):
        self.ok_button.setDisabled(
            self.get_shields_up() == self.shields_up and \
            self.get_shields_down() == self.shields_down)

    def set_shields_up(self, zone):
        self.shields_up = zone
        if self.shields_up_combo.count() > 0:
            self.shields_up_combo.setCurrentIndex(
                self.shields_up_combo.findText(self.shields_up))
        self.shields_combo_changed()

    def set_shields_down(self, zone):
        self.shields_down = zone
        if self.shields_down_combo.count() > 0:
            self.shields_down_combo.setCurrentIndex(
                self.shields_down_combo.findText(self.shields_down))
        self.shields_combo_changed()

    def reset_shields_up(self):
        self.set_shields_up(self.shields_up)
        # remove user key to get fallback again
        self.settings.remove("shields-up")

    def reset_shields_down(self):
        self.set_shields_down(self.shields_down)
        # remove user key to get fallback again
        self.settings.remove("shields-down")

    def get_shields_up(self):
        return str(self.shields_up_combo.currentText())

    def get_shields_down(self):
        return str(self.shields_down_combo.currentText())

    def zones_changed(self):
        up_zone = self.shields_up
        if self.get_shields_up():
            up_zone = self.get_shields_up()
        down_zone = self.shields_down
        if self.get_shields_down():
            down_zone = self.get_shields_down()

        for z in self.fw.getZones():
            self.shields_up_combo.addItem(z)
            self.shields_down_combo.addItem(z)

        self.set_shields_up(up_zone)
        self.set_shields_down(down_zone)

    def ok(self):
        if self.shields_up != self.get_shields_up():
            self.settings.setValue("shields-up", self.get_shields_up())
        if self.shields_down != self.get_shields_down():
            self.settings.setValue("shields-down", self.get_shields_down())
        self.settings.sync()
        self.hide()

# AboutDialog #################################################################

class AboutDialog(QtWidgets.QDialog):
    def __init__(self, name, icon, version, url, copyright, authors, license):
        QtWidgets.QDialog.__init__(self)
        self.setWindowIcon(icon)
        self.setWindowTitle(fromUTF8(escape(_("About %s" % name))))
        self.resize(500, 250)

        vbox = QtWidgets.QVBoxLayout()
        vbox.setSpacing(6)

        hbox = QtWidgets.QHBoxLayout()
        hbox.setSpacing(24)

        label = QtWidgets.QLabel()
        label.setPixmap(icon.pixmap(96))
        label.setMinimumSize(96, 96)
        label.setMaximumSize(96, 96)
        hbox.addWidget(label)

        vbox2 = QtWidgets.QVBoxLayout()
        vbox2.setSpacing(3)

        label = QtWidgets.QLabel(name)
        font = label.font()
        font.setPointSize(font.pointSize()*2)
        font.setBold(True)
        label.setFont(font)
        vbox2.addWidget(label)

        vbox2.addWidget(QtWidgets.QLabel(version))

        label = QtWidgets.QLabel("<a href=\"%s\">%s</a>" % (url, url))
        label.setTextFormat(QtCore.Qt.RichText)
        label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
        label.setOpenExternalLinks(True)

        vbox2.addWidget(label)

        vbox2.addWidget(QtWidgets.QLabel(copyright))

        hbox.addLayout(vbox2)

        vbox.addLayout(hbox)

        tabs = QtWidgets.QTabWidget()
        tabs.setStyleSheet("QTabWidget::tab { padding: 1px 1px 1px 1px; }")

        tab = QtWidgets.QWidget()
        vbox3 = QtWidgets.QVBoxLayout()
        textedit = QtWidgets.QPlainTextEdit()
        #textedit.setStyleSheet("QPlainTextEdit { border: 0; padding: 0; }")
        textedit.setReadOnly(True)
        textedit.setPlainText(fromUTF8("\n".join(authors)))
        vbox3.addWidget(textedit)
        tab.setLayout(vbox3)
        tabs.addTab(tab, fromUTF8(escape(_("Authors"))))

        tab = QtWidgets.QWidget()
        vbox3 = QtWidgets.QVBoxLayout()
        textedit = QtWidgets.QPlainTextEdit()
        #textedit.setStyleSheet("QPlainTextEdit { border: 0; padding: 0; }")
        textedit.setReadOnly(True)
        textedit.setPlainText(license)
        vbox3.addWidget(textedit)
        tab.setLayout(vbox3)
        tabs.addTab(tab, fromUTF8(escape(_("License"))))

        vbox.addWidget(tabs)

        buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Close)
        buttonBox.rejected.connect(self.hide)
        vbox.addWidget(buttonBox)

        self.setLayout(vbox)

# TrayApplet ##################################################################

class TrayApplet(QtWidgets.QSystemTrayIcon):
    def __init__(self):
        super(TrayApplet, self).__init__()
        self.name = _("Firewall Applet")
        self.prog = "firewall-applet"
        self.icon_name = "firewall-applet"
        self.icons = {
            "normal": QtGui.QIcon.fromTheme(self.icon_name),
            "error": QtGui.QIcon.fromTheme(self.icon_name+"-error"),
            "panic": QtGui.QIcon.fromTheme(self.icon_name+"-panic"),
            "normal-shields_up": QtGui.QIcon.fromTheme(self.icon_name+"-shields_up"),
            "normal-shields_down": QtGui.QIcon.fromTheme(self.icon_name+"-shields_down"),
        }
        self.timer = None
        self.mode = None
        self.blink = False
        self.blink_count = 0
        self._blink = False
        self._blink_count = 0
        self.show_inactive = False
        self.tooltip_messages = [ ]

        self.active_zones = { }
        self.connections = { }
        self.connections_name = { }
        self.default_zone = None
        self.zone_connection_editors = { }
        self.zone_interface_editors = { }
        self.zone_source_editors = { }

        # settings

        self.settings = QtCore.QSettings("firewall", "applet")

        # file system watcher

        self.watcher = Watcher(self.load_settings, 2)
        self.watcher.add_watch_file("/etc/firewall/applet.conf")
        self.watcher.add_watch_file(str(self.settings.fileName()))

        # about dialog

        self.about_dialog = AboutDialog(self.name,
                                        self.icons["normal"],
                                        config.VERSION, config.WEBSITE,
                                        config.COPYRIGHT, config.AUTHORS,
                                        config.LICENSE)

        # urgencies

        self.urgencies = { "noicon": QtWidgets.QSystemTrayIcon.NoIcon,
                           "information": QtWidgets.QSystemTrayIcon.Information,
                           "warning": QtWidgets.QSystemTrayIcon.Warning,
                           "critical": QtWidgets.QSystemTrayIcon.Critical }

        # actions

        self.shieldsupAction = QtWidgets.QAction(fromUTF8(escape(_("Shields Up"))),
                                             self)
        self.shieldsupAction.setCheckable(True)
        self.shieldsupAction.setChecked(False)
        self.shieldsupAction.triggered.connect(self.shieldsup_changed_cb)

        self.notificationsAction = QtWidgets.QAction(
            fromUTF8(escape(_("Enable Notifications"))), self)
        self.notificationsAction.setCheckable(True)
        self.notificationsAction.setChecked(False)
        self.notificationsAction.triggered.connect(self.notification_changed_cb)

        self.settingsAction = QtWidgets.QAction(
            fromUTF8(escape(_("Edit Firewall Settings..."))), self)
        self.settingsAction.triggered.connect(self.configure_cb)

        self.changeZonesAction = QtWidgets.QAction(
            fromUTF8(escape(_("Change Zones of Connections..."))), self)
        self.changeZonesAction.triggered.connect(self.nm_connection_editor)

        self.shieldsAction = QtWidgets.QAction(
            fromUTF8(escape(_("Configure Shields UP/Down Zones..."))), self)
        self.shieldsAction.triggered.connect(self.configure_shields)

        self.panicAction = QtWidgets.QAction(
            fromUTF8(escape(_("Block all network traffic"))), self)
        self.panicAction.setCheckable(True)
        self.panicAction.setChecked(False)
        self.panicAction.triggered.connect(self.panic_mode_cb)

        self.aboutAction = QtWidgets.QAction(fromUTF8(escape(_("About"))), self)
        self.aboutAction.triggered.connect(self.about_dialog.exec_)

        #self.quitAction = QtWidgets.QAction(fromUTF8(escape(_("Quit"))), self,
        #                                triggered=self.quit)

        self.connectionsAction = QtWidgets.QWidgetAction(self)
        self.connectionsAction.setDefaultWidget(QtWidgets.QLabel(
            fromUTF8("<b>"+escape(_("Connections"))+"</b> ")))

        self.interfacesAction = QtWidgets.QWidgetAction(self)
        self.interfacesAction.setDefaultWidget(QtWidgets.QLabel(
            fromUTF8("<b>"+escape(_("Interfaces"))+"</b> ")))

        self.sourcesAction = QtWidgets.QWidgetAction(self)
        self.sourcesAction.setDefaultWidget(QtWidgets.QLabel(
            fromUTF8("<b>"+escape(_("Sources"))+"</b> ")))

        # init

        self.left_menu = QtWidgets.QMenu()
        self.left_menu.setStyleSheet('QMenu { margin: 5px; }')

        self.right_menu = QtWidgets.QMenu()
        self.right_menu.addAction(self.shieldsupAction)
        self.right_menu.addAction(self.notificationsAction)
        self.right_menu.addSeparator()
        self.right_menu.addAction(self.settingsAction)
        self.right_menu.addAction(self.changeZonesAction)
        self.right_menu.addAction(self.shieldsAction)
        self.right_menu.addSeparator()
        self.right_menu.addAction(self.panicAction)
        self.right_menu.addSeparator()
        self.right_menu.addAction(self.aboutAction)
        #self.right_menu.addSeparator()
        #self.right_menu.addAction(self.quitAction)
        self.setContextMenu(self.right_menu)

        self.activated.connect(self.activated_cb)

        self.set_mode("error")
        self.set_icon()

        self.setVisible(self.show_inactive)

        # init notification

        Notify.init(self.prog)

        # connect to firewalld

        DBusQtMainLoop(set_as_default=True)
        try:
            self.bus = slip.dbus.SystemBus()
            self.bus.default_timeout = None
        except Exception as msg:
            print("Not using slip", msg)
            self.bus = dbus.SystemBus()

        if nm_is_imported():
            self.bus.add_signal_receiver(
                self.nm_signal_receiver,
                dbus_interface=nm_get_dbus_interface(),
                signal_name='PropertiesChanged',
                member_keyword='member')
        self.nm_signal_receiver()

        self.fw = FirewallClient(self.bus, wait=1)
        self.fw.setExceptionHandler(self._exception_handler)

        self.fw.connect("connection-established", self.connection_established)
        self.fw.connect("connection-lost", self.connection_lost)

        self.fw.connect("reloaded", self.reloaded),
        self.fw.connect("default-zone-changed", self.default_zone_changed)
        self.fw.connect("panic-mode-enabled", self.panic_mode_enabled)
        self.fw.connect("panic-mode-disabled", self.panic_mode_disabled)
        self.fw.connect("interface-added", self.interface_added)
        self.fw.connect("interface-removed", self.interface_removed)
        self.fw.connect("zone-of-interface-changed",
                        self.zone_of_interface_changed)
        self.fw.connect("source-added", self.source_added)
        self.fw.connect("source-removed", self.source_removed)
        self.fw.connect("zone-of-source-changed",
                        self.zone_of_source_changed)

        self.shields_editor = ShieldsEditor(self.fw, self.settings, None, None)

        self.load_settings()

    def _exception_handler(self, exception_message):
        if "NotAuthorizedException" in exception_message:
            self.error(fromUTF8(escape(_("Authorization failed."))))
        elif "INVALID_NAME" in exception_message:
            msg = exception_message.replace("INVALID_NAME", _("Invalid name"))
            self.warning(fromUTF8(escape(msg)))
        elif "NAME_CONFLICT" in exception_message:
            msg = exception_message.replace("NAME_CONFLICT",
                                            _("Name already exists"))
            self.warning(fromUTF8(escape(msg)))
        elif "NO_DEFAULTS" in exception_message:
            pass
        else:
            self.error(fromUTF8(exception_message))

    def quit(self):
        sys.exit(1)

    def set_icon(self, mode=None):
        if mode is not None:
            self.setIcon(self.icons[mode])
        elif self.mode != "normal":
            self.setIcon(self.icons[self.mode])
        elif self.default_zone == self.shields_up:
            self.setIcon(self.icons["normal-shields_up"])
        else:
            self.setIcon(self.icons["normal-shields_down"])

    def load_settings(self, name=None):
        self.settings.sync()

        notifications = self.settings.value("notifications", False, type=bool)
        self.notificationsAction.setChecked(notifications)
        self.show_inactive = self.settings.value("show-inactive", False,
                                                 type=bool)
        self.blink = self.settings.value("blink", False, type=bool)
        self.blink_count = self.settings.value("blink-count", 5, type=int)

        self.shields_up = self.settings.value("shields-up", "block", type=str)
        if self.default_zone:
            self.shieldsupAction.setChecked(
                self.default_zone == self.shields_up)
        self.shields_editor.set_shields_up(self.shields_up)
        self.shields_down = self.settings.value("shields-down", "public",
                                                type=str)
        self.shields_editor.set_shields_down(self.shields_down)

        #print("shields-up=%s" % self.shields_up)
        #print("notifications=%s" % notifications)
        #print("blink=%s" % self.blink)
        #print("blink-count=%s" % self.blink_count)
        #print("show-inactive=%s" % self.show_inactive)

        if not self.fw.connected:
            self.setVisible(self.show_inactive)
        else:
            self.setVisible(True)

    def activated_cb(self, reason):
        if reason == QtWidgets.QSystemTrayIcon.Trigger:
            self.left_menu.popup(QtGui.QCursor.pos())

    def update_active_zones(self):
        self.active_zones.clear()

        # remove all entries for the left menu
        self.left_menu.clear()

        # add connections entry
        self.left_menu.addAction(self.connectionsAction)

        if not self.fw.connected:
            return

        active_zones = self.fw.getActiveZones()
        if active_zones:
            self.active_zones = active_zones

        # get all active connections (NM) and interfaces
        connections = { }
        interfaces = { }
        sources = { }
        for zone in sorted(self.active_zones):
            if "interfaces" in self.active_zones[zone]:
                for interface in sorted(self.active_zones[zone]["interfaces"]):
                    if interface not in self.connections:
                        interfaces[interface] = zone
            if "sources" in self.active_zones[zone]:
                for source in sorted(self.active_zones[zone]["sources"]):
                    sources[source] = zone
        # NM controlled connections
        for interface in self.connections:
            connection = self.connections[interface]
            if connection not in self.connections_name:
                connection_name = None
            else:
                connection_name = self.connections_name[connection]
            zone = nm_get_zone_of_connection(connection)
            connections[connection] = [ zone, connection_name ]

        binding = _("{entry} (Zone: {zone})")

        # add NM controlled bindings
        for connection in sorted(connections):
            zone = connections[connection][0]
            connection_name = connections[connection][1]
            if zone == "":
                _binding = _("{entry} (Default Zone: {default_zone})")
                action = QtWidgets.QAction(
                    fromUTF8(escape(
                        _binding.format(default_zone=self.default_zone,
                                        entry=connection_name))), self)
            else:
                action = QtWidgets.QAction(
                    fromUTF8(escape(binding.format(zone=zone,
                                                   entry=connection_name))), self)
            action.triggered.connect(functools.partial(
                self.zone_connection_editor, connection, connection_name, zone))
            self.left_menu.addAction(action)

        # add interfaces entry
        self.left_menu.addAction(self.interfacesAction)

        # add other interfaces
        for interface in sorted(interfaces):
            zone = interfaces[interface]
            action = QtWidgets.QAction(
                fromUTF8(escape(binding.format(zone=zone, entry=interface))),
                self)
            action.triggered.connect(functools.partial(
                self.zone_interface_editor, interface, zone))
            self.left_menu.addAction(action)

        # add interfaces entry
        self.left_menu.addAction(self.sourcesAction)

        for source in sorted(sources):
            zone = sources[source]
            action = QtWidgets.QAction(
                fromUTF8(escape(binding.format(zone=zone, entry=source))),
                self)
            action.triggered.connect(functools.partial(
                self.zone_source_editor, source, zone))
            self.left_menu.addAction(action)

    def zone_interface_editor(self, interface, zone):
        if interface in self.zone_interface_editors:
            self.zone_interface_editors[interface].set_zone(zone)
            self.zone_interface_editors[interface].show()
            return self.zone_interface_editors[interface].raise_()

        editor = ZoneInterfaceEditor(self.fw, interface, zone)
        self.zone_interface_editors[interface] = editor
        editor.show()
        editor.raise_()
        editor.show()

    def zone_connection_editor(self, connection, connection_name, zone):
        if connection in self.zone_connection_editors:
            self.zone_connection_editors[connection].set_zone(zone)
            self.zone_connection_editors[connection].show()
            return self.zone_connection_editors[connection].raise_()

        editor = ZoneConnectionEditor(self.fw, connection, connection_name, zone)
        self.zone_connection_editors[connection] = editor
        editor.show()
        editor.raise_()
        editor.show()

    def zone_source_editor(self, source, zone):
        if source in self.zone_source_editors:
            self.zone_source_editors[source].set_zone(zone)
            self.zone_source_editors[source].show()
            return self.zone_source_editors[source].raise_()

        editor = ZoneSourceEditor(self.fw, source, zone)
        self.zone_source_editors[source] = editor
        editor.show()
        editor.raise_()
        editor.show()

    def nm_signal_receiver(self, *args, **kwargs):
        self.connections.clear()
        self.connections_name.clear()

        # do not use NMClient could result in python core dump

        if nm_is_imported():
            text = _("Failed to get connections from NetworkManager")
            try:
                nm_get_connections(self.connections, self.connections_name)
            except Exception:
                self.notify(escape(text), urgency=Notify.Urgency.CRITICAL)
                if text not in self.tooltip_messages:
                    self.tooltip_messages.append(text)
            else:
                if text in self.tooltip_messages:
                    self.tooltip_messages.remove(text)

        else:
            text = _("No NetworkManager imports available")
            self.notify(escape(text), urgency=Notify.Urgency.CRITICAL)
            if text not in self.tooltip_messages:
                self.tooltip_messages.append(text)

        self.update_tooltip()

    def notify(self, msg, urgency="noicon", timeout=5):
        #self.showMessage(fromUTF8(escape(self.name)), msg, self.urgencies[urgency], timeout*1000)
        n = Notify.Notification.new(escape(self.name), msg, self.icon_name)
        n.set_urgency(Notify.Urgency.NORMAL)
        try:
            n.show()
        except:
            return

    def shieldsup_changed_cb(self):
        if self.shieldsupAction.isChecked():
            zone = str(self.shields_up)
        else:
            zone = str(self.shields_down)

        if self.fw.connected and self.default_zone != zone:
            try:
                self.fw.setDefaultZone(zone)
            except dbus.exceptions.DBusException as e:
                print("Error: %s" % e.get_dbus_message())

    def notification_changed_cb(self):
        self.settings.setValue("notifications",
                               self.notificationsAction.isChecked())
        self.settings.sync()

    def __blink(self, arg=None):
        if self._blink_count != 0:
            if self._blink_count > 0 and self._blink:
                self._blink_count -= 1
            self._blink = not self._blink
            if not self.timer:
                self.timer = QtCore.QTimer(self)
                self.timer.timeout.connect(self.__blink)
                self.timer.setInterval(1000)
            self.timer.start()
        if not self._blink:
            self.set_icon()
        else:
            self.set_icon("normal")

    def get_mode(self):
        return self.mode

    def set_mode(self, mode):
        if self.mode != mode:
            if self.timer and self.timer.isActive():
                self.timer.stop()
                self._blink = False
            self.mode = mode

        elif self.mode == mode and self.timer:
            if self._blink_count == 0:
                self._blink_count += 1
            return

        if mode == "normal":
            self.set_icon()
            return

        if self.blink:
            if self.blink_count != 0:
                self._blink = True
                self._blink_count = self.blink_count
                self.__blink()
        else:
            self.set_icon()

    def update_tooltip(self):
        if self.get_mode() == "error":
            self.setToolTip(fromUTF8("<big><b>" + \
                                     _("No connection to firewall daemon") + \
                                     "</b></big>"))
            return

        messages = [ ]

        if self.panicAction.isChecked():
            messages.append("<big><b>" + \
                            _("All network traffic is blocked.") + \
                            "</b></big>")

        if self.default_zone:
            messages.append(_("Default Zone: '%s'") % self.default_zone)

        for interface in self.connections:
            connection = self.connections[interface]
            zone = nm_get_zone_of_connection(connection)
            if zone == "":
                text = _("Default Zone '{default_zone}' active for connection "
                         "'{connection}' on interface '{interface}'")
            else:
                text = _("Zone '{zone}' active for connection "
                         "'{connection}' on interface '{interface}'")
            messages.append(text.format(zone=zone,
                                        default_zone=self.default_zone,
                                        connection=connection,
                                        interface=interface))

        if len(self.active_zones) > 0:
            for zone in sorted(self.active_zones):
                if "interfaces" in self.active_zones[zone]:
                    for interface in sorted(self.active_zones[zone]["interfaces"]):
                        if interface not in self.connections:
                            text = _("Zone '{zone}' active for interface "
                                     "'{interface}'")
                            connection = None
                            messages.append(text.format(zone=zone,
                                                        connection=connection,
                                                        interface=interface))
                if "sources" in self.active_zones[zone]:
                    for source in sorted(self.active_zones[zone]["sources"]):
                        text = _("Zone '{zone}' active for source {source}")
                        connection = None
                        messages.append(text.format(zone=zone, source=source))
        else:
            messages.append(_("No Active Zones."))
        messages.extend(self.tooltip_messages)

        tooltip = "<nobr>"+"</nobr><br><nobr>".join(messages)+"</nobr>"
        self.setToolTip(fromUTF8("<font size=\"3\">"+tooltip+"</font>"))
        self.set_icon()

    def show(self):
        # do not automatically show the applet
        pass

    def panic_mode_cb(self):
        if not self.fw or not self.fw.connected:
            return
        if self.panicAction.isChecked():
            self.fw.enablePanicMode()
        else:
            self.fw.disablePanicMode()
        self.panicAction.setChecked(not self.panicAction.isChecked())

    def configure_shields(self):
        self.shields_editor.show()
        self.shields_editor.raise_()

    def nm_connection_editor(self, item, uuid=None):
        if NM_CONNECTION_EDITOR == "":
            self.warning("NetworkManager connection editor is missing.")
            return

        if uuid:
            if "kde-" in NM_CONNECTION_EDITOR:
                os.system("%s %s &" % (NM_CONNECTION_EDITOR, uuid))
            else:
                os.system("%s --edit=%s &" % (NM_CONNECTION_EDITOR, uuid))
        else:
            os.system("%s &" % NM_CONNECTION_EDITOR)

    def warning(self, text):
        QtWidgets.QMessageBox.warning(None, fromUTF8(escape(self.name)), text)

    def error(self, text):
        QtWidgets.QMessageBox.critical(None, fromUTF8(escape(self.name)), text)

    def configure_cb(self, widget):
        os.system("firewall-config &")

    # firewallClient signal receivers

    def connection_established(self, first=False):
        self.default_zone = self.fw.getDefaultZone()
        self.panicAction.setChecked(self.fw.queryPanicMode())
        self.update_active_zones()
        self.shields_editor.zones_changed()

        if self.shields_up:
            self.shieldsupAction.setChecked(
                self.default_zone == self.shields_up)

        if self.notificationsAction.isChecked():
            self.notify(escape(_("Connection to FirewallD established.")))
        self.setVisible(True)
        self.set_mode("normal")
        self.update_tooltip()

    def connection_lost(self):
        self.default_zone = None
        self.set_mode("error")
        self.update_active_zones()
        self.update_tooltip()
        self.panicAction.setChecked(False)
        if self.notificationsAction.isChecked():
            self.notify(escape(_("Connection to FirewallD lost.")))
        self.setVisible(self.show_inactive)

    def reloaded(self):
        if self.notificationsAction.isChecked():
            self.notify(escape(_("FirewallD has been reloaded.")))
        self.update_active_zones()
        self.update_tooltip()

    def default_zone_changed(self, zone):
        self.default_zone = zone
        if self.notificationsAction.isChecked():
            self.notify(escape(_("Default zone changed to '%s'.") % zone))
        if self.shields_up:
            self.shieldsupAction.setChecked(
                self.default_zone == self.shields_up)
        self.update_active_zones()
        self.update_tooltip()

    def _panic_mode(self, enable):
        self.panicAction.setChecked(enable)

        self.update_tooltip()

        if enable:
            self.set_mode("panic")
        else:
            self.set_mode("normal")

        if self.notificationsAction.isChecked():
            ed = { 1: _("All network traffic is blocked."),
                   0: _("Network traffic is not blocked anymore.") }
            self.notify(escape(ed[enable]))

    def panic_mode_enabled(self):
        self._panic_mode(True)

    def panic_mode_disabled(self):
        self._panic_mode(False)

    def _interface(self, zone, interface, enable):
        self.update_active_zones()
        self.update_tooltip()

        # close dialog of removed interface
        if not enable:
            if interface in self.connections:
                connection = self.connections[interface]
                if connection in self.zone_connection_editors:
                    self.zone_connection_editors[connection].hide()
                    del self.zone_connection_editors[connection]
            elif interface in self.zone_interface_editors:
                self.zone_interface_editors[interface].hide()
                del self.zone_interface_editors[interface]

        # send notification if enabled
        if self.notificationsAction.isChecked():
            ed = { 1: _("activated"),
                   0: _("deactivated") }
            if interface in self.connections:
                connection = self.connections[interface]
                zone = nm_get_zone_of_connection(connection)
                if zone == "":
                    text = _("Default zone '{default_zone}' "
                             "{activated_deactivated} for "
                             "connection '{connection}' on "
                             "interface '{interface}'")
                else:
                    text = _("Zone '{zone}' {activated_deactivated} for "
                             "connection '{connection}' on "
                             "interface '{interface}'")
            else:
                connection = None
                text = _("Zone '{zone}' {activated_deactivated} for "
                         "interface '{interface}'")
            self.notify(escape(text.format(
                zone=zone,
                default_zone=self.default_zone,
                activated_deactivated=ed[enable],
                connection=connection, interface=interface)))

    def interface_added(self, zone, interface):
        self._interface(zone, interface, True)

    def interface_removed(self, zone, interface):
        self._interface(zone, interface, False)

    def zone_of_interface_changed(self, zone, interface):
        # update zone editor
        if interface in self.zone_interface_editors:
            self.zone_interface_editors[interface].set_zone(zone)

        self.update_active_zones()
        self.update_tooltip()

        if self.notificationsAction.isChecked():
            self.notify(escape(_("Zone '%s' activated for interface '%s'") % \
                               (zone, interface)))

    def _source(self, zone, source, enable):
        self.update_active_zones()
        self.update_tooltip()

        # close dialog of removed source
        if not enable:
            if source in self.zone_source_editors:
                self.zone_source_editors[source].hide()
                del self.zone_source_editors[source]

        # send notification if enabled
        if self.notificationsAction.isChecked():
            ed = { 1: _("activated"),
                   0: _("deactivated") }
            text = _("Zone '{zone}' {activated_deactivated} for "
                     "source '{source}'")
            self.notify(escape(text.format(
                zone=zone, activated_deactivated=ed[enable], source=source)))

    def source_added(self, zone, source):
        self._source(zone, source, True)

    def source_removed(self, zone, source):
        self._source(zone, source, False)

    def zone_of_source_changed(self, zone, source):
        index = source
        if source in self.zone_source_editors:
            self.zone_source_editors[source].set_zone(zone)

        # update zone editor
        if index in self.zone_interface_editors:
            self.zone_interface_editors[index].set_zone(zone)

        self.update_active_zones()
        self.update_tooltip()

        if self.notificationsAction.isChecked():
            self.notify(escape(_("Zone '%s' activated for source '%s'") % \
                               (zone, source)))

# MAIN ########################################################################

if len(sys.argv) > 1:
    print("""Usage: %s [options]

Options:
  -h, --help     show this help message and exit
"""  % sys.argv[0])
    sys.exit(1)

# reset SIGINT signal to default
signal.signal(signal.SIGINT, signal.SIG_DFL)

app = QtWidgets.QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)

applet = TrayApplet()
applet.show()
sys.exit(app.exec_())