#!/usr/bin/python3

import os
import pwd
import grp
import gettext
import shutil
import re
import subprocess
from random import randint
from setproctitle import setproctitle

import PIL
from PIL import Image
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("AccountsService", "1.0")
from gi.repository import Gtk, GObject, Gio, GdkPixbuf, AccountsService, GLib

gettext.install("cinnamon", "/usr/share/locale")

class PrivHelper(object):
    """A helper for performing temporary privilege drops. Necessary for
    security when accessing user controlled files as root."""

    def __init__(self):

        self.orig_uid = os.getuid()
        self.orig_gid = os.getgid()
        self.orig_groups = os.getgroups()

    def drop_privs(self, user):

        uid = user.get_uid()
        # the user's main group id
        gid = pwd.getpwuid(uid).pw_gid

        # initialize the user's supplemental groups and main group
        os.initgroups(user.get_user_name(), gid)
        os.setegid(gid)
        os.seteuid(uid)

    def restore_privs(self):

        os.seteuid(self.orig_uid)
        os.setegid(self.orig_gid)
        os.setgroups(self.orig_groups)

priv_helper = PrivHelper()

(INDEX_USER_OBJECT, INDEX_USER_PICTURE, INDEX_USER_DESCRIPTION) = range(3)
(INDEX_GID, INDEX_GROUPNAME) = range(2)

class GroupDialog (Gtk.Dialog):
    def __init__ (self, label, value, parent = None):
        super(GroupDialog, self).__init__(None, parent)

        try:
            self.set_modal(True)
            self.set_skip_taskbar_hint(True)
            self.set_skip_pager_hint(True)
            self.set_title("")

            table = DimmedTable()
            table.add_labels([label])

            self.entry = Gtk.Entry()
            self.entry.set_text(value)
            self.entry.connect("changed", self._on_entry_changed)
            table.add_controls([self.entry])

            self.set_border_width(6)

            box = self.get_content_area()
            box.add(table)
            self.show_all()

            self.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, _("OK"), Gtk.ResponseType.OK, )
            self.set_response_sensitive(Gtk.ResponseType.OK, False)

        except Exception as detail:
            print(detail)

    def _on_entry_changed(self, entry):
        name = entry.get_text()
        if " " in name or name.lower() != name:
            entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "xsi-dialog-warning-symbolic")
            entry.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("The group name cannot contain upper-case or space characters"))
            self.set_response_sensitive(Gtk.ResponseType.OK, False)
        else:
            entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, None)
            self.set_response_sensitive(Gtk.ResponseType.OK, True)

        if entry.get_text() == "":
            self.set_response_sensitive(Gtk.ResponseType.OK, False)

class DimmedTable (Gtk.Table):
    def __init__ (self):
        super(DimmedTable, self).__init__()
        self.set_border_width(6)
        self.set_row_spacings(8)
        self.set_col_spacings(15)

    def add_labels(self, texts):
        row = 0
        for text in texts:
            if text is not None:
                label = Gtk.Label(text)
                label.set_alignment(1, 0.5)
                label.get_style_context().add_class("dim-label")
                self.attach(label, 0, 1, row, row+1, xoptions=Gtk.AttachOptions.EXPAND|Gtk.AttachOptions.FILL)
            row = row + 1

    def add_controls(self, controls):
        row = 0
        for control in controls:
            self.attach(control, 1, 2, row, row+1)
            row = row + 1


class EditableEntry (Gtk.Notebook):

    __gsignals__ = {
        'changed': (GObject.SignalFlags.RUN_FIRST, None,
                    (str,))
    }

    PAGE_BUTTON = 0
    PAGE_ENTRY = 1

    def __init__ (self):
        super(EditableEntry, self).__init__()

        self.label = Gtk.Label()
        self.entry = Gtk.Entry()
        self.button = Gtk.Button()

        self.button.set_alignment(0.0, 0.5)
        self.button.set_relief(Gtk.ReliefStyle.NONE)
        self.append_page(self.button, None)
        self.append_page(self.entry, None)
        self.set_current_page(0)
        self.set_show_tabs(False)
        self.set_show_border(False)
        self.editable = False
        self.show_all()

        self.button.connect("released", self._on_button_clicked)
        self.button.connect("activate", self._on_button_clicked)
        self.entry.connect("activate", self._on_entry_validated)
        self.entry.connect("changed", self._on_entry_changed)

    def set_text(self, text):
        self.button.set_label(text)
        self.entry.set_text(text)

    def _on_button_clicked(self, button):
        self.set_editable(True)

    def _on_entry_validated(self, entry):
        self.set_editable(False)
        self.emit("changed", entry.get_text())

    def _on_entry_changed(self, entry):
        self.button.set_label(entry.get_text())

    def set_editable(self, editable):
        if editable:
            self.set_current_page(EditableEntry.PAGE_ENTRY)
        else:
            self.set_current_page(EditableEntry.PAGE_BUTTON)
        self.editable = editable

    def set_tooltip_text(self, tooltip):
        self.button.set_tooltip_text(tooltip)

    def get_editable(self):
        return self.editable

    def get_text(self):
        return self.entry.get_text()

