#!/usr/bin/env python
# coding: utf-8
#
# fskbsetting-daemon
# Copyright (C) FSnow 2009-2013 <fsnow@yandex.ru>
#
# fskbsetting 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 3 of the License, or
# (at your option) any later version.
#
# fskbsetting 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 subprocess
import lxml.etree

from gi.repository import GObject

import dbus
import dbus.service
import dbus.mainloop.glib

from dbus.exceptions import DBusException


class DemoException(dbus.DBusException):
    _dbus_error_name = 'fskbsetting.DemoException'


class NoValidArgumentsException(dbus.DBusException):
    _dbus_error_name = 'fskbsetting.NoValidArgumentsException'


class PermissionDeniedByPolicy(dbus.DBusException):
    _dbus_error_name = 'fskbsetting.PermissionDeniedByPolicy'


class fskbsettingDaemon(dbus.service.Object):
    def __init__(self, conn=None, object_path=None, bus_name=None):
        dbus.service.Object.__init__(self, conn, object_path, bus_name)
        self.enforce_polkit = True
        self.dbus_info      = None
        self.polkit         = None
        self.__caller_pid__ = 0
        self._package_operation_in_progress = False
        self.proc = None

    @dbus.service.method("fskbsetting.SampleInterface",
                         in_signature='', out_signature='',
                         sender_keyword='sender', connection_keyword='conn')
    def RaiseException(self, sender=None, conn=None):
        raise DemoException('RaiseException fskbsetting method')

    @dbus.service.method("fskbsetting.SampleInterface",
                         in_signature='', out_signature='',
                         sender_keyword='sender', connection_keyword='conn')
    def Exit(self, sender=None, conn=None):
        mainloop.quit()

    @dbus.service.method('fskbsetting.SampleInterface',
                     in_signature='ssss', out_signature='',
                     sender_keyword='sender', connection_keyword='conn')
    def set_default_keyboard(self, xkbmodel='', xkblayout='', xkbvariant='', xkboptions='', sender=None, conn=None):
        self._check_polkit_privilege(sender, conn, 'fskbsetting.daemon.start')
        self._validate_arguments(xkbmodel, xkblayout, xkbvariant, xkboptions)

        d = {"XKBMODEL": xkbmodel,
             "XKBLAYOUT": xkblayout,
             "XKBVARIANT": xkbvariant,
             "XKBOPTIONS": xkboptions}

        fname = '/etc/default/keyboard'
        default_keyboard = file(fname).readlines()
        default_keyboard_new = []

        for line in default_keyboard:
            key = line.split('=', 1)[0]
            if key in d.keys():
                default_keyboard_new.append('%s="%s"\n'%(key, d.pop(key)))
            else:
                default_keyboard_new.append(line)

        if len(d)>0:
            for key, val in d .items():
                default_keyboard_new.append('%s="%s"\n'%(key, val))

        fp = open(fname, 'w')
        fp.writelines(default_keyboard_new)
        fp.close()

        subprocess.Popen('udevadm trigger --subsystem-match=input --action=change', shell=True)
        self.proc = subprocess.Popen('update-initramfs -u', shell=True)

    @dbus.service.method('fskbsetting.SampleInterface',
                     in_signature='s', out_signature='',
                     sender_keyword='sender', connection_keyword='conn')
    def set_default_numlockx(self, numlock='', sender=None, conn=None):
        self._check_polkit_privilege(sender, conn, 'fskbsetting.daemon.start')

        if numlock not in ['auto', 'on', 'off', 'keep', 'toggle']:
            raise NoValidArgumentsException

        fname = '/etc/default/numlockx'
        default_numlockx = file(fname).readlines()
        default_numlockx_new = []

        for line in default_numlockx:
            if line.split('=', 1)[0] == 'NUMLOCK':
                default_numlockx_new.append('NUMLOCK=%s\n'%(numlock))
            else:
                default_numlockx_new.append(line)

        fp = open(fname, 'w')
        fp.writelines(default_numlockx_new)
        fp.close()

    @dbus.service.method('fskbsetting.SampleInterface',
                     in_signature='', out_signature='b',
                     sender_keyword='sender', connection_keyword='conn')
    def still_working(self, sender=None, conn=None):
        try:
            if self.proc.poll() is None:
                return True
            else:
                return False
        except:
            return False

    def _check_polkit_privilege(self, sender, conn, privilege):
        """Verify that sender has a given PolicyKit privilege."""
        if sender is None and conn is None:
            raise

        # Get peer PID
        if self.dbus_info is None:
            self.dbus_info = dbus.Interface(conn.get_object('org.freedesktop.DBus',
                '/org/freedesktop/DBus/Bus', False), 'org.freedesktop.DBus')
        pid = self.dbus_info.GetConnectionUnixProcessID(sender)

        # Query PolicyKit
        if self.polkit is None:
            self.polkit = dbus.Interface(dbus.SystemBus().get_object(
                'org.freedesktop.PolicyKit1',
                '/org/freedesktop/PolicyKit1/Authority', False),
                'org.freedesktop.PolicyKit1.Authority')
        try:
            # Don't need is_challenge return here, since we call with AllowUserInteraction
            (is_auth, _, details) = self.polkit.CheckAuthorization(
                    ('unix-process', {'pid': dbus.UInt32(pid, variant_level=1),
                     'start-time': dbus.UInt64(0, variant_level=1)}),
                     privilege, {'': ''}, dbus.UInt32(1), '', timeout=600)
        except:
            raise

        if not is_auth:
            raise

        if self.__caller_pid__ == 0:
            self.__caller_pid__ = pid

    def _validate_arguments(self, xkbmodel, xkblayout, xkbvariant, xkboptions, data=None):
        tree = lxml.etree.parse('/usr/share/X11/xkb/rules/base.xml')

        if xkbmodel.strip() != '' \
        and xkbmodel not in [model.xpath('./configItem/name')[0].text for model in tree.xpath('//model')]:
            raise NoValidArgumentsException

        if xkblayout.strip() == '' \
        or len(xkblayout.split(',')) != len(xkbvariant.split(',')):
            raise NoValidArgumentsException

        valid_layouts = [layout.xpath('./configItem/name')[0].text for layout in tree.xpath('//layout')]
        for layout in xkblayout.split(','):
            if layout not in valid_layouts:
                raise NoValidArgumentsException

        valid_variants = [variant.xpath('./configItem/name')[0].text for variant in tree.xpath('//variant')]
        valid_variants.append('')
        for variant in xkbvariant.split(','):
            if variant not in valid_variants:
                raise NoValidArgumentsException

        valid_options = [option.xpath('./configItem/name')[0].text for option in tree.xpath('//option')]
        valid_options.append('')
        for option in xkboptions.split(','):
            if option not in valid_options:
                raise NoValidArgumentsException


if __name__ == '__main__':
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

    bus = dbus.SystemBus()
    name = dbus.service.BusName("fskbsetting.Daemon", bus)
    object = fskbsettingDaemon(bus, '/fskbsettingDaemon')

    mainloop = GObject.MainLoop()
    mainloop.run()
