# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 Süleyman POYRAZ (Zaryob)
#
# 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.
#

import os
import glob
import fcntl
import random
import shutil
import hashlib

ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz'
ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
ascii_letters = ascii_lowercase + ascii_uppercase
digits = '0123456789'


try:
    import polkit
except ImportError:
    pass


# faces

FACES = [
    "/usr/share/kde4/apps/kdm/pics/users",
]

# messages

invalid_username_msg = {
    "en": "User name is invalid.",
    "tr": "Kullanıcı adı geçersiz.",
    "fr": "Nom d'utilisateur invalide.",
    "es": "Nombre de usuario no es válido.",
    "de": "Benutzername nicht erlaubt.",
    "nl": "Gebruikernaam is ongeldig.",
}

invalid_realname_msg = {
    "en": "Real name is invalid.",
    "tr": "Gerçek isim geçersiz.",
    "fr": "Nom réel invalide.",
    "es": "Nombre real no es válido.",
    "de": "Dieser vollständige Name ist nicht erlaubt.",
    "nl": "Echte naam is ongeldig.",
}

short_password_msg = {
    "en": "Password is too short.",
    "tr": "Parola çok kısa.",
    "fr": "Mot de passe trop court.",
    "es": "Contraseña es demadiado corta.",
    "de": "Passwort ist zu kurz.",
    "nl": "Wachtwoord is te kort.",
}

name_password_msg = {
    "en": "Dont use your name as a password.",
    "tr": "Adınızı parola olarak kullanmayın.",
    "fr": "N'utilisez pas votre nom comme mot de passe.",
    "es": "No use su nombre como contraseña.",
    "de": "Benutzen Sie nicht Ihren Namen als Passwort.",
    "nl": "Uw naam niet als wachtwoord gebruiken.",
}

invalid_group_msg = {
    "en": "Invalid group name:",
    "tr": "Geçersiz grup adı:",
    "fr": "Nom de groupe invalide:",
    "es": "Nombre de grupo inválido:",
    "de": "Gruppenname nicht erlaubt:",
    "nl": "Ongeldige groepnaam:",
}

invalid_userid_msg = {
    "en": "Invalid user ID.",
    "tr": "Geçersiz kullanıcı numarası.",
    "fr": "Identifiant utilisateur invalide.",
    "es": "ID de usuario no válido.",
    "de": "User-ID nicht erlaubt.",
    "nl": "Gebruiker-id is ongeldig.",
}

used_userid_msg = {
    "en": "This user ID is already used.",
    "tr": "Bu kullanıcı numarası zaten kullanılmakta.",
    "fr": "Cet identifiant utilisateur est déjà utilisé.",
    "es": "Este ID de usuario ya está en uso.",
    "de": "Dieser user-ID is schon vergeben.",
    "nl": "Deze gebruiker-id is reeds in gebruik.",
}

used_username_msg = {
    "en": "This user name is already used.",
    "tr": "Bu kullanıcı adı zaten kullanılmakta.",
    "fr": "Ce nom d'utilisateur est déjà utilisé.",
    "es": "Este nombre de usuario ya está en uso.",
    "de": "Dieser Benutzername is schon vergeben.",
    "nl": "Deze gebruikernaam is reeds in gebruik.",
}

no_group_msg = {
    "en": "No such group exists.",
    "tr": "Böyle bir grup yok.",
    "fr": "Il n'existe aucun groupe de ce nom-là.",
    "es": "No existe el grupo.",
    "de": "Diese Gruppe gibt es nicht.",
    "nl": "Deze groep bestaat niet.",
}

no_user_msg = {
    "en": "No user with given ID.",
    "tr": "Verilen numaralı bir kullanıcı yok.",
    "fr": "Il n'existe aucun utilisateur avec et identifiant.",
    "es": "No existe usuario con éste ID.",
    "de": "Es gibt keien Benutzer mit dem angegebenen ID.",
    "nl": "Gebruiker met opgegeven ID bestaat niet.",
}

delete_root_msg = {
    "en": "You cant delete root user.",
    "tr": "Kök kullanıcıyı silemezsiniz.",
    "fr": "Vous ne pouvez pas supprimer l'administrateur.",
    "es": "No se puede eliminar al usuario root.",
    "de": "Benutzer ROOT darf nicht gelöscht werden.",
    "nl": "Systeembeheerder (root) kan niet verwijderd worden.",
}

invalid_groupid_msg = {
    "en": "Invalid group ID.",
    "tr": "Geçersiz grup numarası.",
    "fr": "Identifiant de groupe invalide.",
    "es": "ID del grupo inválido.",
    "de": "Gruppen-ID nicht zulässig.",
    "nl": "Groep-ID is ongeldig.",
}

