#!/usr/bin/python3 -O
"""
This is graphical frontend to msec.
"""

import os
import sys
import getopt
import logging
import signal
import traceback
import queue
from textwrap import wrap
from threading import Thread
import time

# PyGTK
import warnings
warnings.filterwarnings('error', module='gtk')
try:
    from gi import require_version
    from gi.repository import GLib
    require_version('GdkPixbuf', '2.0')
    from gi.repository import GdkPixbuf
    require_version('Gtk', '3.0')
    from gi.repository import Gtk
    import gi
    from gi.repository import GObject
    from gi.repository import Pango
except Warning as e:
    print("ERROR: %s" % e)
    print("Exiting..")
    sys.exit(1)
warnings.resetwarnings()

# config
import config
# helper tools
import tools

# version
try:
    from version import version
except:
    version = "development version"

# libmsec
from libmsec import MSEC, PERMS, Log

# localization
import gettext
try:
    gettext.install("msec")
except IOError:
    _ = str

# text strings
LEVEL_SECURITY_TEXT=_("""<big><b>Choose security level</b></big>
This application allows you to configure your system security. If you wish
to activate it, choose the appropriate security level: """)

# level descriptions
level_descriptions = {
        "standard": _("""This profile configures a reasonably safe set of security features. It is the suggested level for Desktop. If unsure which profile to use, use this one."""),
        "netbook": _("""This profile is focused on netbooks, laptops or low-end devices, which are only accessed by local users and run on batteries."""),

        "secure": _("""This profile is configured to provide maximum security, even at the cost of limiting the remote access to the system. This level is suggested for security-concerned systems and servers. """),

        "fileserver": _("""This profile is targeted on local network servers, which do not receive accesses from unauthorized Internet users."""),

        "webserver": _("""This profile is provided for servers which are intended to be accessed by unauthorized Internet users."""),
        "audit_daily": _("""This profile is intended for the users who do not rely on msec to change system settings, and use it for periodic checks only. It configures all periodic checks to run once a day."""),
        "audit_weekly": _("""This profile is similar to the 'audit_daily' profile, but it runs all checks weekly."""),
}

# level order. Levels will appear in this order, the unspecified levels will appear last
level_order = ["standard", "netbook", "fileserver", "webserver", "secure", "audit_daily", "audit_weekly"]

# description for level without description
DEFAULT_LEVEL_DESCRIPTION="\n".join(wrap(_("""Custom security level."""), 80))


SYSTEM_SECURITY_TEXT=_("""<big><b>System security options</b></big>
These options control the local security configuration, such as the login restrictions,
password configurations, integration with other security tools, and default file creation
permissions.  """)

NETWORK_SECURITY_TEXT=_("""<big><b>Network security options</b></big>
These options define the network security against remote threats, unauthorized accesses,
and breakin attempts.  """)

PERIODIC_SECURITY_TEXT=_("""<big><b>Periodic security checks</b></big>
These options configure the security checks that should be executed periodically.  """)

EXCEPTIONS_TEXT=_("""<big><b>Exceptions</b></big>
Here you can configure the allowed exceptions for msec periodic security
checks. For each supported test, you may add as many exceptions as you want
for each check. Note that each exception is parsed as a regexp.""")

PERMISSIONS_SECURITY_TEXT=_("""<big><b>File permissions</b></big>
These options allow to fine-tune system permissions for important files and directories.
The following permissions are checked periodically, and any change to the owner, group,
or current permission is reported. The permissions can be enforced, automatically
changing them to the specified values when a change is detected.  """)

SAVE_SETTINGS_TEXT=_("""Save and apply new configuration?""")

#TODO: Prepare for translating the deprecated Gtk.STOCK_OK/CANCEL macros
#TEXT_OK = _("OK")
#TEXT_CANCEL = _("Cancel")
TEXT_OK = Gtk.STOCK_OK
TEXT_CANCEL = Gtk.STOCK_CANCEL

# gui-related settings
DEFAULT_SPACING=5
BANNER="msec.png"

# Box helpers to replace deprecated Gtk.VBox / Gtk.HBox
def VBox(spacing=0, homogeneous=False, **kwargs):
    box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=spacing, **kwargs)
    box.set_homogeneous(homogeneous)
    return box

def HBox(spacing=0, homogeneous=False, **kwargs):
    box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=spacing, **kwargs)
    box.set_homogeneous(homogeneous)
    return box

class BackgroundRunner(Thread):
    # background task runner
    def __init__(self, finish, program):
        Thread.__init__(self)
        """Runs a program in background."""
        self.program = program
        self.finish = finish

    def run(self):
        """Installs tomoyo policy"""
        print("Running %s" % self.program)
        try:
            res = os.system(self.program)
            self.finish.put(res)
        except Exception as e:
            print("Aborted: %s" % e)
            self.finish.put(-1)

