# -*- coding: utf-8; Mode: Python; indent-tabs-mode: nil; tab-width: 4 -*-
#
# «dgx-grub» - Configure the GRUB PASSWORD
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

from __future__ import print_function

import os
import re
import subprocess
import pwd
import sys
import contextlib

import debconf

from ubiquity import misc, plugin, validation
from subprocess import PIPE,Popen,run

NAME = 'nv-grub-passwd'
AFTER = 'usersetup'
BEFORE = 'network'
WEIGHT = 20
MINPASSWDLEN = 8

def gen_grub_passwd_hash(passwd):
    '''
    Subroutine to gen password hash using command grub-mkpasswd-pbkdf2
    grub-mkpasswd-pbkdf2 takes in the grub passwd as imput
    Returns: hash string
    '''
    p1 = Popen(["echo","-e","%s\n%s" % (passwd, passwd)], shell=False, stdout=PIPE)
    p2 = Popen(["grub-mkpasswd-pbkdf2"], shell=False, stdin=p1.stdout, stdout=PIPE)
    try:
        output,err = p2.communicate(timeout=300)
    except TimeoutExpired:
        p2.kill()
    if not err and output:
        pass_hash=output.decode('utf-8').split('hash of your password is ')[1].strip()
    return pass_hash

def run_shell_command(cmd):
    '''
    Helper function to run shell commands
    '''
    try:
        run([cmd], shell=True)
    except CalledProcessError as e:
        print(e)

def write_grub_passwd_file(uname, pass_hash):
    '''
        * checks and deletes grub drop-in file 02_passwd if alredy exists
        * creates drop-in file 02_passwd
        * sets permission on the file
        * runs update-grub
    '''

    fname = '/etc/grub.d/02_passwd'
    with contextlib.suppress(FileNotFoundError):
        os.remove(fname)
    file_text="#!/bin/sh -e\ncat << EOF\n\tset superusers=\"%s\"\n\tpassword_pbkdf2 %s %s\nEOF" %(uname,uname,pass_hash)
    try:
        with open(fname, "w") as file:
            file.write("%s" % file_text)
    except IOError as error:
        print('Unable to write to file: {}'.format(error))
    chmod_cmd='chmod a+x %s' % fname
    run_shell_command(chmod_cmd)

def add_unrestricted_keyword():
    '''
    Adds --unrestricted keyword at the end of CLASS definition in the 10_linux file
    This enables paswword less boot if not modifying grub entries
    '''
    fname = '/etc/grub.d/10_linux'
    add_unrestricted_cmd="sed -i \"s/--class os/--class os --unrestricted/\" %s" % fname
    run_shell_command(add_unrestricted_cmd)
    
def grub_passwd_setup(uname, passwd):
    '''
    Wrapper subroutine
    '''
    update_grub = 'update-grub'
    hash_gen = gen_grub_passwd_hash(passwd)
    write_grub_passwd_file(uname, hash_gen)
    add_unrestricted_keyword()
    run_shell_command(update_grub)
    
def randomString():
    import string
    import random

    size = 12
    chars = string.ascii_uppercase + string.ascii_lowercase + string.digits

    return ''.join(random.choice(chars) for _ in range(size))

def FrontendIsUbiquity():
    return 'UBIQUITY_FRONTEND' in os.environ and os.environ['UBIQUITY_FRONTEND'] == 'debconf_ui'

def make_error_string(controller, errors):
    """Returns a newline-separated string of translated error reasons."""
    return "\n".join([controller.get_string(error) for error in errors])

def get_min_passwd_len():
    return MINPASSWDLEN

def gen_grub_passwd(basestr):
    min_length = get_min_passwd_len()

    # Backfill to with zeroes for platforms that require a minimum length
    return basestr.ljust(min_length, '0')

def get_first_user():
    try:
        # XXX 'oem' user not getting deleted during installation
        sysuser = pwd.getpwuid(1001)
    except KeyError:
        sysuser = pwd.getpwuid(1000)

    return sysuser.pw_name

