tails-additional-software-config 10.8 KB
Newer Older
1
2
3
4
5
#!/usr/bin/env python3

"""User interface to configure Tails Additional Software."""

import gettext
6
import os
7
import subprocess
8
9
10
11
12
13
14
15
16
import sys

import apt.cache
import gi

from gi.repository import Gio                               # NOQA: E402
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk                               # NOQA: E402

17
18
19
from tailslib.persistence import (                          # NOQA: E402
    has_unlocked_persistence,
    has_persistence,
20
21
    is_tails_media_writable,
    launch_persistence_setup)
22

23
from tailslib.additionalsoftware import (                   # NOQA: E402
Alan's avatar
Alan committed
24
    get_additional_packages,
Alan's avatar
Alan committed
25
26
    get_packages_list_path,
    filter_package_details)
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

_ = gettext.gettext

UI_FILE = "/usr/share/tails/additional-software/configuration-window.ui"


class ASPConfigApplicationWindow(Gtk.ApplicationWindow):
    def __init__(self, application, get_config_func, remove_asp_func):
        Gtk.ApplicationWindow.__init__(self, application=application)

        self.get_config_func = get_config_func
        self.remove_asp_func = remove_asp_func

        self.connect("show", self.cb_window_show)

        builder = Gtk.Builder.new_from_file(UI_FILE)
        builder.set_translation_domain("tails")
        builder.connect_signals(self)

        self.listbox = builder.get_object("listbox")
        self.no_package_page = builder.get_object("no_package_page")
        self.package_list_page = builder.get_object("package_list_page")
        self.stack = builder.get_object("stack")
50
        self.install_label = builder.get_object("install_label")
51
        self.persistence_button = builder.get_object("persistence_button")
52

53
54
        self.listbox.set_header_func(self._listbox_update_header_func, None)

55
56
57
58
59
        self.set_default_size(width=500, height=-1)
        self.set_icon_name("package-x-generic")
        self.set_titlebar(builder.get_object("headerbar"))
        self.add(builder.get_object("main_box"))

60
61
62
63
64
65
66
67
68
69
70
71
    @staticmethod
    def _listbox_update_header_func(row, before, user_data):
        if not before:
            row.set_header(None)
            return

        current = row.get_header()
        if not current:
            current = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL)
            current.show()
            row.set_header(current)

72
73
74
75
76
77
78
79
80
81
82
    def __show_exception_dialog(self, explanation, exception):
        dialog = Gtk.MessageDialog(
            self,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            Gtk.MessageType.ERROR,
            Gtk.ButtonsType.OK,
            explanation)
        dialog.format_secondary_text(str(exception))
        dialog.run()
        dialog.destroy()

83
    def cb_activate_link(self, label, uri):
Alan's avatar
Alan committed
84
        if uri.endswith(".desktop"):
85
86
87
88
89
            appinfo = Gio.DesktopAppInfo.new(uri)
            appinfo.launch()
            return True

    def cb_listboxrow_remove_button_clicked(self, button, package_name):
90
91
92
93
94
        dialog = Gtk.MessageDialog(
            self,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            Gtk.MessageType.QUESTION,
            Gtk.ButtonsType.NONE,
95
            # Translators: Don't translate {package}, it's a placeholder and will be replaced.
96
97
98
            _("Remove {package} from your additional software? "
              "This will stop installing the package "
              "automatically.").format(package=package_name))
99
100
101
        dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT)
        dialog.add_button(Gtk.STOCK_REMOVE, Gtk.ResponseType.ACCEPT)
        if dialog.run() == Gtk.ResponseType.ACCEPT:
102
103
104
105
            try:
                self.remove_asp_func(package_name)
            except subprocess.CalledProcessError as e:
                self.__show_exception_dialog(
106
                    # Translators: Don't translate {pkg}, it's a placeholder and will be replaced.
107
108
                    _("Failed to remove {pkg}").format(pkg=package_name),
                    e)
109
        dialog.destroy()
110

111
    def cb_persistence_button_clicked(self, button, data=None):
112
        launch_persistence_setup("--force-enable-preset", "AdditionalSoftware")
Alan's avatar
Alan committed
113
        self.update_packages_list()
114
115
        return True

116
    def cb_window_show(self, window):
117
118
119
        self.update_packages_list()

    def update_packages_list(self):
120
121
122
        try:
            packages = self.get_config_func()
        except Exception as e:
123
124
125
            self.__show_exception_dialog(
                _("Failed to read additional software configuration"),
                e)
126
            self.hide()
Alan's avatar
Alan committed
127
            return
128
        self.persistence_button.set_visible(False)
129
        if packages:
130
            self.listbox.foreach(lambda widget, data: widget.destroy(), None)
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
            for package_name, package_description in packages:
                listboxrow = Gtk.ListBoxRow.new()

                hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0)
                hbox.set_border_width(3)

                vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
                name_label = Gtk.Label.new("<b>{}</b>".format(package_name))
                name_label.set_use_markup(True)
                name_label.set_xalign(0)
                vbox.pack_start(name_label, expand=True, fill=True, padding=0)
                description_label = Gtk.Label.new(package_description)
                description_label.set_xalign(0)
                vbox.pack_start(
                    description_label, expand=True, fill=True, padding=0)
                hbox.pack_start(vbox, expand=True, fill=True, padding=12)

                remove_button = Gtk.Button.new_from_icon_name(
                    "window-close-symbolic",
                    Gtk.IconSize.SMALL_TOOLBAR)
                remove_button.set_relief(Gtk.ReliefStyle.NONE)
