Commit a8cbf4ca authored by segfault's avatar segfault

Greeter: Refactor: Redesign how settings are saved and loaded

Settings are now saved to file immediately when the user changes them,
instead of before login. Beside making the code simpler, this makes it
easier to handle saving the admin password file (which we want to delete
if the user disabled the feature after loading a persistent password
file, but want to overwrite or leave untouched in other cases).
parent a047e3be
......@@ -71,7 +71,6 @@ class GreeterApplication(object):
persistence = PersistenceSettings()
self.localisationsettings = LocalisationSettings(
usermanager_loaded_cb=self.usermanager_loaded,
locale_selected_cb=self.on_language_changed
)
self.admin_setting = AdminSetting()
self.network_setting = NetworkSetting()
......@@ -79,7 +78,7 @@ class GreeterApplication(object):
# Initialize the settings
self.settings = GreeterSettingsCollection(
LanguageSettingUI(self.localisationsettings.language),
LanguageSettingUI(self.localisationsettings.language, self.on_language_changed),
KeyboardSettingUI(self.localisationsettings.keyboard),
FormatsSettingUI(self.localisationsettings.formats),
AdminSettingUI(self.admin_setting),
......@@ -90,6 +89,10 @@ class GreeterApplication(object):
# Initialize main window
self.mainwindow = GreeterMainWindow(self, persistence, self.settings)
# Apply the default settings
for setting in self.settings:
setting.apply()
# Inhibit the session being marked as idle
self.inhibit_idle()
......@@ -97,38 +100,15 @@ class GreeterApplication(object):
"""Translate all windows to target language"""
TranslatableWindow.translate_all(lang)
def load_settings(self):
if self.localisationsettings.language.load():
self.settings["language"].selected_code = self.localisationsettings.language.value
self.settings["language"].apply()
if self.localisationsettings.formats.load():
self.settings["formats"].selected_code = self.localisationsettings.formats.value
self.settings["formats"].apply()
if self.localisationsettings.keyboard.load():
self.settings["keyboard"].selected_code = self.localisationsettings.keyboard.value
self.settings["keyboard"].apply()
if self.admin_setting.load():
self.settings["admin"].password = self.admin_setting.password
self.mainwindow.add_setting("admin")
if self.network_setting.load():
if self.network_setting.value != self.settings["network"].value:
self.settings["network"].value = self.network_setting.value
self.mainwindow.add_setting("network")
if self.macspoof_setting.load():
if self.settings["macspoof"].spoofing_enabled != self.macspoof_setting.value:
self.settings["macspoof"].spoofing_enabled = self.macspoof_setting.value
self.mainwindow.add_setting("macspoof")
def login(self):
"""Login GDM to the server"""
logging.debug("login called")
# Apply settings
# We now apply all settings immediately when they are
# changed. The only thing that still happens here is
# concatenating the locale settings files.
self.localisationsettings.apply_to_upcoming_session()
self.admin_setting.apply_to_upcoming_session()
self.macspoof_setting.apply_to_upcoming_session()
self.network_setting.apply_to_upcoming_session()
self.mainwindow.hide()
self.gdmclient.do_login()
......@@ -142,10 +122,9 @@ class GreeterApplication(object):
def on_language_changed(self, locale_code: str):
"""Translate to the given locale"""
self.localisationsettings.formats.on_language_changed(locale_code) # XXX: notify
self.settings["formats"].update_value_label()
self.localisationsettings.keyboard.on_language_changed(locale_code) # XXX: notify
self.settings["keyboard"].update_value_label()
for setting in self.settings.region_settings:
setting.on_language_changed(locale_code)
self.translate_to(locale_code)
self.mainwindow.current_language = localization.language_from_locale(locale_code)
......
......@@ -11,45 +11,38 @@ from tailsgreeter.settings.utils import read_settings, write_settings
class AdminSetting(object):
"""Setting controlling the sudo password"""
def __init__(self):
self.password = None
self.settings_file = tailsgreeter.config.admin_password_path
def apply_to_upcoming_session(self):
if self.password:
proc = subprocess.run(
["mkpasswd", "-s", "--method=sha512crypt"],
input=pipes.quote(self.password).encode(),
capture_output=True,
check=True,
)
hashed_and_salted_pw = proc.stdout.decode().strip()
write_settings(self.settings_file, {
'TAILS_USER_PASSWORD': pipes.quote(hashed_and_salted_pw),
})
logging.debug('password written to %s', self.settings_file)
return
settings_file = tailsgreeter.config.admin_password_path
def save(self, password: str):
proc = subprocess.run(
["mkpasswd", "-s", "--method=sha512crypt"],
input=pipes.quote(password).encode(),
capture_output=True,
check=True,
)
hashed_and_salted_pw = proc.stdout.decode().strip()
write_settings(self.settings_file, {
'TAILS_USER_PASSWORD': pipes.quote(hashed_and_salted_pw),
})
logging.debug('password written to %s', self.settings_file)
def delete(self):
# Try to remove the password file
try:
os.unlink(self.settings_file)
logging.debug('removed %s', self.settings_file)
except OSError:
# It's bad if the file exists and couldn't be removed, so we
# we raise the exception in that case (which prevents the login)
# we raise the exception in that case
if os.path.exists(self.settings_file):
raise
def load(self) -> bool:
def load(self) -> {str, None}:
try:
settings = read_settings(self.settings_file)
except FileNotFoundError:
logging.debug("No persistent admin settings file found (path: %s)", self.settings_file)
return False
return None
password = settings.get('TAILS_USER_PASSWORD')
if password:
self.password = password
logging.debug("Loaded admin password setting")
return True
return settings.get('TAILS_USER_PASSWORD')
import gi
import logging
import os
import tailsgreeter.config
from tailsgreeter.settings.localization import LocalizationSetting, language_from_locale, country_from_locale
......@@ -15,30 +14,30 @@ from gi.repository import GObject, GnomeDesktop, Gtk
class FormatsSetting(LocalizationSetting):
def __init__(self, language_codes: [str]):
super().__init__()
self.value = 'en_US'
self.locales_per_country = self._make_locales_per_country_dict(language_codes)
self.settings_file = tailsgreeter.config.formats_setting_path
def apply_to_upcoming_session(self):
def save(self, locale: str, is_default: bool):
write_settings(self.settings_file, {
'TAILS_FORMATS': self.get_value(),
'IS_DEFAULT': str(not self.value_changed_by_user).lower(),
'TAILS_FORMATS': locale,
'IS_DEFAULT': str(is_default).lower(),
})
def load(self) -> bool:
def load(self) -> ({str, None}, bool):
try:
settings = read_settings(self.settings_file)
except FileNotFoundError:
logging.debug("No persistent formats settings file found (path: %s)", self.settings_file)
return False
return None, False
formats = settings.get('TAILS_FORMATS')
if not formats:
return False
if formats is None:
logging.debug("No formats setting found in settings file (path: %s)", self.settings_file)
return None, False
is_default = settings.get('IS_DEFAULT') == 'true'
self.set_value(formats, chosen_by_user=not is_default)
logging.debug("Loaded formats setting '%s' (is default: %s)", formats, is_default)
return True
return formats, is_default
def get_tree(self) -> Gtk.TreeStore:
treestore = Gtk.TreeStore(GObject.TYPE_STRING, # id
......@@ -65,8 +64,8 @@ class FormatsSetting(LocalizationSetting):
treestore.set(treeiter_locale, 1, self._locale_name(locale))
return treestore
def get_name(self) -> str:
return self._locale_name(self.get_value())
def get_name(self, locale: str) -> str:
return self._locale_name(locale)
def get_default_locale(self, country_code=None) -> str:
"""Return default locale for given country
......@@ -141,12 +140,3 @@ class FormatsSetting(LocalizationSetting):
if language_code not in res[country_code]:
res[country_code].append(language_code)
return res
def on_language_changed(self, language_code: str):
"""Set the formats according to the new language"""
# Don't overwrite user chosen values
if self.value_changed_by_user:
return
logging.debug("setting formats to %s", language_code)
self.set_value(language_code)
......@@ -19,14 +19,13 @@ class KeyboardSetting(LocalizationSetting):
def __init__(self):
super().__init__()
self.xkbinfo = GnomeDesktop.XkbInfo()
self.value = 'us'
self.settings_file = tailsgreeter.config.keyboard_setting_path
def apply_to_upcoming_session(self):
def save(self, value: str, is_default: bool):
try:
layout, variant = self.get_value().split('+')
layout, variant = value.split('+')
except ValueError:
layout = self.get_value()
layout = value
variant = ''
write_settings(self.settings_file, {
......@@ -34,29 +33,28 @@ class KeyboardSetting(LocalizationSetting):
'TAILS_XKBMODEL': 'pc105',
'TAILS_XKBLAYOUT': layout,
'TAILS_XKBVARIANT': variant,
'IS_DEFAULT': str(not self.value_changed_by_user).lower(),
'IS_DEFAULT': str(is_default).lower(),
})
def load(self) -> bool:
def load(self) -> ({str, None}, bool):
try:
settings = read_settings(self.settings_file)
except FileNotFoundError:
logging.debug("No persistent keyboard settings file found (path: %s)", self.settings_file)
return False
return None, False
keyboard_layout = settings.get('TAILS_XKBLAYOUT')
if not keyboard_layout:
return False
if keyboard_layout is None:
logging.debug("No keyboard setting found in settings file (path: %s)", self.settings_file)
return None, False
keyboard_variant = settings.get('TAILS_XKBVARIANT')
if keyboard_variant:
keyboard_layout += "+" + keyboard_variant
is_default = settings.get('IS_DEFAULT') == 'true'
self.set_value(keyboard_layout, chosen_by_user=not is_default)
logging.debug("Loaded keyboard setting '%s' (is default: %s)", keyboard_layout, is_default)
return True
return keyboard_layout, is_default
def get_tree(self, layout_codes=None) -> Gtk.TreeStore:
if not layout_codes:
......@@ -79,8 +77,8 @@ class KeyboardSetting(LocalizationSetting):
treestore.set(treeiter_layout, 1, self._layout_name(layout_code))
return treestore
def get_name(self) -> str:
return self._layout_name(self.get_value())
def get_name(self, value: str) -> str:
return self._layout_name(value)
def get_all(self) -> [str]:
"""Return a list of all keyboard layout codes
......@@ -88,11 +86,6 @@ class KeyboardSetting(LocalizationSetting):
"""
return self.xkbinfo.get_all_layouts()
def set_value(self, layout, chosen_by_user=False):
super().set_value(layout)
self.value_changed_by_user = chosen_by_user
self._apply_layout_to_current_screen()
def _layout_name(self, layout_code) -> str:
layout_exists, display_name, short_name, xkb_layout, xkb_variant = \
self.xkbinfo.get_layout_info(layout_code)
......@@ -192,13 +185,7 @@ class KeyboardSetting(LocalizationSetting):
layouts = filtered_layouts
return layouts
def on_language_changed(self, locale: str):
"""Set the keyboard layout according to the new language"""
# Don't overwrite a user chosen value
if self.value_changed_by_user:
return
def get_layout_for_locale(self, locale: str):
language = language_from_locale(locale)
country = country_from_locale(locale)
......@@ -243,11 +230,10 @@ class KeyboardSetting(LocalizationSetting):
else:
default_layout = 'us'
logging.debug("Using us as fallback default layout")
self.set_value(default_layout)
return default_layout
def _apply_layout_to_current_screen(self):
layout = self.get_value()
logging.debug("layout=%s", layout)
def apply_layout_to_current_screen(self, layout: str):
logging.debug("applying keyboard layout '%s'", layout)
settings = Gio.Settings('org.gnome.desktop.input-sources')
settings.set_value('sources', GLib.Variant('a(ss)', [('xkb', layout)]))
......@@ -21,7 +21,7 @@ from collections import OrderedDict
import gi
import logging
import locale
from typing import Callable
import pipes
import tailsgreeter.config
from tailsgreeter.settings.localization import LocalizationSetting, \
......@@ -37,11 +37,9 @@ from gi.repository import GLib, GObject, GnomeDesktop, Gtk
class LanguageSetting(LocalizationSetting):
def __init__(self, locales: [str], language_changed_cb: Callable):
def __init__(self, locales: [str]):
super().__init__()
self.value = 'en_US'
self.locales = locales
self.language_changed_cb = language_changed_cb
self._user_account = None
self.settings_file = tailsgreeter.config.language_setting_path
......@@ -49,26 +47,27 @@ class LanguageSetting(LocalizationSetting):
self.locales_per_language = self._make_language_to_locale_dict(locales)
self.language_names_per_language = self._make_language_to_language_name_dict(self.lang_codes)
def apply_to_upcoming_session(self):
def save(self, language: str, is_default: bool):
write_settings(self.settings_file, {
'TAILS_LOCALE_NAME': self.get_value(),
'IS_DEFAULT': str(not self.value_changed_by_user).lower(),
'TAILS_LOCALE_NAME': pipes.quote(language),
'IS_DEFAULT': str(is_default).lower(),
})
def load(self) -> bool:
def load(self) -> ({str, None}, bool):
try:
settings = read_settings(self.settings_file)
except FileNotFoundError:
logging.debug("No persistent language settings file found (path: %s)", self.settings_file)
return False
return None, False
language = settings.get('TAILS_LOCALE_NAME')
if not language:
return False
if language is None:
logging.debug("No language setting found in settings file (path: %s)", self.settings_file)
return None, False
is_default = settings.get('IS_DEFAULT') == 'true'
self.set_value(language, chosen_by_user=not is_default)
logging.debug("Loaded language setting '%s' (is default: %s)", language, is_default)
return True
return language, is_default
def get_tree(self) -> Gtk.TreeStore:
treestore = Gtk.TreeStore(GObject.TYPE_STRING, # id
......@@ -92,8 +91,8 @@ class LanguageSetting(LocalizationSetting):
treestore.set(treeiter_locale, 1, self._locale_name(locale_code))
return treestore
def get_name(self) -> str:
return self._locale_name(self.get_value())
def get_name(self, value: str) -> str:
return self._locale_name(value)
def get_default_locale(self, lang_code: str) -> str:
"""Try to find a default locale for the given language
......@@ -114,11 +113,6 @@ class LanguageSetting(LocalizationSetting):
return locales[0]
def set_value(self, locale_code: str, chosen_by_user=False):
super().set_value(locale_code, chosen_by_user)
self._apply_language(locale_code)
self.language_changed_cb(locale_code)
def _language_name(self, lang_code: str) -> str:
default_locale = 'C'
......@@ -189,7 +183,7 @@ class LanguageSetting(LocalizationSetting):
except AttributeError:
return locale_code
def _apply_language(self, language_code: str):
def apply_language(self, language_code: str):
normalized_code = locale.normalize(language_code + '.' + locale.getpreferredencoding())
logging.debug("Setting session language to %s", normalized_code)
if self._user_account:
......
......@@ -38,16 +38,18 @@ class LocalizationSetting(GObject.Object, object):
def get_value(self) -> str:
return self.value
def set_value(self, value, chosen_by_user=False):
self.value = value
self.value_changed_by_user = chosen_by_user
def get_name(self) -> str:
def get_name(self, value: str) -> str:
raise NotImplementedError
def get_tree(self) -> "Gtk.Treestore":
raise NotImplementedError
def save(self, value: str, is_default: bool):
pass
def load(self) -> ({str, None}, bool):
pass
def ln_iso639_tri(ln_CC):
"""get iso639 3-letter code from a language code
......
......@@ -16,7 +16,7 @@ class LocalisationSettings(object):
"""Controller for localisation settings
"""
def __init__(self, usermanager_loaded_cb: Callable, locale_selected_cb: Callable):
def __init__(self, usermanager_loaded_cb: Callable):
self._usermanager_loaded_cb = usermanager_loaded_cb
self._user_account = None
......@@ -28,7 +28,7 @@ class LocalisationSettings(object):
self._actusermanager_loadedid = self._actusermanager.connect(
"notify::is-loaded", self.__on_usermanager_loaded)
self.language = LanguageSetting(locales, locale_selected_cb)
self.language = LanguageSetting(locales)
self.keyboard = KeyboardSetting()
self.formats = FormatsSetting(locales)
......@@ -53,10 +53,6 @@ class LocalisationSettings(object):
self._usermanager_loaded_cb()
def apply_to_upcoming_session(self):
self.language.apply_to_upcoming_session()
self.formats.apply_to_upcoming_session()
self.keyboard.apply_to_upcoming_session()
with open(tailsgreeter.config.locale_setting_path, 'w') as outfile:
for path in (tailsgreeter.config.language_setting_path,
tailsgreeter.config.formats_setting_path,
......
......@@ -9,24 +9,25 @@ class MacSpoofSetting(object):
"""Setting controlling whether the MAC address is spoofed or not"""
def __init__(self):
self.value = True
self.settings_file = tailsgreeter.config.macspoof_setting_path
def apply_to_upcoming_session(self):
def save(self, value: bool):
write_settings(self.settings_file, {
'TAILS_MACSPOOF_ENABLED': pipes.quote(str(self.value)).lower(),
'TAILS_MACSPOOF_ENABLED': pipes.quote(str(value)).lower(),
})
logging.debug('macspoof setting written to %s', self.settings_file)
def load(self) -> bool:
def load(self) -> {bool, None}:
try:
settings = read_settings(self.settings_file)
except FileNotFoundError:
logging.debug("No persistent macspoof settings file found (path: %s)", self.settings_file)
return False
return None
value = settings.get('TAILS_MACSPOOF_ENABLED') == "true"
if value:
self.value = value
logging.debug("Loaded macspoof setting '%s'", value)
return True
value_str = settings.get('TAILS_MACSPOOF_ENABLED')
if value_str is None:
logging.debug("No macspoof setting found in settings file (path: %s)", self.settings_file)
return None
value = value_str == "true"
logging.debug("Loaded macspoof setting '%s'", value)
return value
......@@ -4,33 +4,34 @@ import pipes
import tailsgreeter.config
from tailsgreeter.settings.utils import read_settings, write_settings
NETCONF_DIRECT = "direct"
NETCONF_OBSTACLE = "obstacle"
NETCONF_DISABLED = "disabled"
class NetworkSetting(object):
"""Setting controlling how Tails connects to Tor"""
NETCONF_DIRECT = "direct"
NETCONF_OBSTACLE = "obstacle"
NETCONF_DISABLED = "disabled"
def __init__(self):
self.value = self.NETCONF_DIRECT
self.settings_file = tailsgreeter.config.network_setting_path
def apply_to_upcoming_session(self):
def save(self, value: str):
write_settings(self.settings_file, {
'TAILS_NETCONF': pipes.quote(self.value),
'TAILS_NETCONF': pipes.quote(value),
})
logging.debug('network setting written to %s', self.settings_file)
def load(self) -> bool:
def load(self) -> {bool, None}:
try:
settings = read_settings(self.settings_file)
except FileNotFoundError:
logging.debug("No persistent network settings file found (path: %s)", self.settings_file)
return False
return None
value = settings.get('TAILS_NETCONF')
if value:
self.value = value
logging.debug("Loaded network setting '%s'", value)
return True
if value is None:
logging.debug("No network setting found in settings file (path: %s)", self.settings_file)
return None
logging.debug("Loaded network setting '%s'", value)
return value
import os
from typing import Dict
def write_settings(filename: str, settings: dict):
def write_settings(filename: str, settings: Dict[str, str]):
with open(filename, 'w') as f:
os.chmod(filename, 0o600)
for key, value in settings.items():
f.write('%s=%s\n' % (key, value))
def read_settings(filename: str) -> dict:
def read_settings(filename: str) -> Dict[str, str]:
with open(filename) as f:
lines = f.readlines()
......
......@@ -3,6 +3,7 @@ import gi
from tailsgreeter import TRANSLATION_DOMAIN
import tailsgreeter.config
import tailsgreeter.utils
from tailsgreeter.settings.network import NETCONF_DIRECT, NETCONF_DISABLED, NETCONF_OBSTACLE
from tailsgreeter.ui import _
from tailsgreeter.ui.setting import GreeterSetting