used_groupid_msg = {
    "en": "This group ID is already used.",
    "tr": "Bu grup numarası zaten kullanılmakta.",
    "fr": "Cet identifiant de groupe est déjà utilisé.",
    "es": "Este ID de grupo ya está en uso.",
    "de": "Dieser Gruppen-ID is schon vergeben.",
    "nl": "Deze groep-ID is reeds in gebruik.",
}

used_groupname_msg = {
    "en": "This group name is already used.",
    "tr": "Bu grup adı zaten kullanılmakta.",
    "fr": "Ce nom de groupe est déjà utilisé.",
    "es": "Este nombre de grupo ya está en uso.",
    "de": "Dieser Gruppennam is schon vergeben.",
    "nl": "Deze groepnaam is reeds in gebruik.",
}

# parameters
uid_minimum = 1000
uid_maximum = 65000

#

def setFace(uid, homedir):
    files = []
    for directory in FACES:
        if os.path.exists(directory):
            for filename in os.listdir(directory):
                if filename.endswith(".png"):
                    files.append(os.path.join(directory, filename))
    if len(files):
        icon = os.path.join(homedir, ".face.icon")
        shutil.copy(random.choice(files), icon)
        os.chmod(icon, 0o644)
        os.chown(icon, uid, 100)

def checkName(name):
    first_valid = ascii_letters
    valid = ascii_letters + "_-" + digits
    if len(name) == 0 or len(filter(lambda x: not x in valid, name)) != 0 or not name[0] in first_valid:
        fail(_(invalid_username_msg))

def checkRealName(realname):
    if len(filter(lambda x: x == "\n" or x == ":", realname)) != 0:
        fail(_(invalid_realname_msg))

def checkPassword(password, badlist):
    if len(password) < 1:
        fail(_(short_password_msg))
    if password in badlist:
        fail(_(name_password_msg))

def checkGroupName(name):
    valid = ascii_letters + "_-"
    if name == "" or len(filter(lambda x: not x in valid, name)) != 0:
        fail(_(invalid_group_msg) + " " + name)

#

class User:
    def __init__(self):
        self.password = None

    def __str__(self):
        return "%s (%d, %d)\n  %s\n  %s\n  %s\n  %s" % (
            self.name, self.uid, self.gid,
            self.realname, self.homedir, self.shell,
            self.password
        )


class Group:
    def __str__(self):
        s = "%s (%d)" % (self.name, self.gid)
        for name in self.members:
            s += "\n %s" % name
        return s


class Database:
    passwd_path = "/etc/passwd"
    shadow_path = "/etc/shadow"
    group_path = "/etc/group"
    lock_path = "/etc/.pwd.lock"

    def __init__(self, for_read=False):
        self.lock = open(self.lock_path)
        self.users = {}
        self.users_by_name = {}
        self.groups = {}
        self.groups_by_name = {}

        for line in open(self.passwd_path):
            if line != "" and line != "\n":
                parts = line.rstrip("\n").split(":")
                user = User()
                user.name = parts[0]
                user.uid = int(parts[2])
                user.gid = int(parts[3])
                user.realname = parts[4]
                user.homedir = parts[5]
                user.shell = parts[6]
                self.users[user.uid] = user
                self.users_by_name[user.name] = user

        for line in open(self.shadow_path):
            if line != "" and line != "\n":
                parts = line.rstrip("\n").split(":")
                if self.users_by_name.has_key(parts[0]):
                    user = self.users_by_name[parts[0]]
                    user.password = parts[1]
                    user.pwrest = parts[2:]

        for line in open(self.group_path):
            if line != "" and line != "\n":
                parts = line.rstrip("\n").split(":")
                group = Group()
                group.name = parts[0]
                group.gid = int(parts[2])
                group.members = parts[3].split(",")
                if "" in group.members:
                    group.members.remove("")
                self.groups[group.gid] = group
                self.groups_by_name[group.name] = group

    def sync(self):
        lines = []
        keys = self.users.keys()
        keys.sort()
        for uid in keys:
            user = self.users[uid]
            lines.append("%s:x:%d:%d:%s:%s:%s\n" % (
                user.name, uid, user.gid,
                user.realname, user.homedir, user.shell
            ))
        f = open(self.passwd_path, "w")
        f.writelines(lines)
        f.close()

        lines = []
        keys = self.users.keys()
        keys.sort()
        for uid in keys:
            user = self.users[uid]
            if user.password:
                lines.append("%s:%s:%s\n" % (
                    user.name,
                    user.password,
                    ":".join(user.pwrest)
                ))
            else:
                lines.append("%s::13094:0:99999:7:::\n" % user.name)
        f = open(self.shadow_path, "w")
        f.writelines(lines)
        f.close()

        lines = []
        keys = self.groups.keys()
        keys.sort()
        for gid in keys:
            group = self.groups[gid]
            lines.append("%s:x:%s:%s\n" % (group.name, gid, ",".join(group.members)))
        f = open(self.group_path, "w")
        f.writelines(lines)
        f.close()

    def set_groups(self, name, grouplist):
        for gid in self.groups.keys():
            g = self.groups[gid]
            if name in g.members:
                if not g.name in grouplist:
                    g.members.remove(name)
            else:
                if g.name in grouplist:
                    g.members.append(name)

    def next_uid(self):
        for i in range(uid_minimum, uid_maximum):
            if not self.users.has_key(i):
                return i

    def next_gid(self):
        for i in range(uid_minimum, uid_maximum):
            if not self.groups.has_key(i):
                return i