class PasswordDialog(Gtk.Dialog):

    def __init__ (self, user, password_mask, group_mask, parent = None):
        super(PasswordDialog, self).__init__(None, parent)

        self.user = user
        self.password_mask = password_mask
        self.group_mask = group_mask

        self.set_modal(True)
        self.set_skip_taskbar_hint(True)
        self.set_skip_pager_hint(True)
        self.set_title(_("Change Password"))

        table = DimmedTable()
        table.add_labels([_("New password"), None, _("Confirm password")])

        self.new_password = Gtk.Entry()
        self.new_password.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "xsi-view-refresh-symbolic")
        self.new_password.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("Generate a password"))
        self.new_password.connect("icon-release", self._on_new_password_icon_released)
        self.new_password.connect("changed", self._on_passwords_changed)
        table.attach(self.new_password, 1, 3, 0, 1)

        self.strengh_indicator = Gtk.ProgressBar()
        self.strengh_indicator.set_tooltip_text(_("Your new password needs to be at least 8 characters long"))
        self.strengh_indicator.set_fraction(0.0)
        table.attach(self.strengh_indicator, 1, 2, 1, 2, xoptions=Gtk.AttachOptions.EXPAND|Gtk.AttachOptions.FILL)
        self.strengh_indicator.set_size_request(-1, 1)

        self.strengh_label = Gtk.Label()
        self.strengh_label.set_tooltip_text(_("Your new password needs to be at least 8 characters long"))
        self.strengh_label.set_alignment(1, 0.5)
        table.attach(self.strengh_label, 2, 3, 1, 2)

        self.confirm_password = Gtk.Entry()
        self.confirm_password.connect("changed", self._on_passwords_changed)
        table.attach(self.confirm_password, 1, 3, 2, 3)

        self.show_password = Gtk.CheckButton(_("Show password"))
        self.show_password.connect('toggled', self._on_show_password_toggled)
        table.attach(self.show_password, 1, 3, 3, 4)

        self.set_border_width(6)

        box = self.get_content_area()
        box.add(table)
        self.show_all()

        self.infobar = Gtk.InfoBar()
        self.infobar.set_message_type(Gtk.MessageType.ERROR)
        label = Gtk.Label(_("An error occurred. Your password was not changed."))
        content = self.infobar.get_content_area()
        content.add(label)
        table.attach(self.infobar, 0, 3, 4, 5)

        self.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, _("Change"), Gtk.ResponseType.OK, )

        self.set_passwords_visibility()
        self.set_response_sensitive(Gtk.ResponseType.OK, False)
        self.infobar.hide()

        self.connect("response", self._on_response)

    def _on_response(self, dialog, response_id):
        if response_id == Gtk.ResponseType.OK:
            self.change_password()
        else:
            self.destroy()

    def change_password(self):
        newpass = self.new_password.get_text()
        self.user.set_password(newpass, "")
        mask = self.group_mask.get_text()
        if "nopasswdlogin" in mask:
            subprocess.call(["gpasswd", "-d", self.user.get_user_name(), "nopasswdlogin"])
            mask = mask.split(", ")
            mask.remove("nopasswdlogin")
            mask = ", ".join(mask)
            self.group_mask.set_text(mask)
            self.password_mask.set_text('\u2022\u2022\u2022\u2022\u2022\u2022')
        self.destroy()

    def set_passwords_visibility(self):
        visible = self.show_password.get_active()
        self.new_password.set_visibility(visible)
        self.confirm_password.set_visibility(visible)

    def _on_new_password_icon_released(self, widget, icon_pos, event):
        self.infobar.hide()
        self.show_password.set_active(True)
        characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-"
        newpass = ""
        for i in range (8):
            index = randint(0, len(characters) -1)
            newpass = newpass + characters[index]

        self.new_password.set_text(newpass)
        self.confirm_password.set_text(newpass)
        self.check_passwords()

    def _on_show_password_toggled(self, widget):
        self.set_passwords_visibility()

    # Based on setPasswordStrength() in Mozilla Seamonkey, which is tri-licensed under MPL 1.1, GPL 2.0, and LGPL 2.1.
    # Forked from Ubiquity validation.py
    def password_strength(self, password):
        upper = lower = digit = symbol = 0
        for char in password:
            if char.isdigit():
                digit += 1
            elif char.islower():
                lower += 1
            elif char.isupper():
                upper += 1
            else:
                symbol += 1
        length = len(password)

        length = min(length,4)
        digit = min(digit,3)
        upper = min(upper,3)
        symbol = min(symbol,3)
        strength = (
            ((length * 0.1) - 0.2) +
            (digit * 0.1) +
            (symbol * 0.15) +
            (upper * 0.1))
        if strength > 1:
            strength = 1
        if strength < 0:
            strength = 0
        return strength

    def _on_passwords_changed(self, widget):
        self.infobar.hide()
        new_password = self.new_password.get_text()
        confirm_password = self.confirm_password.get_text()
        strength = self.password_strength(new_password)
        if new_password != confirm_password:
            self.confirm_password.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "xsi-dialog-warning-symbolic")
            self.confirm_password.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("Passwords do not match"))
        else:
            self.confirm_password.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, None)
        if len(new_password) < 8:
            self.strengh_label.set_text(_("Too short"))
            self.strengh_indicator.set_fraction(0.0)
        elif strength < 0.5:
            self.strengh_label.set_text(_("Weak"))
            self.strengh_indicator.set_fraction(0.2)
        elif strength < 0.75:
            self.strengh_label.set_text(_("Fair"))
            self.strengh_indicator.set_fraction(0.4)
        elif strength < 0.9:
            self.strengh_label.set_text(_("Good"))
            self.strengh_indicator.set_fraction(0.6)
        else:
            self.strengh_label.set_text(_("Strong"))
            self.strengh_indicator.set_fraction(1.0)

        self.check_passwords()

    def check_passwords(self):
        new_password = self.new_password.get_text()
        confirm_password = self.confirm_password.get_text()
        if len(new_password) >= 8 and new_password == confirm_password:
            self.set_response_sensitive(Gtk.ResponseType.OK, True)
        else:
            self.set_response_sensitive(Gtk.ResponseType.OK, False)

