Implement show EULA before installation

i9-certified changed/i9-certified/anaconda-34.25.4.9-1.el9_4.inferit.102.certified
Dmitry Samoylik 2 months ago
parent 7bbe89026c
commit 0de60446fe

@ -0,0 +1,488 @@
From 2716b292b9d42961222fd645c6b2d281d46d6688 Mon Sep 17 00:00:00 2001
From: Dmitry Samoylik <Dmitriy.Samoylik@softline.com>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.6 -->
+ <!-- interface-requires AnacondaWidgets 1.0 -->
+ <object class="GtkTextBuffer" id="eulaBuffer">
+ <property name="text">The license will go here</property>
+ </object>
+ <object class="AnacondaSpokeWindow" id="eulaWindow">
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="window_name" translatable="yes">License Information</property>
+ <signal name="button-clicked" handler="on_back_clicked" swapped="no"/>
+ <child internal-child="main_box">
+ <object class="GtkBox" id="AnacondaSpokeWindow-main_box1">
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child internal-child="nav_box">
+ <object class="GtkEventBox" id="AnacondaSpokeWindow-nav_box1">
+ <property name="can_focus">False</property>
+ <child internal-child="nav_area">
+ <object class="GtkGrid" id="AnacondaSpokeWindow-nav_area1">
+ <property name="can_focus">False</property>
+ <property name="margin_left">6</property>
+ <property name="margin_right">6</property>
+ <property name="margin_top">6</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child internal-child="alignment">
+ <object class="GtkAlignment" id="AnacondaSpokeWindow-alignment1">
+ <property name="can_focus">False</property>
+ <property name="xscale">0.8</property>
+ <property name="yscale">0.8</property>
+ <child internal-child="action_area">
+ <object class="GtkBox" id="mainBox">
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel" id="licenseAgreementLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin_top">24</property>
+ <property name="margin_bottom">6</property>
+ <property name="label" translatable="yes">License Agreement:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="eulaBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkScrolledWindow" id="eulaScrolledWindow">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkTextView" id="eulaView">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="margin_bottom">18</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="vscroll_policy">natural</property>
+ <property name="pixels_above_lines">12</property>
+ <property name="editable">False</property>
+ <property name="wrap_mode">word</property>
+ <property name="left_margin">12</property>
+ <property name="right_margin">12</property>
+ <property name="cursor_visible">False</property>
+ <property name="buffer">eulaBuffer</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="agreeCheckButton">
+ <property name="label" translatable="yes">I _accept the license agreement</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_check_button_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
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("<b>%s</b>" % 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

@ -7647,6 +7647,37 @@ msgstr "Закрыть"
msgid "Starting Install to Hard Drive"
msgstr "Начинается установка на жёсткий диск"
msgid "LICENSING"
msgstr "ЛИЦЕНЗИРОВАНИЕ"
msgctxt "GUI|Spoke"
msgid "_License Information"
msgstr "О _лицензии"
msgid "License information"
msgstr "О лицензии"
msgid "License Information"
msgstr "О лицензии"
msgid "License accepted"
msgstr "Лицензия принята"
msgid "License not accepted"
msgstr "Лицензия не принята"
msgid "Read the License Agreement"
msgstr "Прочитайте лицензионное соглашение"
msgid "I accept the license agreement"
msgstr "Принимаю лицензионное соглашение"
msgid "I _accept the license agreement"
msgstr "_Принимаю лицензионное соглашение"
msgid "License Agreement:"
msgstr "Лицензионное соглашение:"
#~ msgid "Failed to attach subscription."
#~ msgstr "Не удалось прикрепить подписку."

@ -1,7 +1,7 @@
Summary: Graphical system installer
Name: anaconda
Version: 34.25.4.9
Release: 1%{?dist}.inferit.101.certified
Release: 1%{?dist}.inferit.102.certified
License: GPLv2+ and MIT
URL: http://fedoraproject.org/wiki/Anaconda
@ -31,6 +31,7 @@ Patch1013: 0013-Fix-smt-url.patch
Patch1014: 0014-Make-branding-free-Russian-translation.patch
Patch1015: 0015-Disable-sphere-url.patch
Patch1016: 0016-Set-LatGrkCyr-8x16-as-default-font-instead-of-eurlat.patch
Patch1017: 0017-Implement-show-EULA-before-installation.patch
# Versions of required components (done so we make sure the buildrequires
# match the requires versions of things).
@ -441,6 +442,9 @@ desktop-file-install --dir=%{buildroot}%{_datadir}/applications %{buildroot}%{_d
%{_prefix}/libexec/anaconda/dd_*
%changelog
* Thu Sep 26 2024 Dmitry Samoylik <Dmitriy.Samoylik@softline.com> - 34.25.4.9-1.inferit.102.certified
- Implement show EULA before installation
* Sat Sep 14 2024 Arkady L. Shane <tigro@msvsphere-os.ru> - 34.25.4.9-1.inferit.101.certified
- Added Certified Flavour

Loading…
Cancel
Save