def setup_home(uid, gid, path):
    if not os.path.exists(path):
        # Copy skeleton home dir
        os.system('/bin/cp -r %s "%s"' % ('/etc/skel', path))
        # Set a random face icon
        faces = glob.glob("/usr/share/*/apps/kdm/pics/users/*.png")
        if len(faces) > 0:
            facepath = os.path.join(path, '.face.icon')
            os.system('/bin/cp --remove-destination "%s" "%s"' % (random.choice(faces), facepath))
            os.chmod(facepath, 0o644)
        # Set ownerships
        os.system('/bin/chown -R %d:%d "%s"' % (uid, gid, path))

    # Make sure at least top of the home dir's permissions are correct
    os.system('/bin/chown %d:%d "%s"' % (uid, gid, path))
    os.chmod(path, 0o711)


# methods

def userList():
    def format(dict, uid):
        item = dict[uid]
        return (item.uid, item.name, item.realname)
    db = Database(for_read=True)
    return map(lambda x: format(db.users, x), db.users)

def userInfo(uid):
    uid = int(uid)
    db = Database(for_read=True)
    if db.users.has_key(uid):
        u = db.users[uid]
        groups = []
        for item in db.groups.keys():
            if u.name in db.groups[item].members:
                groups.append(db.groups[item].name)
        grp = db.groups.get(u.gid, None)
        if grp:
            if grp.name in groups:
                groups.remove(grp.name)
            groups.insert(0, grp.name)
        ret = (
            u.name,
            u.realname,
            u.gid,
            u.homedir,
            u.shell,
            groups,
        )
        return ret
    else:
        fail(_(no_user_msg))

def addUser(uid, name, realname, homedir, shell, password, groups, grants, blocks):
    if not realname:
        realname = ""
    if not homedir:
        homedir = "/home/" + name
    if not shell:
        shell = "/bin/bash"
    if not groups:
        groups = ["nogroup"]
    for item in groups:
        checkGroupName(item)
    checkName(name)
    checkRealName(realname)
    if password:
        checkPassword(password, (name, realname))

    db = Database()

    if uid == -1:
        uid = db.next_uid()
    else:
        try:
            uid = int(uid)
            if uid < 0 or uid > 65536:
                raise
        except:
            fail(_(invalid_userid_msg))
        if db.users.has_key(uid):
            fail(_(used_userid_msg))

    if db.users_by_name.has_key(name):
        fail(_(used_username_msg))

    # First group in the list is the user's main group
    g = db.groups_by_name.get(groups[0], None)
    if not g:
        fail(_(no_group_msg))
    gid = g.gid

    u = User()
    u.uid = uid
    u.gid = gid
    u.name = name
    u.realname = realname
    u.homedir = homedir
    u.shell = shell
    if password:
        u.password = shadowCrypt(password)
    else:
        u.password = "*"
    u.pwrest = [ "13094", "0", "99999", "7", "", "", "" ]
    db.users[uid] = u
    db.set_groups(name, groups)
    # No need to setup a real home dir for daemons
    if uid >= 1000 or homedir.startswith("/home/"):
        setup_home(uid, gid, homedir)
        setFace(uid, homedir)
    db.sync()

    for grant in grants:
        if grant != "":
            grantAuthorization(uid, grant)
    for block in blocks:
        if block != "":
            blockAuthorization(uid, block)

    return uid

def setUser(uid, realname, homedir, shell, password, groups):
    uid = int(uid)

    db = Database()
    u = db.users.get(uid, None)
    if u:
        if realname:
            checkRealName(realname)
            u.realname = realname
        if homedir:
            u.homedir = homedir
        if shell:
            u.shell = shell
        if password:
            checkPassword(password, (u.name, u.realname, realname))
            u.password = shadowCrypt(password)
        if groups:
            # FIXME: check main group
            for item in groups:
                checkGroupName(item)
            db.set_groups(u.name, groups)
        db.sync()
    else:
        fail(_(no_user_msg))