class NewUserDialog(Gtk.Dialog):

    def __init__ (self, parent = None):
        super(NewUserDialog, self).__init__(None, parent)

        try:
            self.set_modal(True)
            self.set_skip_taskbar_hint(True)
            self.set_skip_pager_hint(True)
            self.set_title("")

            self.account_type_combo = Gtk.ComboBoxText()
            self.account_type_combo.append_text(_("Standard"))
            self.account_type_combo.append_text(_("Administrator"))
            self.account_type_combo.set_active(0)

            self.realname_entry = Gtk.Entry()
            self.realname_entry.connect("changed", self._on_info_changed)

            self.username_entry = Gtk.Entry()
            self.username_entry.connect("changed", self._on_info_changed)

            table = DimmedTable()
            table.add_labels([_("Account Type"), _("Full Name"), _("Username")])
            table.add_controls([self.account_type_combo, self.realname_entry, self.username_entry])

            self.set_border_width(6)

            box = self.get_content_area()
            box.add(table)
            self.show_all()

            self.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, _("Add"), Gtk.ResponseType.OK, )
            self.set_response_sensitive(Gtk.ResponseType.OK, False)

        except Exception as detail:
            print(detail)

    def user_exists(self, user_name):
        users = AccountsService.UserManager.get_default().list_users()

        for user in users:
            if user.get_user_name() == user_name:
                return True

        return False

    def _on_info_changed(self, widget):
        fullname = self.realname_entry.get_text()
        username = self.username_entry.get_text()
        valid = True
        if re.search('[^a-z0-9_-]', username):
            self.username_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "xsi-dialog-warning-symbolic")
            self.username_entry.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("Invalid username"))
            valid = False
        elif self.user_exists(username):
            self.username_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "xsi-dialog-warning-symbolic")
            self.username_entry.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("A user with the name '%s' already exists.") % username)
            valid = False
        else:
            self.username_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, None)
        if username == "" or fullname == "":
            valid = False

        self.set_response_sensitive(Gtk.ResponseType.OK, valid)