class PageBase(plugin.PluginUI):
    def __init__(self):
        self.suffix = misc.dmimodel()
        self.allow_password_empty = True

    def get_password(self):
        """Get the grub user's password."""
        raise NotImplementedError('get_password')

    def get_verified_password(self):
        """Get the grub user's password confirmation."""
        raise NotImplementedError('get_verified_password')

    def password_error(self, msg):
        """The selected password was bad."""
        raise NotImplementedError('password_error')

    def clear_errors(self):
        pass

    def info_loop(self, *args):
        """Verify user input."""
        pass


class PageGtk(PageBase):
    plugin_title = 'ubiquity/text/wireless_password_label'

    def __init__(self, controller, *args, **kwargs):
        from gi.repository import Gio, Gtk

        PageBase.__init__(self, *args, **kwargs)
        self.resolver = Gio.Resolver.get_default()
        self.controller = controller

        builder = Gtk.Builder()
        self.controller.add_builder(builder)
        builder.add_from_file(os.path.join(
            os.environ['UBIQUITY_GLADE'], 'stepNvgrubpasswd.ui'))
        builder.connect_signals(self)
        self.page = builder.get_object('stepNvgrubpasswd')
        self.password = builder.get_object('password')
        self.verified_password = builder.get_object('verified_password')
        self.password_error_label = builder.get_object('password_error_label')

        self.password_ok = builder.get_object('password_ok')
        self.password_strength = builder.get_object('password_strength')
        self.password_min_length = get_min_passwd_len()

        # Dodgy hack to let us center the contents of the page without it
        # moving as elements appear and disappear, specifically the full name
        # okay check icon and the hostname error messages.
        paddingbox = builder.get_object('paddingbox')

        def func(box):
            box.get_parent().child_set_property(box, 'expand', False)
            box.set_size_request(box.get_allocation().width / 2, -1)

        paddingbox.connect('realize', func)

        # Some signals need to be connected by hand so that we have the
        # handler ids.

        # The UserSetup component takes care of preseeding passwd/user-uid.
        misc.execute_root('apt-install', 'oem-config-gtk', 'oem-config-slideshow-ubuntu')

        self.resolver_ok = True
        self.plugin_widgets = self.page

    # Functions called by the Page.

    def get_password(self):
        return self.password.get_text()

    def get_verified_password(self):
        return self.verified_password.get_text()

    def password_error(self, msg):
        self.password_strength.hide()
        m = '<small><span foreground="darkred"><b>%s</b></span></small>' % msg
        self.password_error_label.set_markup(m)
        self.password_error_label.show()

    def clear_errors(self):
        self.password_error_label.hide()

    # Callback functions.

    def info_loop(self, widget):
        """check if all entries from Identification screen are filled. Callback
        defined in ui file."""

        # Do some initial validation.  We have to process all the widgets so we
        # can know if we can really show the next button.  Otherwise we'd show
        # it on any field being valid.
        complete = True
        pwlen = len(self.get_password())

        # Check password length first
        if (pwlen < self.password_min_length) and (pwlen > 0):
            self.password_ok.hide()
            self.clear_errors()
            self.password_error("Shorter than %s characters" % \
                str(self.password_min_length))
            password_ok = False
        else:
            self.password_ok.hide()
            self.clear_errors()
            password_ok = validation.gtk_password_validate(
                self.controller,
                self.password,
                self.verified_password,
                self.password_ok,
                self.password_error_label,
                self.password_strength,
                self.allow_password_empty,
            )

        complete = complete and password_ok

        self.controller.allow_go_forward(complete)