152
                remove_button.set_tooltip_text(
153
                    # Translators: Don't translate {package}, it's a placeholder and will be replaced.
154
155
                    _("Stop installing {package} "
                      "automatically").format(package=package_name))
156
157
158
159
160
161
162
163
                remove_button.connect(
                    "clicked", self.cb_listboxrow_remove_button_clicked,
                    package_name)
                hbox.pack_end(
                    remove_button, expand=False, fill=False, padding=0)

                listboxrow.add(hbox)
                self.listbox.add(listboxrow)
Alan's avatar
Alan committed
164
165
166
167
168
            # Add empty listboxrow to finish the list with a separator
            listboxrow = Gtk.ListBoxRow.new()
            listboxrow.set_selectable(False)
            self.listbox.add(listboxrow)

169
170
            self.listbox.show_all()
            self.stack.set_visible_child(self.package_list_page)
171
            self.install_label.set_markup(
172
173
174
175
                _('To add more, install some software using '
                  '<a href="synaptic.desktop">Synaptic Package Manager</a> '
                  'or <a href="org.gnome.Terminal.desktop">APT on the '
                  'command line</a>.'))
176
177
        else:
            self.stack.set_visible_child(self.no_package_page)
178
            self.install_label.set_markup(
179
180
181
182
                _('To do so, install some software using '
                  '<a href="synaptic.desktop">Synaptic Package Manager</a> '
                  'or <a href="org.gnome.Terminal.desktop">APT on the '
                  'command line</a>.'))
183
184
185
186
187
            if has_unlocked_persistence(search_new_persistence=True):
                # The label from the UI file is good unmodified
                pass
            elif has_persistence():
                self.install_label.set_markup(
188
189
190
191
192
193
194
                    _('To do so, unlock your persistent storage '
                      'when starting Tails and '
                      'install some software using '
                      '<a href="synaptic.desktop">Synaptic Package '
                      'Manager</a> or '
                      '<a href="org.gnome.Terminal.desktop">APT on the '
                      'command line</a>.'))
195
            elif is_tails_media_writable():
196
                self.persistence_button.set_visible(True)
197
                self.install_label.set_markup(
198
199
200
201
202
203
                    _('To do so, create a persistent storage and install some '
                      'software using '
                      '<a href="synaptic.desktop">Synaptic Package '
                      'Manager</a> or '
                      '<a href="org.gnome.Terminal.desktop">APT on the '
                      'command line</a>.'))
204
205
            else:   # It's impossible to have a persistent storage
                self.install_label.set_markup(
206
207
208
                    _('To do so, install Tails on a USB stick using '
                      '<a href="tails-installer.desktop">Tails Installer</a> '
                      'and create a persistent storage.'))
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228


class ASPConfigApplication(Gtk.Application):
    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            application_id="org.boum.tails.additional-software-config",
            **kwargs)

    def do_activate(self):
        self.window.present()

    def do_startup(self):
        Gtk.Application.do_startup(self)
        gettext.install("tails")
        self.window = ASPConfigApplicationWindow(
            application=self,
            get_config_func=self.get_asp_configuration,
            remove_asp_func=self.remove_additional_software)

229
        packages_list_file = Gio.File.new_for_path(
230
231
            get_packages_list_path(search_new_persistence=True,
                                   return_nonexistent=True))
Alan's avatar
Alan committed
232
233
234
235
236
237
238
        self.packages_list_monitor = packages_list_file.monitor(
            Gio.FileMonitorFlags.NONE, None)
        self.packages_list_monitor.connect(
            "changed", self.cb_packages_list_changed)

    def cb_packages_list_changed(self, file_monitor, file, other_file,
                                 event_type):
239
240
        if os.access(file.get_path(), os.R_OK):
            self.window.update_packages_list()
Alan's avatar
Alan committed
241

242
    def get_asp_configuration(self):
243
        additional_packages = get_additional_packages(
Alan's avatar
Alan committed
244
            search_new_persistence=True)
245
246
247
248
        apt_cache = apt.cache.Cache()

        packages_with_description = []
        for package in sorted(additional_packages):
Alan's avatar
Alan committed
249
            package_name = filter_package_details(package)
250
            try:
Alan's avatar
Alan committed
251
                apt_package = apt_cache[package_name]
252
253
254
            except KeyError:
                summary = _("[package not available]")
            else:
Alan's avatar
Alan committed
255
256
257
258
                if apt_package.installed:
                    summary = apt_package.installed.summary
                else:
                    summary = apt_package.candidate.summary
259
260
261
262
263
            packages_with_description.append((package, summary))

        return packages_with_description

    def remove_additional_software(self, package_name):
264
265
266
267
        subprocess.run(["pkexec",
                        "/usr/local/sbin/tails-additional-software-remove",
                        package_name],
                       check=True)
268
269
270
271
272


asp_application = ASPConfigApplication()
exit_status = asp_application.run(sys.argv)
sys.exit(exit_status)