class MsecGui(Gtk.Window):
    """Msec GUI"""
    # common columns
    (COLUMN_LEVEL, COLUMN_LEVEL_DESCR, COLUMN_LEVEL_CURRENT) = list(range(3))
    (COLUMN_OPTION, COLUMN_DESCR, COLUMN_VALUE, COLUMN_CUSTOM) = list(range(4))
    (COLUMN_PATH, COLUMN_USER, COLUMN_GROUP, COLUMN_PERM, COLUMN_FORCE, COLUMN_ACL) = list(range(6))
    (COLUMN_EXCEPTION, COLUMN_EXCEPTION_VALUE, COLUMN_POS) = list(range(3))

    def __init__(self, log, msec, perms, msecconfig, permconfig, exceptions, embed=None):
        """Initializes gui"""
        self.log = log
        self.msec = msec
        self.perms = perms

        # current configuration
        self.msecconfig = msecconfig
        self.permconfig = permconfig
        self.exceptions = exceptions

        # pre-defined standard configurations
        self.msec_defaults = {}
        self.perm_defaults = {}
        levels = config.list_available_levels(log, '')
        for z in levels:
            try:
                self.msec_defaults[z] = config.load_defaults(log, z)
            except:
                self.log.error(_("Unable to load configuration for level '%s'") % z)
                traceback.print_exc()
                continue
            try:
                self.perm_defaults[z] = config.load_default_perms(log, z)
            except:
                self.log.error(_("Unable to load permissions for level '%s'") % z)
                traceback.print_exc()
                continue

        # pre-load documentation for all possible options
        self.descriptions = {}
        for option in config.SETTINGS:
            # get documentation for item
            config.find_doc(msec, option, cached=self.descriptions)

        # loading the current config
        self.reload_config()
        if embed:
            # embedding in MCC
            self.window = Gtk.Plug.new(embed)
        else:
            # running standalone
            self.window = Gtk.Window()
            self.window.set_default_size(640, 440)
        self.window.connect('delete-event', self.quit)

        # are we enforcing a level
        self.enforced_level = None
        self.enforcing_level = False

        main_vbox = VBox(spacing=5)
        self.window.add(main_vbox)

        # menu
        menubar = Gtk.MenuBar()
        main_vbox.pack_start(menubar, False, False, 0)
        menus = [
                    (_("_File"),
                    [
                        (_("_Save configuration"), self.ok),
#                        (None, None),
#                        (_("_Import configuration"), None),
#                        (_("_Export configuration"), None),
#                        (None, None),
                        (_("_Quit"), self.quit),
                    ]),
                    (_("_Help"),
                    [
                        (_("_Help"), None),
                        (_("_About"), None),
                    ]),
                ]
        # building menus
        for topmenu, items in menus:
            # file menu
            filemenu = Gtk.MenuItem(label=topmenu, use_underline=True)
            menubar.add(filemenu)
            menu = Gtk.Menu()
            filemenu.set_submenu(menu)
            for submenu, callback in items:
                menuitem = Gtk.MenuItem(label=submenu, use_underline=True)
                #menuitem = Gtk.MenuItem(submenu)
                if callback:
                    menuitem.connect('activate', callback)
                else:
                    menuitem.set_sensitive(False)
                menu.add(menuitem)

        # show logo
        banner = HBox(spacing=10)
        try:
            # logo
            image = Gtk.Image()
            pixbuf = GdkPixbuf.Pixbuf.new_from_file("%s/%s" % (config.MSEC_DIR, BANNER))
            image.set_from_pixbuf(pixbuf)
            banner.pack_start(image, False, False, 0)
            label = Gtk.Label(label=_("MSEC: System Security and Audit"))
            #label.modify_font(Pango.FontDescription("13"))
            banner.pack_start(label, False, False, 0)
            main_vbox.pack_start(banner, False, False, 0)
        except:
            print("Banner %s Not found" % ("%s/%s" % (config.MSEC_DIR, BANNER)))

        # creating main UI
        self.main_notebook = Gtk.Notebook()
        main_vbox.pack_start(self.main_notebook, True, True, 0)

        # creating tabs
        self.notebook = Gtk.Notebook()
        self.main_notebook.append_page(self.create_summary_ui(), Gtk.Label(label=_("Overview")))
        self.main_notebook.append_page(self.notebook, Gtk.Label(label=_("Security settings")))

        # data to change the values
        self.current_options_view = {}
        # checkboxes callbacks
        self.checkboxes_callbacks = {}

        # tabs to create in the intrerface
        tabs = [
            (1, self.level_security_page, _("Basic security")),
            (2, self.system_security_page, _("System security")),
            (3, self.network_security_page, _("Network security")),
            (4, self.periodic_security_page, _("Periodic checks")),
            (5, self.exceptions_page, _("Exceptions")),
            (6, self.permissions_security_page, _("Permissions")),
            ]
        for id, callback, label in tabs:
            self.notebook.append_page(callback(id), Gtk.Label(label=label))

        # are we enabled?
        self.toggle_level(self.base_level)

        # pending signals
        self.signals = queue.Queue()
        GLib.timeout_add(500, self.check_signals)

        self.window.show_all()

    def level_changed(self, treeview, path, col, model):
        """Switches to a new security level"""
        iter = model.get_iter(path)
        level = model.get_value(iter, self.COLUMN_LEVEL)
        print("Switching to %s" % level)
        self.toggle_level(level, force=True)

    def check_signals(self):
        """Checks for received signals"""
        if not self.signals.empty():
            s = self.signals.get()
            if s == signal.SIGTERM:
                self.quit(self.window)
        GLib.timeout_add(500, self.check_signals)

    def check_for_changes(self, curconfig, curperms):
        """Checks for changes in configuration. Returns number of configuration
        changes, the description of changes, and results of msec dry run"""
        # apply config and preview changes
        self.log.start_buffer()
        self.msec.apply(curconfig)
        self.msec.commit(False)
        messages = self.log.get_buffer()
        # check for changed options
        num_changes = 0
        changes = []
        for name, change_type, oldconf, curconf in [
            (_("MSEC option changes"),        _("option"),           self.oldconfig, curconfig),
            (_("System permissions changes"), _("permission check"), self.oldperms, curperms), ]:
            # check for changes
            opt_changes = []
            opt_adds = []
            opt_dels = []
            # changed options
            curchanges = ""
            opt_changes = [opt for opt in oldconf if (curconf.get(opt) != oldconf.get(opt) and curconf.get(opt) != None and curconf.get(opt) != None)]
            if len(opt_changes) > 0:
                curchanges += "\n\t" + "\n\t".join([_("changed %s <b>%s</b> (%s -> %s)") % (change_type, param, oldconf.get(param), curconf.get(param)) for param in opt_changes])
                num_changes += len(opt_changes)
            # new options
            opt_adds = [opt for opt in curconf.list_options() if (opt not in oldconf and curconf.get(opt))]
            if len(opt_adds) > 0:
                curchanges += "\n\t" + "\n\t".join([_("added %s <b>%s</b> (%s)") % (change_type, param, curconf.get(param)) for param in opt_adds])
                num_changes += len(opt_adds)
            # removed options
            opt_dels = [opt for opt in oldconf if ((opt not in curconf.list_options() or curconf.get(opt) == None) and oldconf.get(opt))]
            if len(opt_dels) > 0:
                curchanges += "\n\t" + "\n\t".join([_("removed %s <b>%s</b>") % (change_type, param) for param in opt_dels])
                num_changes += len(opt_dels)
            # adding labels
            if curchanges == "":
                curchanges = _("no changes")
            # store the current changes
            changes.append((name, curchanges))
        # return what we found
        return num_changes, changes, messages

    def ok(self, widget, ask_ignore=False):
        """Ok button"""
        curconfig = self.msecconfig
        curperms = self.permconfig

        # creating preview window
        dialog = Gtk.Dialog(title=_('Saving changes...'), transient_for=self.window, modal=True)
        dialog.add_button(TEXT_CANCEL, Gtk.ResponseType.CANCEL)
        if ask_ignore:
            dialog.add_button(_('Ignore and quit'), Gtk.ResponseType.REJECT)
        button_force = dialog.add_button(_('Force permissions'), Gtk.ResponseType.APPLY)
        button_force.set_tooltip_text(_("Saves the configuration changes, then reapplies all permission settings."))
        button_save = dialog.add_button(TEXT_OK, Gtk.ResponseType.OK)
        button_save.set_tooltip_text(_("Saves the configuration changes. Restart the msec service to apply all changes."))
        dialog.set_default_size(640, 300)
        dialog.set_default_response(Gtk.ResponseType.OK)

        label = Gtk.Label(label=SAVE_SETTINGS_TEXT)
        dialog.vbox.set_spacing(DEFAULT_SPACING)
        dialog.vbox.pack_start(label, False, False, padding=DEFAULT_SPACING)

        dialog.set_resizable(False)

        # detailed information
        exp_vbox = VBox()

        # scrolledwindow
        sw = Gtk.ScrolledWindow()
        sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        exp_vbox.pack_start(sw, True, True, DEFAULT_SPACING)

        vbox = VBox()
        exp_vbox.set_size_request(640, 280)
        sw.add(vbox)

        # were there changes in configuration?
        num_changes, allchanges, messages = self.check_for_changes(curconfig, curperms)

        # TODO: FIX
        for name, changes in allchanges:
            label = Gtk.Label(label=_('<b>%s:</b> <i>%s</i>\n') % (name, changes))
            label.set_use_markup(True)
            label.set_property("xalign", 0.0)
            vbox.pack_start(label, False, False, padding=DEFAULT_SPACING)
        # see if there were any changes to system files
        for msg in messages['info']:
            if msg.find(config.MODIFICATIONS_FOUND) != -1 or msg.find(config.MODIFICATIONS_NOT_FOUND) != -1:
                label = Gtk.Label(label=_("<b>MSEC test run results:</b> <i>%s</i>") % msg)
                label.set_line_wrap(True)
                label.set_use_markup(True)
                label.set_property("xalign", 0.0)
                vbox.pack_start(label, False, False, padding=DEFAULT_SPACING)
                break

        # adding specific messages
        advanced = Gtk.Expander(label=_("Details"))
        vbox_advanced = VBox()
        advanced.add(vbox_advanced)
        vbox.pack_start(advanced, False, False, padding=DEFAULT_SPACING)
        for cat in ['info', 'critical', 'error', 'warn', 'debug']:
            msgs = messages[cat]
            expander = Gtk.Expander(label=_('MSEC messages (%s): %d') % (cat, len(msgs)))
            textview = Gtk.TextView()
            textview.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
            textview.set_editable(False)
            expander.add(textview)
            text_buffer = textview.get_buffer()
            for count, msg in enumerate(msgs, start=1):
                end = text_buffer.get_end_iter()
                text_buffer.insert(end, "%d: %s\n" % (count, msg))
            vbox_advanced.pack_start(expander, False, False, padding=DEFAULT_SPACING)

        # hide all information in an expander
        expander = Gtk.Expander(label=_("Details (%d changes)..") % num_changes)
        expander.add(exp_vbox)
        dialog.vbox.pack_start(expander, False, False, padding=DEFAULT_SPACING)

        dialog.show_all()
        response = dialog.run()
        if response not in [Gtk.ResponseType.OK, Gtk.ResponseType.APPLY]:
            dialog.destroy()
            return response
        dialog.destroy()

        # new base level
        self.msecconfig.set("BASE_LEVEL", self.base_level)
        levelconf = config.load_defaults(log, self.base_level)
        standard_permconf = config.load_default_perms(log, self.base_level)

        # saving the configuration
        self.msecconfig.save(levelconf)
        self.msec.apply(self.msecconfig)
        self.msec.commit(True)

        # merge the new base level's permissions
        self.permconfig.merge(standard_permconf, overwrite=True)
        # check and save all
        self.perms.check_perms(self.permconfig)
        self.permconfig.save(None)

        if response == Gtk.ResponseType.APPLY:
            # enforce file permissions right away
            self.perms.commit(really_commit=True, enforce=True)
        elif response == Gtk.ResponseType.OK:
            self.perms.commit(really_commit=True, enforce=False)

        self.reload_config()

        return response

    def reload_config(self):
        """Reloads config files"""
        # msecconfig
        self.msecconfig.reset()
        self.msecconfig.load()
        config.merge_with_baselevel(log, self.msecconfig, self.msecconfig.get_base_level(), config.load_defaults, root='')
        # permconfig
        self.permconfig.reset()
        self.permconfig.load()
        config.merge_with_baselevel(log, self.permconfig, self.msecconfig.get_base_level(), config.load_default_perms, root='')
        # merge legacy perm.local if present
        self.permlocal = config.PermConfig(self.log, config=config.PERM_LOCAL)
        if os.access(config.PERM_LOCAL, os.R_OK):
            self.permlocal.load()
            self.permconfig.merge(self.permlocal, overwrite=True)
        # exceptions
        self.exceptions.reset()
        self.exceptions.load()
        # saving old config
        self.oldconfig = {}
        for opt in self.msecconfig.list_options():
            self.oldconfig[opt] = self.msecconfig.get(opt)
        # permissions
        self.oldperms = {}
        for opt in self.permconfig.list_options():
            self.oldperms[opt] = self.permconfig.get(opt)

        # what level are we?
        level = self.msecconfig.get("BASE_LEVEL")
        if not level:
            self.log.info(_("No base msec level specified, using '%s'") % config.STANDARD_LEVEL)
            self.base_level = config.STANDARD_LEVEL
        else:
            self.log.info(_("Detected base msec level '%s'") % level)
            self.base_level = level

    def create_treeview(self, options):
        """Creates a treeview from given list of options"""
        sw = Gtk.ScrolledWindow()
        sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)

        # list of options
        lstore = Gtk.ListStore(
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_INT)

        # treeview
        treeview = Gtk.TreeView(model=lstore)
        treeview.set_search_column(self.COLUMN_DESCR)

        treeview.connect('row-activated', self.option_changed, lstore)

        # configuring columns

        # column for option names
        renderer = Gtk.CellRendererText()
        renderer.set_property('width', 200)
        column = Gtk.TreeViewColumn(_('Security Option'), renderer, text=self.COLUMN_OPTION, weight=self.COLUMN_CUSTOM)
        column.set_sort_column_id(self.COLUMN_OPTION)
        column.set_resizable(True)
        column.set_expand(True)
        treeview.append_column(column)

        # column for descriptions
        renderer = Gtk.CellRendererText()
        renderer.set_property('wrap-width', 400)
        renderer.set_property('wrap-mode', Pango.WrapMode.WORD_CHAR)
        column = Gtk.TreeViewColumn(_('Description'), renderer, text=self.COLUMN_DESCR, weight=self.COLUMN_CUSTOM)
        treeview.append_column(column)
        column.set_expand(True)
        #treeview.append_column(column)

        # column for values
        column = Gtk.TreeViewColumn(_('Value'), Gtk.CellRendererText(), text=self.COLUMN_VALUE, weight=self.COLUMN_CUSTOM)
        column.set_sort_column_id(self.COLUMN_VALUE)
        treeview.append_column(column)

        sw.add(treeview)

        for option in options:
            # retreiving option description
            if option not in config.SETTINGS:
                # invalid option
                self.log.error(_("Invalid option '%s'!") % option)
                continue
            # getting level settings, callback and valid params
            callback, params = config.SETTINGS[option]

            # now for the value
            value = self.msecconfig.get(option)
            # check for disabled options
            if not value:
                value = config.OPTION_DISABLED

            # description
            doc = config.find_doc(self.msec, option, self.descriptions)

            # was it changed? if yes, change description to italic
            if self.option_is_changed(option, value):
                custom = Pango.Weight.BOLD
            else:
                custom = Pango.Weight.NORMAL

            # building the option
            iter = lstore.append()
            lstore.set(iter,
                    self.COLUMN_OPTION, option,
                    self.COLUMN_DESCR, doc,
                    self.COLUMN_VALUE, value,
                    self.COLUMN_CUSTOM, custom,
                    )
        return sw, lstore

    def option_is_changed(self, option, value, level=None):
        """Checks if the option is different from one specified by base level"""
        if not level:
            level = self.base_level
        conf = self.msec_defaults[level]
        if conf.get(option) != value:
            # it was changed
            return True
        else:
            return False

    def create_summary_ui(self):
        """Builds the security summary UI"""
        vbox = VBox(spacing=20)

        grid = Gtk.Grid()
        grid.set_column_spacing(10)
        grid.set_row_spacing(6)

        def make_status_label(text):
            label = Gtk.Label(label=text)
            label.set_property("xalign", 0.0)
            label.set_property("yalign", 0.5)
            label.set_hexpand(True)
            label.set_halign(Gtk.Align.FILL)
            return label

        def create_security_item(grid, row, text, icon=None):
            # show logo
            if icon:
                try:
                    # logo
                    image = Gtk.Image()
                    # use a fixed size for icons to avoid oveload
                    pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(icon, 32, 32)
                    image.set_from_pixbuf(pixbuf)
                    image.set_hexpand(False)
                    grid.attach(image, 0, row, 1, 1)
                except Exception as e:
                    print("Unable to load icon %s: %s" % (icon, e))

            grid.attach(make_status_label(text), 1, row, 1, 1)

        row = 0
        # firewall
        create_security_item(grid, row, _("Firewall"), "/usr/share/mcc/themes/default/firewall-mdk.png")
        grid.attach(make_status_label(tools.find_firewall_info(log)), 2, row, 1, 1)

        button = Gtk.Button(label=_("Configure"))
        button.connect('clicked', self.run_configure_app, tools.FIREWALL_CMD)
        grid.attach(button, 3, row, 1, 1)

        row += 1

        # updates
        create_security_item(grid, row, _("Updates"), "/usr/share/mcc/themes/default/MageiaUpdate.png")
        grid.attach(make_status_label(tools.get_updates_status(log)), 2, row, 1, 1)
        button = Gtk.Button(label=_("Update now"))
        button.connect('clicked', self.run_configure_app, tools.UPDATE_CMD)
        grid.attach(button, 3, row, 1, 1)

        row += 1

        # security
        create_security_item(grid, row, _("Security"), "/usr/share/mcc/themes/default/security-mdk.png")
        baselevel = self.msecconfig.get("BASE_LEVEL")
        if baselevel == config.NONE_LEVEL:
            msec_status = [_("Msec is disabled")]
        else:
            msec_status = [_("Msec is enabled"),
                           _("Base security level: '%s'") % baselevel
            ]
            # find out custom settings
            custom_count = 0
            base_config = self.msec_defaults[baselevel]
            for o in self.msecconfig.list_options():
                if self.msecconfig.get(o) != base_config.get(o):
                    custom_count += 1
            if custom_count > 0:
                msec_status.append(_("Custom settings: %d") % custom_count)

        grid.attach(make_status_label("\n".join(msec_status)), 2, row, 1, 1)
        button = Gtk.Button(label=_("Configure"))
        button.connect('clicked', lambda x: self.main_notebook.set_current_page(1))
        grid.attach(button, 3, row, 1, 1)

        row += 1

        # msec reports
        label = Gtk.Label(label=_("Periodic checks"))
        label.set_property("xalign", 0.0)
        label.set_property("yalign", 0.5)
        grid.attach(label, 1, row, 1, 1)

        row += 1

        for check, logfile, updated_n, updated in tools.periodic_check_status(log):
            if not updated:
                updated = _("Never")
            label = Gtk.Label(label=_("Check: %s. Last run: %s") % (check, updated))
            label.set_property("xalign", 0.0)
            label.set_hexpand(True)
            label.set_halign(Gtk.Align.FILL)
            grid.attach(label, 2, row, 1, 1)

            h = HBox(spacing=6)
            button_show = Gtk.Button(label=_("Show results"))
            if updated_n:
                button_show.connect('clicked', self.show_test_results, logfile)
            else:
                button_show.set_sensitive(False)
            h.pack_start(button_show, False, False, 0)
            button_run = Gtk.Button(label=_("Run now"))
            button_run.connect('clicked', self.run_periodic_check, check)
            h.pack_start(button_run, False, False, 0)
            grid.attach(h, 3, row, 1, 1)
            row += 1

        vbox.pack_start(grid, False, False, 0)
        return vbox

    def process_events(self):
        """Process pending gtk events"""
        while Gtk.events_pending():
            Gtk.main_iteration()

    def show_test_results(self, widget, logfile):
        """Shows results of last periodic check"""
        # first, try to read the file
        data = []
        try:
            with open(logfile, "r") as fd:
                data = fd.readlines()
        except OSError as e:
            data = [_("Unable to read log file: %s") % e]

        dialog = Gtk.Dialog(title=_("Periodic check results"),
                            transient_for=self.window, modal=True)
        dialog.add_button(TEXT_OK, Gtk.ResponseType.OK)
        dialog.set_size_request(640, 280)

        text_buffer = Gtk.TextBuffer()
        text_buffer.create_tag("monospace", family="monospace", editable=False)
        text_iter = text_buffer.get_iter_at_offset(0)
        for l in data:
            text_buffer.insert_with_tags_by_name(text_iter, l, "monospace")
        view = Gtk.TextView.new_with_buffer(text_buffer)
        view.set_editable(False)

        sw = Gtk.ScrolledWindow()
        sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        sw.add(view)

        dialog.vbox.pack_start(sw, True, True, 0)
        dialog.show_all()
        dialog.run()
        dialog.destroy()

    def run_periodic_check(self, widget, check):
        """Shows results for the test"""
        dialog = Gtk.MessageDialog(
                transient_for=self.window,
                modal=True,
                message_type=Gtk.MessageType.INFO,
                buttons=Gtk.ButtonsType.YES_NO)
        dialog.set_markup(_("Do you want to run the <b>%s</b> periodic check? Please note that it could take a considerable time to finish.") % check)
        dialog.show_all()
        ret = dialog.run()
        dialog.destroy()
        if ret != Gtk.ResponseType.YES:
            return

        # progress bar
        progress = Gtk.Window()
        progress.set_title(_("Please wait, running checks..."))
        progress.set_transient_for(self.window)
        progress.set_modal(True)
        progress.connect('delete-event', lambda *w: None)

        vbox = VBox(spacing=10)
        progress.add(vbox)
        progressbar = Gtk.ProgressBar()
        progressbar.set_text(_("Please wait, running checks..."))
        vbox.pack_start(progressbar, True, True, 0)

        label = Gtk.Label(label=_("Please wait, this might take a few minutes."))
        vbox.pack_start(label, True, True, 0)

        # show window
        progress.show_all()

        self.process_events()
        # queue to signal that job is finished
        q = queue.Queue()

        if check == "manual":
            program = os.path.join(config.MSEC_DIR, "security.sh")
        else:
            program = "/etc/cron.%s/msec" % check
        installer = BackgroundRunner(finish=q, program=program)
        installer.start()

        while 1:
            self.process_events()
            if not q.empty():
                result = q.get()
                break
            else:
                progressbar.pulse()
                time.sleep(0.5)

        progress.destroy()

        if result == 0:
            text = _("Periodic check was executed successfully!")
            msg_type = Gtk.MessageType.INFO
        else:
            text = _("An error occurred while running periodic check.")
            msg_type = Gtk.MessageType.ERROR
        # policy was initialized
        dialog = Gtk.MessageDialog(
                transient_for=self.window,
                modal=True,
                message_type=msg_type,
                text=text,
                buttons=Gtk.ButtonsType.OK
                )
        dialog.show_all()
        dialog.run()
        dialog.destroy()

    def run_configure_app(self, widget, cmd):
        """Runs application-specific configuration"""
        os.system(cmd)
        self.reload_summary()

    def reload_summary(self):
        """Reloads summary tab"""
        pass

    def level_security_page(self, id):
        """Builds the basic security page"""
        vbox = VBox()

        entry = Gtk.Label(label=LEVEL_SECURITY_TEXT)
        entry.set_use_markup(True)
        vbox.pack_start(entry, False, False, 0)

        # none
        self.msec_enabled = Gtk.CheckButton(label=_("Enable MSEC tool"))
        if self.base_level != config.NONE_LEVEL:
            self.msec_enabled.set_active(True)
        self.msec_enabled.connect('clicked', self.enable_disable_msec)
        vbox.pack_start(self.msec_enabled, False, False, 0)

        # security levels
        self.levels_frame = Gtk.Frame.new(_("Select the base security level"))
        levels_vbox = VBox()
        self.levels_frame.add(levels_vbox)
        # create the security level selection screen
        sw = Gtk.ScrolledWindow()
        sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)

        # list of levels
        lstore = Gtk.ListStore(
                GObject.TYPE_STRING,
                GObject.TYPE_STRING,
                GObject.TYPE_INT)

        # treeview
        treeview = Gtk.TreeView(model=lstore)
        treeview.set_search_column(self.COLUMN_LEVEL_DESCR)
        treeview.connect('row-activated', self.level_changed, lstore)

        # columns
        # column for level names
        renderer = Gtk.CellRendererText()
        column = Gtk.TreeViewColumn(_('Level name'), renderer, text=self.COLUMN_OPTION, weight=self.COLUMN_LEVEL_CURRENT)
        column.set_sort_column_id(self.COLUMN_LEVEL)
        column.set_resizable(True)
        column.set_expand(False)
        treeview.append_column(column)

        # column for descriptions
        renderer = Gtk.CellRendererText()
        renderer.set_property('wrap-width', 600)
        renderer.set_property('wrap-mode', Pango.WrapMode.WORD_CHAR)
        column = Gtk.TreeViewColumn(_('Description'), renderer, text=self.COLUMN_DESCR, weight=self.COLUMN_LEVEL_CURRENT)
        treeview.append_column(column)
        column.set_expand(True)
        #treeview.append_column(column)

        sw.add(treeview)

        # first, add levels from level_order
        levels = []
        for level in level_order:
            if level in self.msec_defaults:
                levels.append(level)
        # then, add all other levels
        for level in self.msec_defaults:
            if level not in levels:
                levels.append(level)

        # now build the gui
        for level in levels:
            # skip NONE level, as it disables msec
            if level == "none":
                continue
            if level in level_descriptions:
                descr = level_descriptions[level]
            else:
                descr = DEFAULT_LEVEL_DESCRIPTION
            # TODO: mark current level as bold
            iter = lstore.append()
            if self.base_level == level:
                weight = Pango.Weight.BOLD
            else:
                weight = Pango.Weight.NORMAL
            lstore.set(iter,
                    self.COLUMN_LEVEL, level,
                    self.COLUMN_LEVEL_DESCR, descr,
                    self.COLUMN_LEVEL_CURRENT, weight)

        levels_vbox.pack_start(sw, True, True, 0)
        vbox.pack_start(self.levels_frame, True, True, 0)

        # save the list of levels
        self.level_list = lstore

        # putting levels to vbox

        # notifications by email
        hbox = HBox()
        self.notify_mail = Gtk.CheckButton(label=_("Send security alerts by email to:"))
        if self.msecconfig.get("MAIL_WARN") == "yes":
            self.notify_mail.set_active(True)
        hbox.pack_start(self.notify_mail, False, False, 0)

        # email address
        self.email_entry = Gtk.Entry()
        email = self.msecconfig.get("MAIL_USER")
        if not email:
            email = ""
        self.email_entry.set_text(email)
        self.email_entry.connect('changed', self.change_email)
        hbox.pack_start(self.email_entry, False, False, 5)
        vbox.pack_start(hbox, False, False, 0)

        # updating the mail address/checkbox relationship
        self.notify_mail.connect('clicked', self.notify_mail_changed, self.email_entry)
        self.checkboxes_callbacks["MAIL_WARN"] = (self.notify_mail_changed, self.notify_mail, hbox)

        self.notify_mail_changed(self.notify_mail, hbox)

        # notifications on desktop
        self.notify_desktop = Gtk.CheckButton(label=_("Display security alerts on desktop"))
        if self.msecconfig.get("NOTIFY_WARN") == "yes":
            self.notify_desktop.set_active(True)
        self.notify_desktop.connect('clicked', self.notify_changed, None)
        vbox.pack_start(self.notify_desktop, False, False, 0)
        self.checkboxes_callbacks["NOTIFY_WARN"] = (self.notify_changed, self.notify_desktop, None)

        return vbox

    def change_email(self, widget):
        """Email address was changed"""
        email = widget.get_text()
        self.msecconfig.set("MAIL_USER", email)

    def notify_mail_changed(self, widget, param):
        """Changes to mail notifications"""
        status = widget.get_active()
        if status:
            self.msecconfig.set("MAIL_WARN", "yes")
            param.set_sensitive(True)
        else:
            if self.msecconfig.get("MAIL_WARN"):
                self.msecconfig.set("MAIL_WARN", "no")
            param.set_sensitive(False)

    def notify_changed(self, widget, param):
        """Changes to mail notifications"""
        status = widget.get_active()
        if status:
            self.msecconfig.set("NOTIFY_WARN", "yes")
        else:
            self.msecconfig.set("NOTIFY_WARN", "no")

    def enable_disable_msec(self, widget):
        """Enables/disables msec tool"""
        newstatus = widget.get_active()
        if newstatus == False:
            self.toggle_level(config.NONE_LEVEL, force=True)
        else:
            # revert back to the selected level or switch to 'Standard' if none
            level = config.STANDARD_LEVEL
            iter = self.level_list.get_iter_first()
            while iter:
                list_level = self.level_list.get_value(iter, self.COLUMN_LEVEL)
                list_weight = self.level_list.get_value(iter, self.COLUMN_LEVEL_CURRENT)
                if list_weight == Pango.Weight.BOLD:
                    # found previous level
                    level = list_level
                    break
                iter = self.level_list.iter_next(iter)
            self.toggle_level(level, force=True)


    def toggle_level(self, level, force=False):
        """Enables/disables graphical items for msec"""

        if level != config.NONE_LEVEL:
            enabled = True
        else:
            enabled = False

        # update notebook pages
        npages = self.notebook.get_n_pages()
        self.levels_frame.set_sensitive(enabled)
        for page in range(1, npages):
            curpage = self.notebook.get_nth_page(page)
            curpage.set_sensitive(enabled)
            label = self.notebook.get_tab_label(curpage)
            label.set_sensitive(enabled)

        if level == self.base_level:
            # Not changing anything
            return

        # updating the markup of new current level unless switching to 'None' level
        # in this case, we'll still use current level in case user decides to switch back later
        if level != config.NONE_LEVEL:
            iter = self.level_list.get_iter_first()
            while iter:
                list_level = self.level_list.get_value(iter, self.COLUMN_LEVEL)
                if list_level != level:
                    # not current level, changing font weight
                    self.level_list.set(iter,
                            self.COLUMN_LEVEL_CURRENT, Pango.Weight.NORMAL)
                else:
                    # updating current level
                    self.level_list.set(iter,
                            self.COLUMN_LEVEL_CURRENT, Pango.Weight.BOLD)
                iter = self.level_list.iter_next(iter)

        # what is the current level?
        defconfig = self.msec_defaults[level]

        for z in self.current_options_view:
            options, curconfig = self.current_options_view[z]
            iter = options.get_iter_first()
            # what options are we changing
            if curconfig.__class__ == config.MsecConfig:
                # main msec options
                while iter:
                    option = options.get_value(iter, self.COLUMN_OPTION)
                    curvalue = options.get_value(iter, self.COLUMN_VALUE)
                    newvalue = defconfig.get(option)
                    # changing option
                    if force:
                        if curvalue != newvalue:
                            self.log.debug("%s: %s -> %s" % (option, curvalue, newvalue))
                            options.set(iter, self.COLUMN_VALUE, newvalue)
                            # reset customization
                            curconfig.set(option, newvalue)
                    # set option as normal
                    options.set(iter, self.COLUMN_CUSTOM, Pango.Weight.NORMAL)
                    ## skip custom options
                    #print "Base level: %s" % self.base_level
                    #if self.option_is_changed(option, curvalue):
                    #    # custom option
                    #    print "Custom option detected: %s" % option
                    iter = options.iter_next(iter)
            elif curconfig.__class__ == config.PermConfig:
                self.reset_permissions(None, options, level=level)
            else:
                #print curconfig.__class__
                pass
        # checkboxes
        for option in self.checkboxes_callbacks:
            if force:
                func, widget, callback = self.checkboxes_callbacks[option]
                if defconfig.get(option) == "yes":
                    widget.set_active(True)
                else:
                    widget.set_active(False)
                self.msecconfig.set(option, defconfig.get(option))
                func(widget, callback)
        # finally, change new base_level
        self.base_level = level

    def force_level(self, widget, level):
        """Defines a given security level"""
        if widget.get_active():
            #self.base_level = level
            # update everything
            self.toggle_level(level, force=True)

    def system_security_page(self, id):
        """Builds the system security page"""
        vbox = VBox()

        entry = Gtk.Label(label=SYSTEM_SECURITY_TEXT)
        entry.set_use_markup(True)
        vbox.pack_start(entry, False, False, 0)

        # system security options
        options_view, model = self.create_treeview(config.SETTINGS_SYSTEM)
        self.current_options_view[id] = (model, self.msecconfig)
        vbox.pack_start(options_view, True, True, 0)

        return vbox

    def network_security_page(self, id):
        """Builds the network security page"""
        vbox = VBox()

        entry = Gtk.Label(label=NETWORK_SECURITY_TEXT)
        entry.set_use_markup(True)
        vbox.pack_start(entry, False, False, 0)

        # network security options
        options_view, model = self.create_treeview(config.SETTINGS_NETWORK)
        self.current_options_view[id] = (model, self.msecconfig)
        vbox.pack_start(options_view, True, True, 0)

        return vbox

    def periodic_security_page(self, id):
        """Builds the periodic security page"""
        vbox = VBox()

        entry = Gtk.Label(label=PERIODIC_SECURITY_TEXT)
        entry.set_use_markup(True)
        vbox.pack_start(entry, False, False, 0)

        periodic_checks = self.msecconfig.get("CHECK_SECURITY")

        self.periodic_checks = Gtk.CheckButton(label=_("Enable periodic security checks"))
        if periodic_checks == "yes":
            self.periodic_checks.set_active(True)
        vbox.pack_start(self.periodic_checks, False, False, 0)

        # network security options
        options_view, model = self.create_treeview(config.SETTINGS_PERIODIC)
        vbox.pack_start(options_view, True, True, 0)

        # see if these tests are enabled
        self.periodic_checks.connect('clicked', self.periodic_tests, options_view)
        if periodic_checks == 'no':
            # disable all periodic tests
            options_view.set_sensitive(False)

        # save options
        self.current_options_view[id] = (model, self.msecconfig)

        # save the checkboxes
        self.checkboxes_callbacks["CHECK_SECURITY"] = (self.periodic_tests, self.periodic_checks, options_view)

        return vbox

    def periodic_tests(self, widget, options):
        '''Enables/disables periodic security tests.'''
        status = widget.get_active()
        if status:
            self.msecconfig.set("CHECK_SECURITY", "yes")
            options.set_sensitive(True)
        else:
            self.msecconfig.set("CHECK_SECURITY", "no")
            options.set_sensitive(False)

    def exceptions_page(self, id):
        """Builds the exceptions page"""
        vbox = VBox()

        entry = Gtk.Label(label=EXCEPTIONS_TEXT)
        entry.set_use_markup(True)
        vbox.pack_start(entry, False, False, 0)

        sw = Gtk.ScrolledWindow()
        sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)

        # list of options
        lstore = Gtk.ListStore(
            GObject.TYPE_STRING,
            GObject.TYPE_STRING
            )

        # treeview
        treeview = Gtk.TreeView(model=lstore)
        treeview.set_search_column(self.COLUMN_EXCEPTION)

        # TODO: fix
        treeview.connect('row-activated', self.exception_changed, lstore)

        # configuring columns

        # column for exception position
        column = Gtk.TreeViewColumn(_('Security check'), Gtk.CellRendererText(), text=self.COLUMN_EXCEPTION)
        column.set_sort_column_id(self.COLUMN_EXCEPTION)
        column.set_expand(True)
        treeview.append_column(column)

        # column for check exception
        column = Gtk.TreeViewColumn(_('Exception'), Gtk.CellRendererText(), text=self.COLUMN_EXCEPTION_VALUE)
        column.set_sort_column_id(self.COLUMN_EXCEPTION_VALUE)
        column.set_expand(True)
        treeview.append_column(column)

        sw.add(treeview)

        for option, value in self.exceptions.list_options():
            # building the option
            iter = lstore.append()
            lstore.set(iter,
                    self.COLUMN_EXCEPTION, option,
                    self.COLUMN_EXCEPTION_VALUE, value,
                    )
        vbox.pack_start(sw, True, True, 0)
        self.current_options_view[id] = (lstore, self.exceptions)

        # buttons hbox
        hbox = HBox(homogeneous=True, spacing=10)

        # add
        button = Gtk.Button(label=_("Add a rule"))
        button.connect('clicked', self.add_exception, lstore)
        hbox.pack_start(button, False, False, 0)

        # delete
        button = Gtk.Button(label=_("Delete"))
        button.connect('clicked', self.remove_exception, treeview)
        hbox.pack_start(button, False, False, 0)

        vbox.pack_start(hbox, False, False, 0)

        return vbox

    def permissions_security_page(self, id):
        """Builds the permissions security page"""
        vbox = VBox()

        entry = Gtk.Label(label=PERMISSIONS_SECURITY_TEXT)
        entry.set_use_markup(True)
        vbox.pack_start(entry, False, False, 0)

        sw = Gtk.ScrolledWindow()
        sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)

        # list of options
        lstore = Gtk.ListStore(
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_BOOLEAN,
            GObject.TYPE_STRING)

        # treeview
        treeview = Gtk.TreeView(model=lstore)
        treeview.set_search_column(self.COLUMN_DESCR)

        # TODO: fix
        treeview.connect('row-activated', self.permission_changed, lstore)

        # configuring columns

        # column for path mask
        column = Gtk.TreeViewColumn(_('Path'), Gtk.CellRendererText(), text=self.COLUMN_PATH)
        column.set_sort_column_id(self.COLUMN_PATH)
        column.set_expand(True)
        treeview.append_column(column)

        # column for user
        column = Gtk.TreeViewColumn(_('User'), Gtk.CellRendererText(), text=self.COLUMN_USER)
        column.set_sort_column_id(self.COLUMN_USER)
        column.set_expand(True)
        treeview.append_column(column)

        # column for group
        column = Gtk.TreeViewColumn(_('Group'), Gtk.CellRendererText(), text=self.COLUMN_GROUP)
        column.set_sort_column_id(self.COLUMN_GROUP)
        column.set_expand(True)
        treeview.append_column(column)

        # column for permissions
        column = Gtk.TreeViewColumn(_('Permissions'), Gtk.CellRendererText(), text=self.COLUMN_PERM)
        column.set_sort_column_id(self.COLUMN_VALUE)
        column.set_expand(True)
        treeview.append_column(column)

        # column for force option
        renderer = Gtk.CellRendererToggle()
        renderer.connect('toggled', self.toggle_enforced, lstore)
        column = Gtk.TreeViewColumn(_('Enforce'), renderer, active=self.COLUMN_FORCE)
        column.set_sort_column_id(self.COLUMN_FORCE)
        column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
        column.set_fixed_width(50)
        column.set_expand(True)
        treeview.append_column(column)

        # column for Acl
        column = Gtk.TreeViewColumn(_('Acl'), Gtk.CellRendererText(), text=self.COLUMN_ACL)
        column.set_sort_column_id(self.COLUMN_ACL)
        column.set_expand(True)
        treeview.append_column(column)

        sw.add(treeview)

        for file in self.permconfig.list_options():
            user_s, group_s, perm_s, force, acl = self.permconfig.get(file)

            # convert to boolean
            force = bool(force)

            # building the option
            iter = lstore.append()
            lstore.set(iter,
                    self.COLUMN_PATH, file,
                    self.COLUMN_USER, user_s,
                    self.COLUMN_GROUP, group_s,
                    self.COLUMN_PERM, perm_s,
                    self.COLUMN_FORCE, force,
                    self.COLUMN_ACL, acl,
                    )
        vbox.pack_start(sw, True, True, 0)
        self.current_options_view[id] = (lstore, self.permconfig)

        # buttons hbox
        hbox = HBox(homogeneous=True, spacing=10)

        # # up
        # button = Gtk.Button(_("Up"))
        # button.connect('clicked', self.move_rule_up, lstore)
        # hbox.pack_start(button, False, 0)

        # # down
        # button = Gtk.Button(_("Down"))
        # button.connect('clicked', self.move_rule_up, lstore)
        # hbox.pack_start(button, False, 0)

        # # default
        # button = Gtk.Button(_("Reset to default level permissions"))
        # button.connect('clicked', self.reset_permissions, lstore)
        # hbox.pack_start(button, False, 0)

        # add
        button = Gtk.Button(label=_("Add a rule"))
        button.connect('clicked', self.add_permission_check, lstore)
        hbox.pack_start(button, False, False, 0)

        # delete
        button = Gtk.Button(label=_("Delete"))
        button.connect('clicked', self.remove_permission_check, treeview)
        hbox.pack_start(button, False, False, 0)

        ## edit
        #button = Gtk.Button(_("Edit"))
        #button.connect('clicked', self.move_rule_up, lstore)
        #hbox.pack_start(button, False, 0)

        vbox.pack_start(hbox, False, False, 0)

        return vbox

    def reset_permissions(self, widget, model, level=None):
        """Reset permissions to default specified by level"""
        model.clear()
        self.permconfig.reset()
        if not level:
            defperms = self.perm_defaults[self.base_level]
        else:
            defperms = self.perm_defaults[level]
        for file in defperms.list_options():
            user_s, group_s, perm_s, force_s, acls = defperms.get(file)

            # convert to boolean
            force_val = bool(force_s)

            # building the option
            iter = model.append()
            model.set(iter,
                    self.COLUMN_PATH, file,
                    self.COLUMN_USER, user_s,
                    self.COLUMN_GROUP, group_s,
                    self.COLUMN_PERM, perm_s,
                    self.COLUMN_FORCE, force_val,
                    self.COLUMN_ACL, acls,
                    )
            # changing back force value
            self.permconfig.set(file, (user_s, group_s, perm_s, force_s, acls))

        # overlay perm.local entries on top of level defaults
        if os.access(config.PERM_LOCAL, os.R_OK):
            self.permlocal = config.PermConfig(self.log, config=config.PERM_LOCAL)
            self.permlocal.load()
            for file in self.permlocal.list_options():
                user_s, group_s, perm_s, force_s, acls = self.permlocal.get(file)
                force_val = bool(force_s)
                self.permconfig.set(file, (user_s, group_s, perm_s, force_s, acls))
                # update or append in the model
                found = False
                iter = model.get_iter_first()
                while iter:
                    if model.get_value(iter, self.COLUMN_PATH) == file:
                        model.set(iter,
                                self.COLUMN_USER, user_s,
                                self.COLUMN_GROUP, group_s,
                                self.COLUMN_PERM, perm_s,
                                self.COLUMN_FORCE, force_val,
                                self.COLUMN_ACL, acls,
                                )
                        found = True
                        break
                    iter = model.iter_next(iter)
                if not found:
                    iter = model.append()
                    model.set(iter,
                            self.COLUMN_PATH, file,
                            self.COLUMN_USER, user_s,
                            self.COLUMN_GROUP, group_s,
                            self.COLUMN_PERM, perm_s,
                            self.COLUMN_FORCE, force_val,
                            self.COLUMN_ACL, acls,
                            )

    def remove_exception(self, widget, treeview):
        """Removes an exception from list"""
        model, iter = treeview.get_selection().get_selected()
        if not iter:
            # nothing selected
            return
        pos, = model.get_path(iter)
        self.exceptions.remove(pos)
        model.remove(iter)

        # save exceptions
        self.exceptions.save()

    def remove_permission_check(self, widget, treeview):
        """Removes a permission check for file"""
        model, iter = treeview.get_selection().get_selected()
        if not iter:
            # nothing selected
            return
        file = model.get_value(iter, self.COLUMN_PATH)
        self.permconfig.remove(file)
        model.remove(iter)

    def toggle_enforced(self, cell, path, model):
        '''Toggles a forced permission on an item'''
        iter = model.get_iter((int(path),))
        file = model.get_value(iter, self.COLUMN_PATH)
        fixed = model.get_value(iter, self.COLUMN_FORCE)

        user, group, perm, force, acl = self.permconfig.get(file)

        # do something with the value
        fixed = not fixed

        # set new value
        model.set(iter, self.COLUMN_FORCE, fixed)
        if fixed:
            force = "force"
        else:
            force = ""
        self.permconfig.set(file, (user, group, perm, force, acl))
        # save to enable reload_config() to load the changes
        standard_permconf = config.load_default_perms(log, self.base_level)
        self.permconfig.save(standard_permconf)

    def add_permission_check(self, widget, model):
        """Adds a permission check"""
        return self.permission_changed(None, None, None, model)

    def add_exception(self, widget, model):
        """Adds a new exception"""
        return self.exception_changed(None, None, None, model)

    def exception_changed(self, treeview, path, col, model):
        """Processes an exception change. If path is None, adds a new item."""
        if path:
            iter = model.get_iter(path)
            exception_pos, = path
            module = model.get_value(iter, self.COLUMN_EXCEPTION)
            exception = model.get_value(iter, self.COLUMN_EXCEPTION_VALUE)
            title = _("Editing exception")
        else:
            exception_pos = -1
            module = ""
            exception = ""
            title = _("Adding new exception")

        # asks for new parameter value
        dialog = Gtk.Dialog(title=title, transient_for=self.window, modal=True)
        dialog.add_button(TEXT_OK, Gtk.ResponseType.OK)
        dialog.add_button(TEXT_CANCEL, Gtk.ResponseType.CANCEL)
        label = Gtk.Label(label=_("Editing exception. Please select the correspondent msec check and exception value\n"))
        label.set_line_wrap(True)
        label.set_use_markup(True)
        dialog.vbox.pack_start(label, False, False, 0)

        # module
        hbox = HBox()
        hbox.pack_start(Gtk.Label(label=_("Check: ")), True, True, 0)
        entry_module = Gtk.ComboBoxText()
        pos = 0
        for item in config.CHECKS_WITH_EXCEPTIONS:
            entry_module.append_text(item)
            if item == module:
                entry_module.set_active(pos)
            pos += 1
        if not module:
            entry_module.set_active(0)
        hbox.pack_start(entry_module, True, True, 0)
        dialog.vbox.pack_start(hbox, False, False, 0)

        # exception
        hbox = HBox()
        hbox.pack_start(Gtk.Label(label=_("Exception: ")), True, True, 0)
        entry_exception = Gtk.Entry()
        entry_exception.set_text(exception)
        hbox.pack_start(entry_exception, True, True, 0)
        dialog.vbox.pack_start(hbox, False, False, 0)

        dialog.show_all()
        response = dialog.run()
        if response != Gtk.ResponseType.OK:
            dialog.destroy()
            return

        new_check = entry_module.get_active_text()
        new_exception = entry_exception.get_text()
        dialog.destroy()

        self.exceptions.set(exception_pos, (new_check, new_exception))
        if not path:
            # adding new entry
            iter = model.append()
        model.set(iter, self.COLUMN_EXCEPTION, new_check)
        model.set(iter, self.COLUMN_EXCEPTION_VALUE, new_exception)

        # save exceptions
        self.exceptions.save()

    def permission_changed(self, treeview, path, col, model):
        """Processes a permission change. If path is None, adds a new item."""
        if path:
            iter = model.get_iter(path)
            file = model.get_value(iter, self.COLUMN_PATH)
            user = model.get_value(iter, self.COLUMN_USER)
            group = model.get_value(iter, self.COLUMN_GROUP)
            perm = model.get_value(iter, self.COLUMN_PERM)
            force = model.get_value(iter, self.COLUMN_FORCE)
            acl = model.get_value(iter, self.COLUMN_ACL)
            title = _("Changing permissions for %s") % file
        else:
            file = ""
            user = ""
            group = ""
            perm = ""
            force = ""
            acl = ""
            title = _("Adding new permission check")

        if not force:
            force = ""
        else:
            force = "force"

        # asks for new parameter value
        dialog = Gtk.Dialog(title=title, transient_for=self.window, modal=True)
        dialog.add_button(TEXT_OK, Gtk.ResponseType.OK)
        dialog.add_button(TEXT_CANCEL, Gtk.ResponseType.CANCEL)
        label = Gtk.Label(label=_("Changing permissions on <b>%s</b>") % (file or _("new file")))
        label.set_line_wrap(True)
        label.set_use_markup(True)
        dialog.vbox.pack_start(label, False, False, padding=5)

        # aligning entries
        sizegroup1 = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL)
        sizegroup2 = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL)

        if not path:
            # file
            hbox = HBox()
            label = Gtk.Label(label=_("File: "))
            hbox.pack_start(label, True, True, 0)
            entry_file = Gtk.Entry()
            entry_file.set_text(file)
            hbox.pack_start(entry_file, True, True, 0)
            sizegroup1.add_widget(label)
            sizegroup2.add_widget(entry_file)
            dialog.vbox.pack_start(hbox, False, False, 0)

        label = Gtk.Label(label=_("Please specify new file owner and permissions, or use 'current' to keep current settings."))
        label.set_line_wrap(True)
        label.set_use_markup(True)
        dialog.vbox.pack_start(label, False, False, padding=5)

        # user
        hbox = HBox()
        label = Gtk.Label(label=_("User: "))
        hbox.pack_start(label, True, True, 0)
        entry_user = Gtk.Entry()
        entry_user.set_text(user)
        hbox.pack_start(entry_user, True, True, 0)
        sizegroup1.add_widget(label)
        sizegroup2.add_widget(entry_user)
        dialog.vbox.pack_start(hbox, False, False, 0)

        # group
        hbox = HBox()
        label = Gtk.Label(label=_("Group: "))
        hbox.pack_start(label, True, True, 0)
        entry_group = Gtk.Entry()
        entry_group.set_text(group)
        hbox.pack_start(entry_group, True, True, 0)
        sizegroup1.add_widget(label)
        sizegroup2.add_widget(entry_group)
        dialog.vbox.pack_start(hbox, False, False, 0)

        # perm
        hbox = HBox()
        label = Gtk.Label(label=_("Permissions: "))
        hbox.pack_start(label, True, True, 0)
        entry_perm = Gtk.Entry()
        entry_perm.set_text(perm)
        hbox.pack_start(entry_perm, True, True, 0)
        sizegroup1.add_widget(label)
        sizegroup2.add_widget(entry_perm)
        dialog.vbox.pack_start(hbox, False, False, 0)

        check_force = Gtk.CheckButton(label=_("Force permissions"))
        # new entries default to enforced permissions
        check_force.set_active(bool(force) if path else True)
        check_force.set_tooltip_text(_("When enabled, permissions will be enforced regardless of the current settings."))
        check_force.set_halign(Gtk.Align.CENTER)
        dialog.vbox.pack_start(check_force, False, False, 0)

        label = Gtk.Label(label=_("To enforce additional ACL (Access Control List) on file, specify them in the following format:\nuser1:acl,user2:acl\nRefer to 'man setfacl' for details."))
        label.set_line_wrap(True)
        label.set_use_markup(True)
        dialog.vbox.pack_start(label, False, False, padding=5)

        # acl
        hbox = HBox()
        label = Gtk.Label(label=_("ACL: "))
        hbox.pack_start(label, True, True, 0)
        entry_acl = Gtk.Entry()
        entry_acl.set_text(acl)
        hbox.pack_start(entry_acl, True, True, 0)
        sizegroup1.add_widget(label)
        sizegroup2.add_widget(entry_acl)
        dialog.vbox.pack_start(hbox, False, False, 0)

        # determine if this entry is already in perm.local
        in_local = bool(path) and file in self.permlocal.list_options()
        check_local = Gtk.CheckButton(label=_("Save to perm.local"))
        check_local.set_active(in_local)
        check_local.set_tooltip_text(_("Enable saving to %s, a local override file.") % config.PERM_LOCAL)
        check_local.set_halign(Gtk.Align.CENTER)
        dialog.vbox.pack_start(check_local, False, False, 0)

        dialog.show_all()
        response = dialog.run()
        if response != Gtk.ResponseType.OK:
            dialog.destroy()
            return

        newfile = entry_file.get_text() if not path else file
        newuser = entry_user.get_text()
        newgroup = entry_group.get_text()
        newperm = entry_perm.get_text()
        newacl = entry_acl.get_text()
        newforce = "force" if check_force.get_active() else ""

        # if acl is specified, the permissions will be enforced
        if newacl != "":
            newforce = "force"
        save_to_local = check_local.get_active()
        dialog.destroy()

        self.permconfig.set(newfile, (newuser, newgroup, newperm, newforce, newacl))
        if not path:
            # adding new entry
            iter = model.append()
        model.set(iter, self.COLUMN_PATH, newfile)
        model.set(iter, self.COLUMN_USER, newuser)
        model.set(iter, self.COLUMN_GROUP, newgroup)
        model.set(iter, self.COLUMN_PERM, newperm)
        model.set(iter, self.COLUMN_FORCE, newforce == "force")
        model.set(iter, self.COLUMN_ACL, newacl)

        # save to perm.local on request, otherwise remove from it
        if save_to_local:
            self.permlocal.set(newfile, (newuser, newgroup, newperm, newforce, newacl))
        else:
            if newfile in self.permlocal.list_options():
                self.permlocal.remove(newfile)
        self.permlocal.save(None)

        # save to enable reload_config() to load the changes
        standard_permconf = config.load_default_perms(log, self.base_level)
        self.permconfig.save(standard_permconf)

    def option_changed(self, treeview, path, col, model):
        """Processes an option change"""
        iter = model.get_iter(path)
        param = model.get_value(iter, self.COLUMN_OPTION)
        descr = model.get_value(iter, self.COLUMN_DESCR)
        value = model.get_value(iter, self.COLUMN_VALUE)

        # option is disabled?
        if not value:
            value = config.OPTION_DISABLED

        callback, params = config.SETTINGS[param]
        conf_def = self.msec_defaults[self.base_level]

        # Highlighting default options
        def_start=""
        def_end=""
        if self.base_level == config.STANDARD_LEVEL:
            def_start="<b>"
            def_end="</b>"
        elif self.base_level == config.SECURE_LEVEL:
            sec_start="<b>"
            sec_end="</b>"

        val_def = conf_def.get(param)

        # asks for new parameter value
        dialog = Gtk.Dialog(title=_("Select new value for %s") % (param),
                transient_for=self.window, modal=True)
        dialog.add_button(TEXT_OK, Gtk.ResponseType.OK)
        dialog.add_button(TEXT_CANCEL, Gtk.ResponseType.CANCEL)
        # option title
        label = Gtk.Label(label="<b>%s</b>\n" % param)
        label.set_use_markup(True)
        # description
        dialog.vbox.pack_start(label, True, True, 0)
        label = Gtk.Label(label=_("<i>%s</i>\n\n\tCurrent value:\t\t\t<i>%s</i>\n\t%sDefault level value:\t<i>%s</i>%s\n") %
                (descr, value, def_start, val_def, def_end,))
        label.set_line_wrap(True)
        label.set_use_markup(True)
        dialog.vbox.pack_start(label, True, True, 0)
        dialog.vbox.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), True, True, 0)

        # new value
        hbox = HBox()
        label = Gtk.Label(label=_("New value:"))
        label.set_halign(Gtk.Align.CENTER)
        label.set_valign(Gtk.Align.CENTER)
        label.set_hexpand(True)
        label.set_vexpand(True)
        hbox.add(label)
        if '*' in params:
            # string parameter
            entry = Gtk.Entry()
            entry.set_text(value)
        else:
            # combobox parameter
            entry = Gtk.ComboBoxText()
            # add an item to disable a check
            if config.OPTION_DISABLED not in params:
                params.append(config.OPTION_DISABLED)
            for item in params:
                entry.append_text(item)
            if value not in params:
                entry.append_text(value)
                params.append(value)
            active = params.index(value)
            entry.set_active(active)
        hbox.pack_start(entry, True, True, 0)
        dialog.vbox.pack_start(hbox, True, True, 0)

        dialog.show_all()
        response = dialog.run()
        if response != Gtk.ResponseType.OK:
            dialog.destroy()
            return

        # process new parameter
        if '*' in params:
            newval = entry.get_text()
        else:
            newval = entry.get_active_text()
        dialog.destroy()

        # update options
        self.msecconfig.set(param, newval)

        # is it different from default? if yes, change description to italic
        doc = config.find_doc(self.msec, param, self.descriptions)
        if self.option_is_changed(param, newval):
            custom = Pango.Weight.BOLD
        else:
            custom = Pango.Weight.NORMAL

        model.set(iter, self.COLUMN_VALUE, newval)
        model.set(iter, self.COLUMN_DESCR, doc)
        model.set(iter, self.COLUMN_CUSTOM, custom)

    def signal_quit(self, s):
        """Quits via a signal"""
        self.signals.put(s)
        return True

    def quit(self, widget, event=None):
        """Quits the application"""
        num_changes, allchanges, messages = self.check_for_changes(self.msecconfig, self.permconfig)

        if num_changes == 0:
            Gtk.main_quit()
            return False
        else:
            ret = self.ok(widget, ask_ignore=True)
            if ret == Gtk.ResponseType.OK or ret == Gtk.ResponseType.REJECT:
                Gtk.main_quit()
            else:
                return True