class PageKde(PageBase):
    plugin_breadcrumb = 'ubiquity/text/breadcrumb_user'

    def __init__(self, controller, *args, **kwargs):
        PageBase.__init__(self, *args, **kwargs)
        self.controller = controller

        from PyQt5 import uic
        from PyQt5.QtGui import QPixmap

        self.plugin_widgets = uic.loadUi(
            '/usr/share/ubiquity/qt/stepNvgrubpasswd.ui')
        self.page = self.plugin_widgets

        if self.controller.oem_config:
            self.page.login_pass.hide()


        # The UserSetup component takes care of preseeding passwd/user-uid.
        misc.execute_root('apt-install', 'oem-config-kde')

        warningIcon = QPixmap(
            "/usr/share/icons/oxygen/48x48/status/dialog-warning.png")
        self.page.password_error_image.setPixmap(warningIcon)

        self.clear_errors()

        # self.page.password.textChanged[str].connect(self.on_password_changed)
        # self.page.verified_password.textChanged[str].connect(
        #    self.on_verified_password_changed)
        self.page.login_pass.clicked[bool].connect(self.on_login_pass_clicked)
        self.page.login_auto.clicked[bool].connect(self.on_login_auto_clicked)

        self.page.password_debug_warning_label.setVisible(
            'UBIQUITY_DEBUG' in os.environ)

    def on_password_changed(self):
        pass

    def on_verified_password_changed(self):
        pass

    def get_password(self):
        return str(self.page.password.text())

    def get_verified_password(self):
        return str(self.page.verified_password.text())

    def password_error(self, msg):
        self.page.password_error_reason.setText(msg)
        self.page.password_error_image.show()
        self.page.password_error_reason.show()

    def clear_errors(self):
        self.page.password_error_image.hide()
        self.page.password_error_reason.hide()


class PageDebconf(PageBase):
    plugin_title = 'ubiquity/text/wireless_password_label'

    def __init__(self, controller, *args, **kwargs):
        self.controller = controller


class PageNoninteractive(PageBase):
    def __init__(self, controller, *args, **kwargs):
        PageBase.__init__(self, *args, **kwargs)
        self.controller = controller
        self.password = ''
        self.verifiedpassword = ''
        self.console = self.controller._wizard.console


    def get_password(self):
        """Get the user's password."""
        return self.controller.dbfilter.db.get('grubpasswd/grub-password')

    def get_verified_password(self):
        """Get the user's password confirmation."""
        return self.controller.dbfilter.db.get('grubpasswd/grub-password-again')

    def password_error(self, msg):
        """The selected password was bad."""
        print('\nBad password: %s' % msg, file=self.console)
        import getpass
        self.password = getpass.getpass('Password: ')
        self.verifiedpassword = getpass.getpass('Password again: ')

    def clear_errors(self):
        pass


class Page(plugin.Plugin):
    def prepare(self, unfiltered=False):
        if FrontendIsUbiquity():
            return ['/usr/share/nvidia/nv-grub-passwd.sh']

        # Load debconf templates
        (_, _) = subprocess.getstatusoutput(
            '/usr/share/nvidia/nv-grub-passwd.sh load_only')

        # We need to call info_loop as we switch to the page so the next button
        # gets disabled.
        self.ui.info_loop(None)

        # End here, don't return a command to fall through to
        # Page[Gtk|Kde] cases
        return

    def ok_handler(self):
        self.ui.clear_errors()

        password = self.ui.get_password()
        password_confirm = self.ui.get_verified_password()

        self.preseed('grubpasswd/grub-password', password)
        self.preseed('grubpasswd/grub-password-again', password_confirm)
        if self.ui.controller.oem_config:
            self.preseed('passwd/user-uid', '29999')
        else:
            self.preseed('passwd/user-uid', '')

        plugin.Plugin.ok_handler(self)

    def error(self, priority, question):
        if question.startswith('grubpasswd/grub-password-'):
            self.ui.password_error(self.extended_description(question))
        else:
            self.ui.error_dialog(
                self.description(question),
                self.extended_description(question))
        return plugin.Plugin.error(self, priority, question)

class Install(plugin.InstallPlugin):
    def install(self, target, progress, *args, **kwargs):
        import syslog

        # By the time we get to the install phase, console output in
        # GTK mode no longer gets saved to oem-config.log.  So instead
        # we'll use the following log function
        def log_to_syslog(msg):
            syslog.syslog("oem-config: %s: %s" % (NAME, msg))

        grubusername = get_first_user()
        grubpassword = ""

        try:
            # Retrieve user specified password
            grubpassword = self.db.get('grubpasswd/grub-password')

            if grubpassword:
                grub_passwd_setup(grubusername, grubpassword)
            else:
                log_to_syslog("Skipping GRUB password")
        except:
            log_to_syslog("Failed to retrieve GRUB user info")
        return plugin.InstallPlugin.install(self, target, progress, *args, **kwargs)