class GroupsDialog(Gtk.Dialog):

    def __init__ (self, username, parent = None):
        super(GroupsDialog, self).__init__(None, parent)

        try:
            self.set_modal(True)
            self.set_skip_taskbar_hint(True)
            self.set_skip_pager_hint(True)
            self.set_title("")
            self.set_default_size(200, 480)

            scrolled = Gtk.ScrolledWindow()
            viewport = Gtk.Viewport()
            vbox = Gtk.VBox()
            self.checkboxes = []
            groups = sorted(grp.getgrall(), key=lambda x: x[0], reverse=False)
            for group in groups:
                checkbox = Gtk.CheckButton(group[0])
                self.checkboxes.append(checkbox)
                vbox.add(checkbox)
                if username in group[3]:
                    checkbox.set_active(True)

            viewport.add(vbox)
            scrolled.add(viewport)
            self.set_border_width(6)

            box = self.get_content_area()
            box.pack_start(scrolled, True, True, 0)
            self.show_all()

            self.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, _("OK"), Gtk.ResponseType.OK, )

        except Exception as detail:
            print(detail)

    def get_selected_groups(self):
        groups = []
        for checkbox in self.checkboxes:
            if checkbox.get_active():
                groups.append(checkbox.get_label())
        return groups

class Module:
    def __init__(self):
        try:
            self.builder = Gtk.Builder()
            self.builder.set_translation_domain('cinnamon') # let it translate!
            self.builder.add_from_file("/usr/share/cinnamon/cinnamon-settings-users/cinnamon-settings-users.ui")
            self.window = self.builder.get_object("main_window")
            self.window.connect("destroy", Gtk.main_quit)

            self.window.set_title(_("Users and Groups"))
            self.builder.get_object("label_users").set_label(_("Users"))
            self.builder.get_object("label_groups").set_label(_("Groups"))

            self.builder.get_object("button_add_user").connect("clicked", self.on_user_addition)
            self.builder.get_object("button_delete_user").connect("clicked", self.on_user_deletion)
            self.builder.get_object("button_add_group").connect("clicked", self.on_group_addition)
            self.builder.get_object("button_edit_group").connect("clicked", self.on_group_edition)
            self.builder.get_object("button_delete_group").connect("clicked", self.on_group_deletion)

            self.users = Gtk.TreeStore(object, GdkPixbuf.Pixbuf, str)
            self.users.set_sort_column_id(2, Gtk.SortType.ASCENDING)

            self.groups = Gtk.TreeStore(int, str)
            self.groups.set_sort_column_id(1, Gtk.SortType.ASCENDING)

            self.users_treeview = self.builder.get_object("treeview_users")
            self.users_treeview.set_rules_hint(True)

            self.groups_treeview = self.builder.get_object("treeview_groups")

            self.users_treeview.get_selection().connect("changed", self.on_user_selection)
            self.groups_treeview.get_selection().connect("changed", self.on_group_selection)

            column = Gtk.TreeViewColumn()
            cell = Gtk.CellRendererPixbuf()
            column.pack_start(cell, True)
            column.add_attribute(cell, 'pixbuf', INDEX_USER_PICTURE)
            cell.set_property('ypad', 1)
            self.users_treeview.append_column(column)

            column = Gtk.TreeViewColumn()
            cell = Gtk.CellRendererText()
            column.pack_start(cell, True)
            column.add_attribute(cell, 'markup', INDEX_USER_DESCRIPTION)
            self.users_treeview.append_column(column)

            column = Gtk.TreeViewColumn()
            cell = Gtk.CellRendererText()
            column.pack_start(cell, True)
            column.add_attribute(cell, 'text', INDEX_GROUPNAME)
            column.set_sort_column_id(1)
            self.groups_treeview.append_column(column)

            self.builder.get_object("button_delete_user").set_sensitive(False)
            self.builder.get_object("button_edit_group").set_sensitive(False)
            self.builder.get_object("button_delete_group").set_sensitive(False)

            self.face_button = Gtk.Button()
            self.face_image = Gtk.Image()
            self.face_image.set_size_request(96, 96)
            self.face_button.set_image(self.face_image)
            self.face_image.set_from_file("/usr/share/cinnamon/faces/user-generic.png")
            self.face_button.set_alignment(0.0, 0.5)
            self.face_button.set_tooltip_text(_("Click to change the picture"))

            self.menu = Gtk.Menu()

            separator = Gtk.SeparatorMenuItem()
            face_browse_menuitem = Gtk.MenuItem(_("Browse for more pictures..."))
            face_browse_menuitem.connect('activate', self._on_face_browse_menuitem_activated)
            self.face_button.connect("button-release-event", self.menu_display)

            row = 0
            col = 0
            num_cols = 4
            face_dirs = ["/usr/share/cinnamon/faces"]
            for face_dir in face_dirs:
                if os.path.exists(face_dir):
                    pictures = sorted(os.listdir(face_dir))
                    for picture in pictures:
                        path = os.path.join(face_dir, picture)
                        file = Gio.File.new_for_path(path)
                        file_icon = Gio.FileIcon.new(file)
                        image = Gtk.Image.new_from_gicon (file_icon, Gtk.IconSize.DIALOG)
                        menuitem = Gtk.MenuItem()
                        menuitem.add(image)
                        menuitem.connect('activate', self._on_face_menuitem_activated, path)
                        self.menu.attach(menuitem, col, col+1, row, row+1)
                        col = (col+1) % num_cols
                        if col == 0:
                            row = row + 1

            row = row + 1

            self.menu.attach(separator, 0, 4, row, row+1)
            self.menu.attach(face_browse_menuitem, 0, 4, row+2, row+3)

            self.account_type_combo = Gtk.ComboBoxText()
            self.account_type_combo.append_text(_("Standard"))
            self.account_type_combo.append_text(_("Administrator"))
            self.account_type_combo.connect("changed", self._on_accounttype_changed)

            self.realname_entry = EditableEntry()
            self.realname_entry.connect("changed", self._on_realname_changed)
            self.realname_entry.set_tooltip_text(_("Click to change the name"))

            self.password_mask = Gtk.Label()
            self.password_mask.set_alignment(0.0, 0.5)
            self.password_button = Gtk.Button()
            self.password_button.add(self.password_mask)
            self.password_button.set_relief(Gtk.ReliefStyle.NONE)
            self.password_button.set_tooltip_text(_("Click to change the password"))
            self.password_button.connect('activate', self._on_password_button_clicked)
            self.password_button.connect('released', self._on_password_button_clicked)

            self.groups_label = Gtk.Label()
            self.groups_label.set_line_wrap(True)
            self.groups_label.set_alignment(0, 0.5)
            self.groups_button = Gtk.Button()
            self.groups_button.add(self.groups_label)
            self.groups_button.set_relief(Gtk.ReliefStyle.NONE)
            self.groups_button.set_tooltip_text(_("Click to change the groups"))
            self.groups_button.connect("clicked", self._on_groups_button_clicked)

            box = Gtk.Box()
            box.pack_start(self.face_button, False, False, 0)

            table = DimmedTable()
            table.add_labels([_("Picture"), _("Account Type"), _("Name"), _("Password"), _("Groups")])
            table.add_controls([box, self.account_type_combo, self.realname_entry, self.password_button, self.groups_button])

            self.builder.get_object("box_users").add(table)

            self.accountService = AccountsService.UserManager.get_default()
            self.accountService.connect('notify::is-loaded', self.on_accounts_service_loaded)

            self.load_groups()

            self.window.show_all()

            self.builder.get_object("box_users").hide()

        except Exception as detail:
            print(detail)

    def _on_password_button_clicked(self, widget):
        model, treeiter = self.users_treeview.get_selection().get_selected()
        if treeiter is not None:
            user = model[treeiter][INDEX_USER_OBJECT]
            dialog = PasswordDialog(user, self.password_mask, self.groups_label, self.window)
            response = dialog.run()

    def _on_groups_button_clicked(self, widget):
        model, treeiter = self.users_treeview.get_selection().get_selected()
        if treeiter is not None:
            user = model[treeiter][INDEX_USER_OBJECT]
            dialog = GroupsDialog(user.get_user_name(), self.window)
            response = dialog.run()
            if response == Gtk.ResponseType.OK:
                groups = dialog.get_selected_groups()
                subprocess.call(["usermod", user.get_user_name(), "-G", ",".join(groups)])
                groups.sort()
                self.groups_label.set_text(", ".join(groups))
            dialog.destroy()

    def _on_accounttype_changed(self, combobox):
        model, treeiter = self.users_treeview.get_selection().get_selected()
        if treeiter is not None:
            user = model[treeiter][INDEX_USER_OBJECT]
            if self.account_type_combo.get_active() == 1:
                user.set_account_type(AccountsService.UserAccountType.ADMINISTRATOR)
            else:
                user.set_account_type(AccountsService.UserAccountType.STANDARD)

            groups = []
            for group in grp.getgrall():
                if user.get_user_name() in group[3]:
                    groups.append(group[0])
            groups.sort()
            self.groups_label.set_text(", ".join(groups))

    def _on_realname_changed(self, widget, text):
        model, treeiter = self.users_treeview.get_selection().get_selected()
        if treeiter is not None:
            user = model[treeiter][INDEX_USER_OBJECT]
            user.set_real_name(text)
            description = "<b>%s</b>\n%s" % (GLib.markup_escape_text(text), GLib.markup_escape_text(user.get_user_name()))
            model.set_value(treeiter, INDEX_USER_DESCRIPTION, description)

    def _on_face_browse_menuitem_activated(self, menuitem):
        model, treeiter = self.users_treeview.get_selection().get_selected()
        if treeiter is not None:
            user = model[treeiter][INDEX_USER_OBJECT]
            dialog = Gtk.FileChooserDialog(None, None, Gtk.FileChooserAction.OPEN, (_("Cancel"), Gtk.ResponseType.CANCEL, _("Open"), Gtk.ResponseType.OK))
            filter = Gtk.FileFilter()
            filter.set_name(_("Images"))
            filter.add_mime_type("image/*")
            dialog.add_filter(filter)

            box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
            self.frame = Gtk.Frame(visible=False, no_show_all=True)
            preview = Gtk.Image(visible=True)

            box.pack_start(self.frame, False, False, 0)
            self.frame.add(preview)
            dialog.set_preview_widget(box)
            dialog.set_preview_widget_active(True)
            dialog.set_use_preview_label(False)

            box.set_margin_end(12)
            box.set_margin_top(12)
            box.set_size_request(128, -1)

            dialog.connect("update-preview", self.update_preview_cb, preview)

            response = dialog.run()
            if response == Gtk.ResponseType.OK:
                path = dialog.get_filename()
                image = PIL.Image.open(path)
                image.thumbnail((96, 96), Image.LANCZOS)
                face_path = os.path.join(user.get_home_dir(), ".face")
                try:
                    try:
                        os.remove(face_path)
                    except OSError:
                        pass
                    priv_helper.drop_privs(user)
                    image.save(face_path, "png")
                finally:
                    priv_helper.restore_privs()
                user.set_icon_file(face_path)
                self.face_image.set_from_file(face_path)
                model.set_value(treeiter, INDEX_USER_PICTURE, GdkPixbuf.Pixbuf.new_from_file_at_size(face_path, 48, 48))
                model.row_changed(model.get_path(treeiter), treeiter)

            dialog.destroy()

    def update_preview_cb (self, dialog, preview):
        # Different widths make the dialog look really crappy as it resizes -
        # constrain the width and adjust the height to keep perspective.
        filename = dialog.get_preview_filename()
        if filename is not None:
            if os.path.isfile(filename):
                try:
                    pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(filename, 128, 128)
                    if pixbuf is not None:
                        preview.set_from_pixbuf(pixbuf)
                        self.frame.show()
                        return
                except GLib.Error as e:
                    print("Unable to generate preview for file '%s' - %s\n" % (filename, e.message))

        preview.clear()
        self.frame.hide()

    def _on_face_menuitem_activated(self, menuitem, path):
        if os.path.exists(path):
            model, treeiter = self.users_treeview.get_selection().get_selected()
            if treeiter is not None:
                user = model[treeiter][INDEX_USER_OBJECT]
                user.set_icon_file(path)
                self.face_image.set_from_file(path)
                face_path = os.path.join(user.get_home_dir(), ".face")
                try:
                    try:
                        os.remove(face_path)
                    except OSError:
                        pass
                    priv_helper.drop_privs(user)
                    shutil.copy(path, face_path)
                finally:
                    priv_helper.restore_privs()
                model.set_value(treeiter, INDEX_USER_PICTURE, GdkPixbuf.Pixbuf.new_from_file_at_size(path, 48, 48))
                model.row_changed(model.get_path(treeiter), treeiter)


    def menu_display(self, widget, event):
        if event.button == 1:
            self.menu.popup(None, None, self.popup_menu_below_button, self.face_button, event.button, event.time)
            self.menu.show_all()

    def popup_menu_below_button (self, *args):
        # the introspection for GtkMenuPositionFunc seems to change with each Gtk version,
        # this is a workaround to make sure we get the menu and the widget
        menu = args[0]
        widget = args[-1]

        # here I get the coordinates of the button relative to
        # window (self.window)
        button_x, button_y = widget.get_allocation().x, widget.get_allocation().y

        # now convert them to X11-relative
        unused_var, window_x, window_y = widget.get_window().get_origin()
        x = window_x + button_x
        y = window_y + button_y

        # now move the menu below the button
        y += widget.get_allocation().height

        push_in = True # push_in is True so all menu is always inside screen
        return x, y, push_in

    def on_accounts_service_loaded(self, user, param):
        self.load_users()

    def load_users(self):
        self.users.clear()
        users = self.accountService.list_users()
        for user in users:
            if os.path.exists(user.get_icon_file()):
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(user.get_icon_file(), 48, 48)
            else:
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size("/usr/share/cinnamon/faces/user-generic.png", 48, 48)

            real_name = GLib.markup_escape_text(user.get_real_name())
            user_name = GLib.markup_escape_text(user.get_user_name())
            description = f"<b>{real_name}</b>\n{user_name}"
            piter = self.users.append(None, [user, pixbuf, description])
        self.users_treeview.set_model(self.users)

    def load_groups(self):
        self.groups.clear()
        groups = sorted(grp.getgrall(), key=lambda x: x[0], reverse=False)
        for group in groups:
            (gr_name, gr_passwd, gr_gid, gr_mem) = group
            piter = self.groups.append(None, [gr_gid, gr_name])
        self.groups_treeview.set_model(self.groups)