# {{{ usage
def usage():
    """Prints help message"""
    print("""Msec: Mandriva Security Center (%s).

Arguments to msecgui:
    -h, --help              displays this helpful message.
    -d                      enable debugging messages.
    -e, --embedded <XID>    embed in MCC.
""" % version)
# }}}

if __name__ == "__main__":
    log_level = logging.INFO
    PlugWindowID = None

    # parse command line
    try:
        opt, args = getopt.getopt(sys.argv[1:], 'hde:', ['help', 'debug', 'embedded='])
    except getopt.error:
        usage()
        sys.exit(1)
    for o in opt:
        # help
        if o[0] == '-h' or o[0] == '--help':
            usage()
            sys.exit(0)
        # list
        elif o[0] == '-d' or o[0] == '--debug':
            log_level = logging.DEBUG
        elif o[0] == '-e' or o[0] == '--embedded':
            try:
                PlugWindowID = int(o[1])
            except:
                print("Error: bad master window XID (%s)!" % o[1], file=sys.stderr)
                sys.exit(1)

    # configuring logging
    log = Log(interactive=True, log_syslog=False, log_file=True, log_level=log_level, log_path=config.SECURITYLOG)

    # loading initial config
    msec_config = config.MsecConfig(log, config=config.SECURITYCONF)

    # loading permissions config
    perm_conf = config.PermConfig(log, config=config.PERMCONF)

    # loading exceptions
    exceptions = config.ExceptionConfig(log, config=config.EXCEPTIONSCONF)

    # creating an msec instance
    msec = MSEC(log)
    perms = PERMS(log)

    log.info("Starting gui..")

    gui = MsecGui(log, msec, perms, msec_config, perm_conf, exceptions, embed=PlugWindowID)
    signal.signal(signal.SIGTERM, lambda s, f: gui.signal_quit(s))
    Gtk.main()

