From 2716b292b9d42961222fd645c6b2d281d46d6688 Mon Sep 17 00:00:00 2001 From: Dmitry Samoylik Date: Thu, 26 Sep 2024 16:57:48 +0300 Subject: [PATCH] Implement show EULA before installation --- data/anaconda.conf | 4 +- pyanaconda/core/configuration/license.py | 2 - pyanaconda/core/eula.py | 23 ++++ pyanaconda/ui/categories/eula.py | 14 +++ pyanaconda/ui/gui/spokes/eula.glade | 136 +++++++++++++++++++++++ pyanaconda/ui/gui/spokes/eula.py | 107 ++++++++++++++++++ pyanaconda/ui/tui/spokes/eula.py | 128 +++++++++++++++++++++ 7 files changed, 409 insertions(+), 5 deletions(-) create mode 100644 pyanaconda/core/eula.py create mode 100644 pyanaconda/ui/categories/eula.py create mode 100644 pyanaconda/ui/gui/spokes/eula.glade create mode 100644 pyanaconda/ui/gui/spokes/eula.py create mode 100644 pyanaconda/ui/tui/spokes/eula.py diff --git a/data/anaconda.conf b/data/anaconda.conf index b5878e3..c15e99d 100644 --- a/data/anaconda.conf +++ b/data/anaconda.conf @@ -308,9 +308,7 @@ password_policies = # If the given distribution has an EULA & feels the need to # tell the user about it fill in this variable by a path # pointing to a file with the EULA on the installed system. -# -# This is currently used just to show the path to the file to -# the user at the end of the installation. + eula = diff --git a/pyanaconda/core/configuration/license.py b/pyanaconda/core/configuration/license.py index 04c44bf..a51f52a 100644 --- a/pyanaconda/core/configuration/license.py +++ b/pyanaconda/core/configuration/license.py @@ -31,7 +31,5 @@ class LicenseSection(Section): tell the user about it fill in this variable by a path pointing to a file with the EULA on the installed system. - This is currently used just to show the path to the file to - the user at the end of the installation. """ return self._get_option("eula", str) diff --git a/pyanaconda/core/eula.py b/pyanaconda/core/eula.py new file mode 100644 index 0000000..15a393e --- /dev/null +++ b/pyanaconda/core/eula.py @@ -0,0 +1,23 @@ +import os +from pyanaconda.core.configuration.anaconda import conf + +def get_license_file_name(): + """Get filename of the license file best matching current localization settings. + :return: filename of the license file or None if no license file found + :rtype: str or None + """ + if not conf.license.eula: + return None + + if not os.path.exists(conf.license.eula): + return None + + return conf.license.eula + + +def eula_available(): + """Report if it looks like there is an EULA available on the system. + :return: True if an EULA seems to be available, False otherwise + :rtype: bool + """ + return bool(get_license_file_name()) diff --git a/pyanaconda/ui/categories/eula.py b/pyanaconda/ui/categories/eula.py new file mode 100644 index 0000000..0a4fe96 --- /dev/null +++ b/pyanaconda/ui/categories/eula.py @@ -0,0 +1,14 @@ +from pyanaconda.ui.categories import SpokeCategory +from pyanaconda.core.i18n import _ + +__all__ = ["LicensingCategory"] + +class LicensingCategory(SpokeCategory): + + @staticmethod + def get_title(): + return _("LICENSING") + + @staticmethod + def get_sort_order(): + return 900 diff --git a/pyanaconda/ui/gui/spokes/eula.glade b/pyanaconda/ui/gui/spokes/eula.glade new file mode 100644 index 0000000..1d340f0 --- /dev/null +++ b/pyanaconda/ui/gui/spokes/eula.glade @@ -0,0 +1,136 @@ + + + + + + The license will go here + + + False + True + True + License Information + + + + False + vertical + 6 + + + False + + + False + 6 + 6 + 6 + + + + + False + False + 0 + + + + + False + 0.8 + 0.8 + + + False + vertical + + + True + False + start + 24 + 6 + License Agreement: + + + False + True + 0 + + + + + True + False + True + True + vertical + + + True + True + in + + + True + True + 18 + True + True + natural + 12 + False + word + 12 + 12 + False + eulaBuffer + + + + + False + True + 0 + + + + + I _accept the license agreement + True + True + False + True + 0 + True + + + + False + True + 2 + + + + + False + True + 1 + + + + + + + True + True + 1 + + + + + + + + + diff --git a/pyanaconda/ui/gui/spokes/eula.py b/pyanaconda/ui/gui/spokes/eula.py new file mode 100644 index 0000000..a487c6b --- /dev/null +++ b/pyanaconda/ui/gui/spokes/eula.py @@ -0,0 +1,107 @@ +import logging + +from pyanaconda.ui.common import FirstbootOnlySpokeMixIn +from pyanaconda.ui.gui.spokes import NormalSpoke +from pyanaconda.core.i18n import _, CN_ +from pyanaconda.core import eula +from pyanaconda.ui.categories.eula import LicensingCategory +from pyanaconda.anaconda_loggers import get_module_logger +from pykickstart.constants import FIRSTBOOT_RECONFIG + +log = get_module_logger(__name__) +__all__ = ["EULASpoke"] + + +class EULASpoke(FirstbootOnlySpokeMixIn, NormalSpoke): + """The EULA spoke""" + + builderObjects = ["eulaBuffer", "eulaWindow"] + mainWidgetName = "eulaWindow" + uiFile = "spokes/eula.glade" + icon = "application-certificate-symbolic" + title = CN_("GUI|Spoke", "_License Information") + category = LicensingCategory + + @staticmethod + def get_screen_id(): + """Return a unique id of this UI screen.""" + return "license-information" + + def initialize(self): + log.debug("initializing the EULA spoke") + NormalSpoke.initialize(self) + + self._have_eula = True + self._eula_buffer = self.builder.get_object("eulaBuffer") + self._agree_check_button = self.builder.get_object("agreeCheckButton") + self._agree_label = self._agree_check_button.get_child() + self._agree_text = self._agree_label.get_text() + + log.debug("looking for the license file") + license_file = eula.get_license_file_name() + if not license_file: + log.error("no license found") + self._have_eula = False + self._eula_buffer.set_text(_("No license found")) + return + + # if there is "eula <...>" in kickstart, use its value + if self.data.eula.agreed is not None: + self._agree_check_button.set_active(self.data.eula.agreed) + + self._eula_buffer.set_text("") + itr = self._eula_buffer.get_iter_at_offset(0) + log.debug("opening the license file") + with open(license_file, "r") as fobj: + # insert the first line without prefixing with space + try: + first_line = next(fobj) + except StopIteration: + # nothing in the file + return + self._eula_buffer.insert(itr, first_line.strip()) + + # EULA file may be preformatted for the console, we want to let Gtk + # format it (blank lines should be preserved) + for line in fobj: + stripped_line = line.strip() + if stripped_line: + self._eula_buffer.insert(itr, " " + stripped_line) + else: + self._eula_buffer.insert(itr, "\n\n") + + def refresh(self): + self._agree_check_button.set_sensitive(self._have_eula) + self._agree_check_button.set_active(self.data.eula.agreed) + + def apply(self): + self.data.eula.agreed = self._agree_check_button.get_active() + + @property + def completed(self): + return not self._have_eula or self.data.eula.agreed + + @property + def status(self): + if not self._have_eula: + return _("No license found") + + return _("License accepted") if self.data.eula.agreed else _("License not accepted") + + @classmethod + def should_run(cls, environment, data): + if eula.eula_available(): + # don't run if we are in initial-setup in reconfig mode and the EULA has already been accepted + if FirstbootOnlySpokeMixIn.should_run(environment, data) and data and data.firstboot.firstboot == FIRSTBOOT_RECONFIG and data.eula.agreed: + log.debug("not running license spoke: reconfig mode & license already accepted") + return False + return True + return False + + def on_check_button_toggled(self, *args): + if self._agree_check_button.get_active(): + log.debug("license is now accepted") + self._agree_label.set_markup("%s" % self._agree_text) + else: + log.debug("license no longer accepted") + self._agree_label.set_markup(self._agree_text) diff --git a/pyanaconda/ui/tui/spokes/eula.py b/pyanaconda/ui/tui/spokes/eula.py new file mode 100644 index 0000000..a3e8e62 --- /dev/null +++ b/pyanaconda/ui/tui/spokes/eula.py @@ -0,0 +1,128 @@ +import logging + +from pyanaconda.ui.tui.spokes import NormalTUISpoke +from simpleline.render.widgets import TextWidget, CheckboxWidget +from simpleline.render.containers import ListRowContainer +from simpleline.render.screen import UIScreen, InputState +from simpleline.render.screen_handler import ScreenHandler +from pyanaconda.ui.common import FirstbootOnlySpokeMixIn +from pyanaconda.core import eula +from pyanaconda.ui.categories.eula import LicensingCategory +from pyanaconda.core.i18n import _, N_ +from pykickstart.constants import FIRSTBOOT_RECONFIG + +log = logging.getLogger("initial-setup") + +__all__ = ["EULASpoke"] + + +class EULASpoke(FirstbootOnlySpokeMixIn, NormalTUISpoke): + """The EULA spoke providing ways to read the license and agree/disagree with it.""" + + category = LicensingCategory + + @staticmethod + def get_screen_id(): + """Return a unique id of this UI screen.""" + return "license-information" + + def __init__(self, *args, **kwargs): + NormalTUISpoke.__init__(self, *args, **kwargs) + self.title = _("License information") + self._container = None + + def initialize(self): + NormalTUISpoke.initialize(self) + + def refresh(self, args=None): + NormalTUISpoke.refresh(self, args) + + self._container = ListRowContainer(1) + + log.debug("license found") + # make the options aligned to the same column (the checkbox has the + # '[ ]' prepended) + self._container.add(TextWidget("%s\n" % _("Read the License Agreement")), + self._show_license_screen_callback) + + self._container.add(CheckboxWidget(title=_("I accept the license agreement"), + completed=self.data.eula.agreed), + self._license_accepted_callback) + self.window.add_with_separator(self._container) + + @property + def completed(self): + # Either there is no EULA available, or user agrees/disagrees with it. + return self.data.eula.agreed + + @property + def mandatory(self): + # This spoke is always mandatory. + return True + + @property + def status(self): + return _("License accepted") if self.data.eula.agreed else _("License not accepted") + + @classmethod + def should_run(cls, environment, data): + if eula.eula_available(): + # don't run if we are in initial-setup in reconfig mode and the EULA has already been accepted + if FirstbootOnlySpokeMixIn.should_run(environment, data) and data and data.firstboot.firstboot == FIRSTBOOT_RECONFIG and data.eula.agreed: + log.debug("not running license spoke: reconfig mode & license already accepted") + return False + return True + return False + + def apply(self): + # nothing needed here, the agreed field is changed in the input method + pass + + def input(self, args, key): + if not self._container.process_user_input(key): + return key + + return InputState.PROCESSED + + @staticmethod + def _show_license_screen_callback(data): + # show license + log.debug("showing the license") + eula_screen = LicenseScreen() + ScreenHandler.push_screen(eula_screen) + + def _license_accepted_callback(self, data): + # toggle EULA agreed checkbox by changing ksdata + log.debug("license accepted state changed to: %s", self.data.eula.agreed) + self.data.eula.agreed = not self.data.eula.agreed + self.redraw() + + +class LicenseScreen(UIScreen): + """Screen showing the License without any input from user requested.""" + + def __init__(self): + super().__init__() + + self._license_file = eula.get_license_file_name() + + def refresh(self, args=None): + super().refresh(args) + + # read the license file and make it one long string so that it can be + # processed by the TextWidget to fit in the screen in a best possible + # way + log.debug("reading the license file") + with open(self._license_file, 'r') as f: + license_text = f.read() + + self.window.add_with_separator(TextWidget(license_text)) + + def input(self, args, key): + """ Handle user input. """ + return InputState.PROCESSED_AND_CLOSE + + def prompt(self, args=None): + # we don't want to prompt user, just close the screen + self.close() + return None -- 2.39.2