#USER CALLBACKS

    def on_user_selection(self, selection):
        self.password_button.set_sensitive(True)
        self.password_button.set_tooltip_text("")

        model, treeiter = selection.get_selected()
        if treeiter is not None:
            user = model[treeiter][INDEX_USER_OBJECT]
            self.builder.get_object("button_delete_user").set_sensitive(True)
            self.realname_entry.set_text(user.get_real_name())

            if user.get_password_mode() == AccountsService.UserPasswordMode.REGULAR:
                self.password_mask.set_text('\u2022\u2022\u2022\u2022\u2022\u2022')
            elif user.get_password_mode() == AccountsService.UserPasswordMode.NONE:
                self.password_mask.set_markup("<b>%s</b>" % _("No password set"))
            else:
                self.password_mask.set_text(_("Set at login"))

            if user.get_account_type() == AccountsService.UserAccountType.ADMINISTRATOR:
                self.account_type_combo.set_active(1)
            else:
                self.account_type_combo.set_active(0)

            pixbuf = None
            path = user.get_icon_file()
            message = ""

            if os.path.exists(path):
                try:
                    pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
                except GLib.Error as e:
                    message = "Could not load pixbuf from '%s': %s" % (path, e.message)
                    error = True

                if pixbuf is not None:
                    if pixbuf.get_height() > 96 or pixbuf.get_width() > 96:
                        try:
                            pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(path, 96, 96)
                        except GLib.Error as e:
                            message = "Could not scale pixbuf from '%s': %s" % (path, e.message)
                            error = True

            if pixbuf:
                self.face_image.set_from_pixbuf(pixbuf)
            else:
                if message != "":
                    print(message)
                self.face_image.set_from_file("/usr/share/cinnamon/faces/user-generic.png")

            groups = []
            for group in grp.getgrall():
                if user.get_user_name() in group[3]:
                    groups.append(group[0])
            groups.sort()
            self.groups_label.set_text(", ".join(groups))
            self.builder.get_object("box_users").show()

            # Count the number of connections for the currently logged-in user
            connections = int(subprocess.check_output(["w", "-h", user.get_user_name()]).decode("utf-8").count("\n"))
            if connections > 0:
                self.builder.get_object("button_delete_user").set_sensitive(False)
                self.builder.get_object("button_delete_user").set_tooltip_text(_("This user is currently logged in"))
            else:
                self.builder.get_object("button_delete_user").set_sensitive(True)
                self.builder.get_object("button_delete_user").set_tooltip_text("")

            if os.path.exists("/home/.ecryptfs/%s" % user.get_user_name()):
                self.password_button.set_sensitive(False)
                self.password_button.set_tooltip_text(_("The user's home directory is encrypted. To preserve access to the encrypted directory, only the user should change this password."))

        else:
            self.builder.get_object("button_delete_user").set_sensitive(False)
            self.builder.get_object("box_users").hide()

    def on_user_deletion(self, event):
        model, treeiter = self.users_treeview.get_selection().get_selected()
        if treeiter is not None:
            user = model[treeiter][INDEX_USER_OBJECT]
            message = _("Are you sure you want to permanently delete %s and all the files associated with this user?") % user.get_user_name()
            d = Gtk.MessageDialog(self.window,
                                  Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
                                  Gtk.MessageType.QUESTION,
                                  Gtk.ButtonsType.YES_NO,
                                  message)
            d.set_markup(message)
            d.set_default_response(Gtk.ResponseType.NO)
            r = d.run()
            d.destroy()
            if r == Gtk.ResponseType.YES:
                result = self.accountService.delete_user(user, True)
                if result:
                    model.remove(treeiter)
                    self.load_groups()

    def on_user_addition(self, event):
        dialog = NewUserDialog(self.window)
        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            if dialog.account_type_combo.get_active() == 1:
                account_type = AccountsService.UserAccountType.ADMINISTRATOR
            else:
                account_type = AccountsService.UserAccountType.STANDARD
            fullname = dialog.realname_entry.get_text()
            username = dialog.username_entry.get_text()
            new_user = self.accountService.create_user(username, fullname, account_type)
            new_user.set_password_mode(AccountsService.UserPasswordMode.NONE)
            pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size("/usr/share/cinnamon/faces/user-generic.png", 48, 48)
            description = "<b>%s</b>\n%s" % (GLib.markup_escape_text(fullname), GLib.markup_escape_text(username))
            piter = self.users.append(None, [new_user, pixbuf, description])
            # Add the user to his/her own group and sudo if Administrator was selected
            if dialog.account_type_combo.get_active() == 1:
                subprocess.call(["usermod", username, "-G", "%s,sudo,nopasswdlogin" % username])
            else:
                subprocess.call(["usermod", username, "-G", "%s,nopasswdlogin" % username])
            self.load_groups()
        dialog.destroy()

    def on_user_edition(self, event):
        model, treeiter = self.users_treeview.get_selection().get_selected()
        if treeiter is not None:
            print("Editing user %s" % model[treeiter][INDEX_USER_OBJECT].get_user_name())