def deleteUser(uid, deletefiles):
    uid = int(uid)

    if uid == 0:
        fail(_(delete_root_msg))

    db = Database()
    u = db.users.get(uid, None)
    if u:
        #delete authorizations of user
        try:
            polkit.auth_revoke_all(uid)
        except:
            pass

        home = u.homedir[:]
        db.set_groups(u.name, [])
        del db.users[uid]
        db.sync()
        if deletefiles:
            os.system('/bin/rm -rf "%s"' % home)


def groupList():
    def format(dict, gid):
        item = dict[gid]
        return (item.gid, item.name)
    db = Database(for_read=True)
    return map(lambda x: format(db.groups, x), db.groups)

def addGroup(gid, name):
    checkGroupName(name)

    db = Database()
    if gid == -1:
        gid = db.next_gid()
    else:
        try:
            gid = int(gid)
            if gid < 0 or gid > 65536:
                raise
        except:
            fail(_(invalid_groupid_msg))
        if db.groups.has_key(gid):
            fail(_(used_groupid_msg))

    if db.groups_by_name.has_key(name):
        fail(_(used_groupname_msg))

    g = Group()
    g.gid = gid
    g.name = name
    g.members = []
    db.groups[gid] = g
    db.sync()

    return gid

def deleteGroup(gid):
    gid = int(gid)

    db = Database()
    if db.groups.has_key(gid):
        del db.groups[gid]
        db.sync()


#
# Crypt function for shadow file
#

def shadowCrypt(password):
    des_salt = list('./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
    salt, magic = str(random.random())[-8:], '$1$'

    ctx = hashlib.md5(password)
    ctx.update(magic)
    ctx.update(salt)

    ctx1 = hashlib.md5(password)
    ctx1.update(salt)
    ctx1.update(password)

    final = ctx1.digest()

    for i in range(len(password), 0 , -16):
        if i > 16:
            ctx.update(final)
        else:
            ctx.update(final[:i])

    i = len(password)

    while i:
        if i & 1:
            ctx.update('\0')
        else:
            ctx.update(password[:1])
        i = i >> 1
    final = ctx.digest()

    for i in range(1000):
        ctx1 = hashlib.md5()
        if i & 1:
            ctx1.update(password)
        else:
            ctx1.update(final)
        if i % 3: ctx1.update(salt)
        if i % 7: ctx1.update(password)
        if i & 1:
            ctx1.update(final)
        else:
            ctx1.update(password)
        final = ctx1.digest()

    def _to64(v, n):
        r = ''
        while (n-1 >= 0):
            r = r + des_salt[v & 0x3F]
            v = v >> 6
            n = n - 1
        return r

    rv = magic + salt + '$'
    final = map(ord, final)
    l = (final[0] << 16) + (final[6] << 8) + final[12]
    rv = rv + _to64(l, 4)
    l = (final[1] << 16) + (final[7] << 8) + final[13]
    rv = rv + _to64(l, 4)
    l = (final[2] << 16) + (final[8] << 8) + final[14]
    rv = rv + _to64(l, 4)
    l = (final[3] << 16) + (final[9] << 8) + final[15]
    rv = rv + _to64(l, 4)
    l = (final[4] << 16) + (final[10] << 8) + final[5]
    rv = rv + _to64(l, 4)
    l = final[11]
    rv = rv + _to64(l, 2)

    return rv

#
# List authorizations by UID
#

def listUserAuthorizations(uid):
    actions = polkit.auth_list_uid(int(uid))
    auths = []
    for action in actions:
        action_info = polkit.action_info(action['action_id'])
        auths.append((action['action_id'], action['scope'], action_info['description'], action_info['policy_active'], action['negative']))
    return auths

#
# Grant authorization to user
#

def grantAuthorization(uid, action):
    uid = int(uid)
    if action == "*":
        for action_id in polkit.action_list():
            try:
                polkit.auth_revoke(uid, action_id)
                polkit.auth_add(action_id, polkit.SCOPE_ALWAYS, uid)
            except:
                return False
    else:
        try:
            polkit.auth_revoke(uid, action)
            polkit.auth_add(action, polkit.SCOPE_ALWAYS, uid)
        except:
            return False
    return True

#
# Revoke authorization of user
#

def revokeAuthorization(uid, action):
    uid = int(uid)
    if action == "*":
        for action_id in polkit.action_list():
            try:
                polkit.auth_revoke(uid, action_id)
            except:
                return False
    else:
        try:
            polkit.auth_revoke(uid, action)
        except:
            return False
    return True

#
# Block authorization of user
#

def blockAuthorization(uid, action):
    uid = int(uid)
    if action == "*":
        for action_id in polkit.action_list():
            try:
                polkit.auth_revoke(uid, action_id)
                polkit.auth_block(uid, action_id)
            except:
                return False
    else:
        try:
            polkit.auth_revoke(uid, action)
            polkit.auth_block(uid, action)
        except:
            return False
    return True