# GROUPS CALLBACKS

    def on_group_selection(self, selection):
        model, treeiter = selection.get_selected()
        if treeiter is not None:
            self.builder.get_object("button_edit_group").set_sensitive(True)
            self.builder.get_object("button_delete_group").set_sensitive(True)
            self.builder.get_object("button_delete_group").set_tooltip_text("")
            group = model[treeiter][INDEX_GROUPNAME]
            for p in pwd.getpwall():
                username = p[0]
                primary_group = grp.getgrgid(p[3])[0]
                if primary_group == group:
                    self.builder.get_object("button_delete_group").set_sensitive(False)
                    self.builder.get_object("button_delete_group").set_tooltip_text(_("This group is set as %s's primary group") % username)
                    break

        else:
            self.builder.get_object("button_edit_group").set_sensitive(False)
            self.builder.get_object("button_delete_group").set_sensitive(False)
            self.builder.get_object("button_delete_group").set_tooltip_text("")

    def on_group_deletion(self, event):
        model, treeiter = self.groups_treeview.get_selection().get_selected()
        if treeiter is not None:
            group = model[treeiter][INDEX_GROUPNAME]
            message = _("Are you sure you want to permanently delete %s?") % group
            d = Gtk.MessageDialog(self.window,
                                  Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
                                  Gtk.MessageType.QUESTION,
                                  Gtk.ButtonsType.YES_NO,
                                  message)
            d.set_markup(message)
            d.set_default_response(Gtk.ResponseType.NO)
            r = d.run()
            if r == Gtk.ResponseType.YES:
                subprocess.call(["groupdel", group])
                self.load_groups()
            d.destroy()

    def on_group_addition(self, event):
        dialog = GroupDialog(_("Group Name"), "", self.window)
        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            subprocess.call(["groupadd", dialog.entry.get_text().lower()])
            self.load_groups()
        dialog.destroy()

    def on_group_edition(self, event):
        model, treeiter = self.groups_treeview.get_selection().get_selected()
        if treeiter is not None:
            group = model[treeiter][INDEX_GROUPNAME]
            dialog = GroupDialog(_("Group Name"), group, self.window)
            response = dialog.run()
            if response == Gtk.ResponseType.OK:
                subprocess.call(["groupmod", group, "-n", dialog.entry.get_text().lower()])
                self.load_groups()
            dialog.destroy()


if __name__ == "__main__":
    setproctitle("cinnamon-settings-users")
    module = Module()
    Gtk.main()
