From beb163cfd15b93bf664b6a0590e114d4432e3ef9 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Fri, 9 Sep 2022 11:44:59 +0200 Subject: [proxy] allow prompting the user when a package suitable for current site is found; add proxy's options page This commit also causes uncached responses to be forced on all HTTP request to external servers. This is needed to make sure that changes Haketilo makes to HTTP response headers are always picked up by the browser. The drawback is that this increases network traffic causing a performance hit. We might optimize this in the future. --- src/hydrilla/locales/en_US/LC_MESSAGES/messages.po | 130 ++++++++++++++++++--- src/hydrilla/proxy/policies/base.py | 15 ++- src/hydrilla/proxy/policies/payload.py | 52 ++++++--- src/hydrilla/proxy/state.py | 5 - src/hydrilla/proxy/state_impl/base.py | 3 +- src/hydrilla/proxy/state_impl/concrete_state.py | 53 ++++++--- src/hydrilla/proxy/state_impl/repos.py | 5 - src/hydrilla/proxy/web_ui/options.py | 69 +++++++++++ src/hydrilla/proxy/web_ui/prompts.py | 116 ++++++++++++++---- src/hydrilla/proxy/web_ui/root.py | 4 +- .../proxy/web_ui/templates/base.html.jinja | 17 ++- .../templates/items/item_viewversion.html.jinja | 8 +- .../templates/items/package_viewversion.html.jinja | 12 +- .../proxy/web_ui/templates/options.html.jinja | 86 ++++++++++++++ .../prompts/auto_install_error.html.jinja | 37 +++--- .../prompts/package_suggestion.html.jinja | 58 +++++++++ .../web_ui/templates/repos/show_single.html.jinja | 6 +- 17 files changed, 552 insertions(+), 124 deletions(-) create mode 100644 src/hydrilla/proxy/web_ui/options.py create mode 100644 src/hydrilla/proxy/web_ui/templates/options.html.jinja create mode 100644 src/hydrilla/proxy/web_ui/templates/prompts/package_suggestion.html.jinja diff --git a/src/hydrilla/locales/en_US/LC_MESSAGES/messages.po b/src/hydrilla/locales/en_US/LC_MESSAGES/messages.po index 4807509..c8d8831 100644 --- a/src/hydrilla/locales/en_US/LC_MESSAGES/messages.po +++ b/src/hydrilla/locales/en_US/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: hydrilla 2.0\n" "Report-Msgid-Bugs-To: koszko@koszko.org\n" -"POT-Creation-Date: 2022-09-08 17:52+0200\n" +"POT-Creation-Date: 2022-09-09 11:30+0200\n" "PO-Revision-Date: 2022-02-12 00:00+0000\n" "Last-Translator: Wojtek Kosior \n" "Language: en_US\n" @@ -190,7 +190,7 @@ msgstr "" "Attempt was made to configure Haketilo Mitmproxy addon with data " "directory path but it has already been configured." -#: src/hydrilla/proxy/addon.py:166 +#: src/hydrilla/proxy/addon.py:165 msgid "err.proxy.unknown_error_{}_try_again" msgstr "" "Haketilo experienced an error. Try again.\n" @@ -205,39 +205,43 @@ msgstr "Requested file could not be found." msgid "api.resource_not_enabled_for_access" msgstr "Requested resource is not enabled for access." -#: src/hydrilla/proxy/state_impl/concrete_state.py:106 +#: src/hydrilla/proxy/state_impl/concrete_state.py:120 msgid "err.proxy.unknown_db_schema" msgstr "" "Haketilo's data files have been altered, possibly by a newer version of " "Haketilo." -#: src/hydrilla/proxy/state_impl/concrete_state.py:110 +#: src/hydrilla/proxy/state_impl/concrete_state.py:124 msgid "err.proxy.no_sqlite_foreign_keys" msgstr "" "This installation of Haketilo uses an SQLite version which does not " "support foreign key constraints." -#: src/hydrilla/proxy/web_ui/templates/base.html.jinja:63 +#: src/hydrilla/proxy/web_ui/templates/base.html.jinja:69 msgid "web_ui.base.title.haketilo_proxy" msgstr "Haketilo" -#: src/hydrilla/proxy/web_ui/templates/base.html.jinja:228 +#: src/hydrilla/proxy/web_ui/templates/base.html.jinja:238 msgid "web_ui.base.nav.home" msgstr "Home" -#: src/hydrilla/proxy/web_ui/templates/base.html.jinja:229 +#: src/hydrilla/proxy/web_ui/templates/base.html.jinja:239 +msgid "web_ui.base.nav.options" +msgstr "Options" + +#: src/hydrilla/proxy/web_ui/templates/base.html.jinja:240 msgid "web_ui.base.nav.packages" msgstr "Packages" -#: src/hydrilla/proxy/web_ui/templates/base.html.jinja:230 +#: src/hydrilla/proxy/web_ui/templates/base.html.jinja:241 msgid "web_ui.base.nav.libraries" msgstr "Libraries" -#: src/hydrilla/proxy/web_ui/templates/base.html.jinja:231 +#: src/hydrilla/proxy/web_ui/templates/base.html.jinja:242 msgid "web_ui.base.nav.repos" msgstr "Repositories" -#: src/hydrilla/proxy/web_ui/templates/base.html.jinja:232 +#: src/hydrilla/proxy/web_ui/templates/base.html.jinja:243 msgid "web_ui.base.nav.load" msgstr "Import from file" @@ -257,7 +261,7 @@ msgstr "" #: src/hydrilla/proxy/web_ui/templates/index.html.jinja:37 msgid "web_ui.home.heading.about_haketilo" -msgstr "About Haketilo" +msgstr "About this tool" #: src/hydrilla/proxy/web_ui/templates/index.html.jinja:41 msgid "web_ui.home.haketilo_is_blah_blah" @@ -268,6 +272,7 @@ msgstr "" "extension but has since been made into an HTTP proxy." #: src/hydrilla/proxy/web_ui/templates/items/item_viewversion.html.jinja:25 +#: src/hydrilla/proxy/web_ui/templates/prompts/package_suggestion.html.jinja:30 #: src/hydrilla/proxy/web_ui/templates/repos/show_single.html.jinja:33 msgid "web_ui.err.file_installation_error" msgstr "Failed to install needed items from repository." @@ -277,6 +282,7 @@ msgid "web_ui.err.uninstall_disallowed" msgstr "This item is required and cannot be uninstalled." #: src/hydrilla/proxy/web_ui/templates/items/item_viewversion.html.jinja:33 +#: src/hydrilla/proxy/web_ui/templates/prompts/package_suggestion.html.jinja:34 #: src/hydrilla/proxy/web_ui/templates/repos/show_single.html.jinja:37 msgid "web_ui.err.repo_communication_error" msgstr "Couldn't communicate with repository." @@ -515,15 +521,83 @@ msgstr "Available packages" msgid "web_ui.packages.enabled_version_{}" msgstr "enabled version {}" +#: src/hydrilla/proxy/web_ui/templates/options.html.jinja:23 +msgid "web_ui.options.title" +msgstr "Global options" + +#: src/hydrilla/proxy/web_ui/templates/options.html.jinja:27 +msgid "web_ui.options.heading" +msgstr "Haketilo global options" + +#: src/hydrilla/proxy/web_ui/templates/options.html.jinja:37 +msgid "web_ui.options.packages_are_used_when_enabled" +msgstr "" +"Hektilo is currently configured to only use packages that were explicitly" +" enabled." + +#: src/hydrilla/proxy/web_ui/templates/options.html.jinja:40 +msgid "web_ui.options.user_gets_asked_whether_to_enable_package" +msgstr "" +"Hektilo is currently configured to ask whenever a package is found that " +"could be used for the current site." + +#: src/hydrilla/proxy/web_ui/templates/options.html.jinja:44 +msgid "web_ui.options.packages_are_used_automatically" +msgstr "" +"Hektilo is currently configured to automatically use packages that are " +"available for the current site." + +#: src/hydrilla/proxy/web_ui/templates/options.html.jinja:51 +msgid "web_ui.options.use_enabled_button" +msgstr "Use when enabled" + +#: src/hydrilla/proxy/web_ui/templates/options.html.jinja:54 +msgid "web_ui.options.use_question_button" +msgstr "Ask whether to use" + +#: src/hydrilla/proxy/web_ui/templates/options.html.jinja:57 +msgid "web_ui.options.use_auto_button" +msgstr "Use automatically" + +#: src/hydrilla/proxy/web_ui/templates/options.html.jinja:70 +msgid "web_ui.options.scripts_are_allowed_by_default" +msgstr "" +"By default Haketilo currently allows JavaScript sent by websites to the " +"browser to execute." + +#: src/hydrilla/proxy/web_ui/templates/options.html.jinja:73 +msgid "web_ui.options.scripts_are_blocked_by_default" +msgstr "" +"By default Haketilo currently blocks JavaScript sent by websites to the " +"browser from executing." + +#: src/hydrilla/proxy/web_ui/templates/options.html.jinja:77 +msgid "web_ui.options.allow_scripts_button" +msgstr "Allow scripts" + +#: src/hydrilla/proxy/web_ui/templates/options.html.jinja:78 +msgid "web_ui.options.block_scripts_button" +msgstr "Block scripts" + #: src/hydrilla/proxy/web_ui/templates/prompts/auto_install_error.html.jinja:24 msgid "web_ui.prompts.auto_install_error.title" msgstr "Installation failure" -#: src/hydrilla/proxy/web_ui/templates/prompts/auto_install_error.html.jinja:41 +#: src/hydrilla/proxy/web_ui/templates/prompts/auto_install_error.html.jinja:29 +msgid "web_ui.err.retry_install.file_installation_error" +msgstr "" +"Another failure occured when retrying to install needed items from " +"repository." + +#: src/hydrilla/proxy/web_ui/templates/prompts/auto_install_error.html.jinja:33 +msgid "web_ui.err.retry_install.repo_communication_error" +msgstr "Another failure occured when retrying to communicate with repository." + +#: src/hydrilla/proxy/web_ui/templates/prompts/auto_install_error.html.jinja:37 msgid "web_ui.prompts.auto_install_error.heading" msgstr "Installation failure" -#: src/hydrilla/proxy/web_ui/templates/prompts/auto_install_error.html.jinja:46 +#: src/hydrilla/proxy/web_ui/templates/prompts/auto_install_error.html.jinja:42 msgid "web_ui.prompts.auto_install_error.package_{}_failed_to_install" msgstr "" "Automatically activated package '{}' failed to install because Haketilo " @@ -531,14 +605,36 @@ msgstr "" "that you do have network connection and try again. You can also choose to" " permanently disable the package." -#: src/hydrilla/proxy/web_ui/templates/prompts/auto_install_error.html.jinja:52 +#: src/hydrilla/proxy/web_ui/templates/prompts/auto_install_error.html.jinja:47 msgid "web_ui.prompts.auto_install_error.disable_button" msgstr "Disable" -#: src/hydrilla/proxy/web_ui/templates/prompts/auto_install_error.html.jinja:57 +#: src/hydrilla/proxy/web_ui/templates/prompts/auto_install_error.html.jinja:48 msgid "web_ui.prompts.auto_install_error.retry_button" msgstr "Retry installation" +#: src/hydrilla/proxy/web_ui/templates/prompts/package_suggestion.html.jinja:25 +msgid "web_ui.prompts.package_suggestion.title" +msgstr "Package suggestion" + +#: src/hydrilla/proxy/web_ui/templates/prompts/package_suggestion.html.jinja:38 +msgid "web_ui.prompts.package_suggestion.heading" +msgstr "Package suitable for current site was found" + +#: src/hydrilla/proxy/web_ui/templates/prompts/package_suggestion.html.jinja:43 +msgid "web_ui.prompts.package_suggestion.do_you_want_to_enable_package_{}" +msgstr "" +"Do you want to enable package '{}'? It will then be used whenever you " +"visit this site." + +#: src/hydrilla/proxy/web_ui/templates/prompts/package_suggestion.html.jinja:48 +msgid "web_ui.prompts.package_suggestion.disable_button" +msgstr "Disable" + +#: src/hydrilla/proxy/web_ui/templates/prompts/package_suggestion.html.jinja:49 +msgid "web_ui.prompts.package_suggestion.enable_button" +msgstr "Enable" + #: src/hydrilla/proxy/web_ui/templates/repos/add.html.jinja:23 msgid "web_ui.repos.add.title" msgstr "New repository" @@ -660,11 +756,11 @@ msgstr "Last refreshed on {}." msgid "web_ui.repos.single.refresh_now_button" msgstr "Refresh" -#: src/hydrilla/proxy/web_ui/templates/repos/show_single.html.jinja:162 +#: src/hydrilla/proxy/web_ui/templates/repos/show_single.html.jinja:163 msgid "web_ui.repos.item_count_{mappings}_{resources}" msgstr "packages: {mappings}; libraries: {resources}" -#: src/hydrilla/proxy/web_ui/templates/repos/show_single.html.jinja:173 +#: src/hydrilla/proxy/web_ui/templates/repos/show_single.html.jinja:174 msgid "web_ui.repos.single.remove_button" msgstr "Remove repository" diff --git a/src/hydrilla/proxy/policies/base.py b/src/hydrilla/proxy/policies/base.py index bb95d29..8ffc45e 100644 --- a/src/hydrilla/proxy/policies/base.py +++ b/src/hydrilla/proxy/policies/base.py @@ -58,22 +58,21 @@ class Policy(ABC): """....""" process_request: t.ClassVar[bool] = False process_response: t.ClassVar[bool] = False + anticache: t.ClassVar[bool] = True priority: t.ClassVar[PolicyPriority] - @property - def anticache(self) -> bool: - return self.process_request or self.process_response - def consume_request(self, request_info: http_messages.RequestInfo) \ -> t.Optional[ProducedMessage]: - """....""" - return None + raise NotImplementedError( + 'This kind of policy does not consume requests.' + ) def consume_response(self, response_info: http_messages.ResponseInfo) \ -> t.Optional[http_messages.ProducedResponse]: - """....""" - return None + raise NotImplementedError( + 'This kind of policy does not consume responses.' + ) # mypy needs to be corrected: diff --git a/src/hydrilla/proxy/policies/payload.py b/src/hydrilla/proxy/policies/payload.py index a063c7c..1069c41 100644 --- a/src/hydrilla/proxy/policies/payload.py +++ b/src/hydrilla/proxy/policies/payload.py @@ -60,6 +60,20 @@ class PayloadAwarePolicy(base.Policy): return f'{request_url.url_without_path}/{"/".join(base_path_segments)}/' + def _payload_details_to_signed_query_string( + self, + _salt: str, + **extra_keys: str + ) -> str: + params: t.Mapping[str, str] = { + 'payload_id': self.payload_data.ref.id, + **extra_keys + } + + serializer = URLSafeSerializer(self.payload_data.global_secret, _salt) + + return urlencode({'details': serializer.dumps(params)}) + @dc.dataclass(frozen=True) # type: ignore[misc] class PayloadAwarePolicyFactory(base.PolicyFactory): @@ -283,20 +297,18 @@ class AutoPayloadInjectPolicy(PayloadInjectPolicy): return super().consume_response(response_info) except (state.RepoCommunicationError, state.FileInstallationError, _PayloadHasProblemsError) as ex: - params = { - 'next_url': response_info.url.orig_url, - 'payload_id': self.payload_data.ref.id + extra_params: dict[str, str] = { + 'next_url': response_info.url.orig_url } - if isinstance(ex, state.FileInstallationError): - params['repo_id'] = ex.repo_id - params['file_sha256'] = ex.sha256 + extra_params['repo_id'] = ex.repo_id + extra_params['file_sha256'] = ex.sha256 - serializer = URLSafeSerializer( - self.payload_data.global_secret, - salt = 'auto_install_error' + query = self._payload_details_to_signed_query_string( + _salt = 'auto_install_error', + **extra_params ) - query = urlencode({'details': serializer.dumps(params)}) + redirect_url = 'https://hkt.mitm.it/auto_install_error?' + query msg = 'Error occured when installing payload. Redirecting.' @@ -310,13 +322,25 @@ class AutoPayloadInjectPolicy(PayloadInjectPolicy): @dc.dataclass(frozen=True) class PayloadSuggestPolicy(PayloadAwarePolicy): """....""" + process_request: t.ClassVar[bool] = True + priority: t.ClassVar[base.PolicyPriority] = base.PolicyPriority._ONE - def make_response(self, request_info: http_messages.RequestInfo) \ + def consume_request(self, request_info: http_messages.RequestInfo) \ -> http_messages.ProducedResponse: - """....""" - # TODO: implement - return http_messages.ProducedResponse(200, ((b'a', b'b'),), b'') + query = self._payload_details_to_signed_query_string( + _salt = 'package_suggestion', + next_url = request_info.url.orig_url + ) + + redirect_url = 'https://hkt.mitm.it/package_suggestion?' + query + msg = 'A package was found that could be used on this site. Redirecting.' + + return http_messages.ProducedResponse( + status_code = 303, + headers = [(b'Location', redirect_url.encode())], + body = msg.encode() + ) @dc.dataclass(frozen=True, unsafe_hash=True) diff --git a/src/hydrilla/proxy/state.py b/src/hydrilla/proxy/state.py index 491a865..abea7a7 100644 --- a/src/hydrilla/proxy/state.py +++ b/src/hydrilla/proxy/state.py @@ -447,11 +447,6 @@ class HaketiloState(ABC): """....""" ... - @abstractmethod - def get_repo_iteration(self, repo_iteration_id: str) -> RepoIterationRef: - """....""" - ... - @abstractmethod def mapping_store(self) -> MappingStore: ... diff --git a/src/hydrilla/proxy/state_impl/base.py b/src/hydrilla/proxy/state_impl/base.py index 0559a42..75d733f 100644 --- a/src/hydrilla/proxy/state_impl/base.py +++ b/src/hydrilla/proxy/state_impl/base.py @@ -140,10 +140,9 @@ class HaketiloStateWithFields(st.HaketiloState): """....""" store_dir: Path connection: sqlite3.Connection + settings: st.HaketiloGlobalSettings current_cursor: t.Optional[sqlite3.Cursor] = None - #settings: st.HaketiloGlobalSettings - secret: bytes = dc.field(default_factory=(lambda: secrets.token_bytes(16))) policy_tree: PolicyTree = PolicyTree() diff --git a/src/hydrilla/proxy/state_impl/concrete_state.py b/src/hydrilla/proxy/state_impl/concrete_state.py index 8bd25a9..3611db1 100644 --- a/src/hydrilla/proxy/state_impl/concrete_state.py +++ b/src/hydrilla/proxy/state_impl/concrete_state.py @@ -56,10 +56,24 @@ from . import _operations here = Path(__file__).resolve().parent -@dc.dataclass(frozen=True, unsafe_hash=True) -class ConcreteRepoIterationRef(st.RepoIterationRef): - pass - +def load_settings(cursor: sqlite3.Cursor) -> st.HaketiloGlobalSettings: + cursor.execute( + ''' + SELECT + default_allow_scripts, repo_refresh_seconds, mapping_use_mode + FROM + general + ''' + ) + + (default_allow_scripts, repo_refresh_seconds, + mapping_use_mode), = cursor.fetchall() + + return st.HaketiloGlobalSettings( + default_allow_scripts = default_allow_scripts, + repo_refresh_seconds = repo_refresh_seconds, + mapping_use_mode = st.MappingUseMode(mapping_use_mode) + ) @dc.dataclass class ConcreteHaketiloState(base.HaketiloStateWithFields): @@ -233,9 +247,6 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): def repo_store(self) -> st.RepoStore: return repos.ConcreteRepoStore(self) - def get_repo_iteration(self, repo_iteration_id: str) -> st.RepoIterationRef: - return ConcreteRepoIterationRef(repo_iteration_id) - def mapping_store(self) -> st.MappingStore: return items.ConcreteMappingStore(self) @@ -255,11 +266,8 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): return self.secret def get_settings(self) -> st.HaketiloGlobalSettings: - return st.HaketiloGlobalSettings( - mapping_use_mode = st.MappingUseMode.AUTO, - default_allow_scripts = False, - repo_refresh_seconds = 0 - ) + with self.lock: + return self.settings def update_settings( self, @@ -268,7 +276,18 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): default_allow_scripts: t.Optional[bool] = None, repo_refresh_seconds: t.Optional[int] = None ) -> None: - raise NotImplementedError() + with self.cursor(transaction=True) as cursor: + def set_opt(col_name: str, val: t.Union[bool, int, str]) -> None: + cursor.execute(f'UPDATE general SET {col_name} = ?;', (val,)) + + if mapping_use_mode is not None: + set_opt('mapping_use_mode', mapping_use_mode.value) + if default_allow_scripts is not None: + set_opt('default_allow_scripts', default_allow_scripts) + if repo_refresh_seconds is not None: + set_opt('repo_refresh_seconds', repo_refresh_seconds) + + self.settings = load_settings(cursor) @staticmethod def make(store_dir: Path) -> 'ConcreteHaketiloState': @@ -277,7 +296,11 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): isolation_level = None, check_same_thread = False ) + + global_settings = load_settings(connection.cursor()) + return ConcreteHaketiloState( - store_dir = store_dir, - connection = connection + store_dir = store_dir, + connection = connection, + settings = global_settings ) diff --git a/src/hydrilla/proxy/state_impl/repos.py b/src/hydrilla/proxy/state_impl/repos.py index 8a3fe64..383d147 100644 --- a/src/hydrilla/proxy/state_impl/repos.py +++ b/src/hydrilla/proxy/state_impl/repos.py @@ -363,8 +363,3 @@ class ConcreteRepoStore(st.RepoStore): result.append(make_repo_display_info(ref, *rest)) return result - - -@dc.dataclass(frozen=True, unsafe_hash=True) -class ConcreteRepoIterationRef(st.RepoIterationRef): - state: base.HaketiloStateWithFields diff --git a/src/hydrilla/proxy/web_ui/options.py b/src/hydrilla/proxy/web_ui/options.py new file mode 100644 index 0000000..f24c356 --- /dev/null +++ b/src/hydrilla/proxy/web_ui/options.py @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +# Proxy web UI options page. +# +# This file is part of Hydrilla&Haketilo. +# +# Copyright (C) 2022 Wojtek Kosior +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# +# I, Wojtek Kosior, thereby promise not to sue for violation of this +# file's license. Although I request that you do not make use this code +# in a proprietary program, I am not going to enforce this in court. + +# Enable using with Python 3.7. +from __future__ import annotations + +import typing as t + +import flask +import werkzeug + +from .. import state as st +from . import _app + + +bp = flask.Blueprint('options', __package__) + +@bp.route('/options', methods=['GET']) +def options(errors: t.Mapping[str, bool] = {}) -> werkzeug.Response: + html = flask.render_template( + 'options.html.jinja', + settings = _app.get_haketilo_state().get_settings(), + **errors + ) + return flask.make_response(html, 200) + +@bp.route('/options', methods=['POST']) +def options_post() -> werkzeug.Response: + action = flask.request.form['action'] + + state = _app.get_haketilo_state() + + if action == 'use_enabled': + state.update_settings(mapping_use_mode=st.MappingUseMode.WHEN_ENABLED) + elif action == 'use_auto': + state.update_settings(mapping_use_mode=st.MappingUseMode.AUTO) + elif action == 'use_question': + state.update_settings(mapping_use_mode=st.MappingUseMode.QUESTION) + elif action == 'allow_scripts': + state.update_settings(default_allow_scripts=True) + elif action == 'block_scripts': + state.update_settings(default_allow_scripts=False) + else: + raise ValueError() + + return flask.redirect(flask.url_for('.options'), 303) diff --git a/src/hydrilla/proxy/web_ui/prompts.py b/src/hydrilla/proxy/web_ui/prompts.py index b546e47..58b7906 100644 --- a/src/hydrilla/proxy/web_ui/prompts.py +++ b/src/hydrilla/proxy/web_ui/prompts.py @@ -42,30 +42,37 @@ from . import _app bp = flask.Blueprint('prompts', __package__) -@bp.route('/auto_install_error', methods=['GET']) -def auto_install_error_prompt() -> werkzeug.Response: - state = _app.get_haketilo_state() +def deserialized_request_details(salt: str) -> t.Mapping[str, str]: serializer = URLSafeSerializer( - state.get_secret(), - salt = 'auto_install_error' + _app.get_haketilo_state().get_secret(), + salt = salt ) + return serializer.loads(flask.request.args['details']) + + +@bp.route('/auto_install_error', methods=['GET']) +def auto_install_error_prompt(errors: t.Mapping[str, bool] = {}) \ + -> werkzeug.Response: try: - details: t.Mapping[str, str] = \ - serializer.loads(flask.request.args['details']) + details = deserialized_request_details('auto_install_error') except: return flask.redirect(flask.url_for('home')) try: - payload_ref = state.payload_store().get(details['payload_id']) + payload_store = _app.get_haketilo_state().payload_store() + payload_ref = payload_store.get(details['payload_id']) display_info = payload_ref.get_display_info() + if not display_info.has_problems: + return flask.redirect(details['next_url']) + html = flask.render_template( 'prompts/auto_install_error.html.jinja', display_info = display_info, - next_url = details['next_url'] + **errors ) return flask.make_response(html, 200) except st.MissingItemError: @@ -73,12 +80,16 @@ def auto_install_error_prompt() -> werkzeug.Response: @bp.route('/auto_install_error', methods=['POST']) def auto_install_error_prompt_post() -> werkzeug.Response: + try: + details = deserialized_request_details('auto_install_error') + except: + return flask.redirect(flask.url_for('home'), code=303) + form_data = flask.request.form action = form_data['action'] mapping_ver_id = str(int(form_data['mapping_ver_id'])) - payload_id = str(int(form_data['payload_id'])) - next_url = form_data['next_url'] + payload_id = str(int(details['payload_id'])) state = _app.get_haketilo_state() @@ -92,24 +103,81 @@ def auto_install_error_prompt_post() -> werkzeug.Response: if action == 'disable_mapping': mapping_ver_ref.update_mapping_status(st.EnabledStatus.DISABLED) elif action == 'retry_install': - payload_ref.ensure_items_installed() + payload_ref.ensure_items_installed() else: raise ValueError() - except (st.RepoCommunicationError, st.FileInstallationError): - params = {'payload_id': payload_id, 'next_url': next_url} + except st.RepoCommunicationError: + assert action == 'retry_install' + return auto_install_error_prompt({'repo_communication_error': True}) + except st.FileInstallationError: + assert action == 'retry_install' + return auto_install_error_prompt({'file_installation_error': True}) + except st.MissingItemError: + flask.abort(404) - serializer = URLSafeSerializer( - state.get_secret(), - salt = 'auto_install_error' - ) - query = urlencode({'details': params}) - redirect_url = flask.url_for( - '.auto_install_error_prompt', - details = serializer.dumps(params) + return flask.redirect(details['next_url']) + + +@bp.route('/package_suggestion', methods=['GET']) +def package_suggestion_prompt(errors: t.Mapping[str, bool] = {}) \ + -> werkzeug.Response: + try: + details = deserialized_request_details('package_suggestion') + except: + return flask.redirect(flask.url_for('home')) + + try: + payload_store = _app.get_haketilo_state().payload_store() + payload_ref = payload_store.get(details['payload_id']) + + display_info = payload_ref.get_display_info() + + if display_info.mapping_info.active != st.ActiveStatus.AUTO: + return flask.redirect(details['next_url']) + + html = flask.render_template( + 'prompts/package_suggestion.html.jinja', + display_info = display_info, + **errors ) + return flask.make_response(html, 200) + except st.MissingItemError: + flask.abort(404) + +@bp.route('/package_suggestion', methods=['POST']) +def package_suggestion_prompt_post() -> werkzeug.Response: + try: + details = deserialized_request_details('package_suggestion') + except: + return flask.redirect(flask.url_for('home')) + + form_data = flask.request.form + action = form_data['action'] - return flask.redirect(redirect_url) + mapping_ver_id = str(int(form_data['mapping_ver_id'])) + + state = _app.get_haketilo_state() + + try: + mapping_ver_store = state.mapping_version_store() + mapping_ver_ref = mapping_ver_store.get(mapping_ver_id) + + if action == 'disable_mapping': + mapping_ver_ref.update_mapping_status(st.EnabledStatus.DISABLED) + elif action == 'enable_mapping': + mapping_ver_ref.update_mapping_status( + enabled = st.EnabledStatus.ENABLED, + frozen = st.FrozenStatus.EXACT_VERSION + ) + else: + raise ValueError() + except st.RepoCommunicationError: + assert action == 'enable_mapping' + return package_suggestion_prompt({'repo_communication_error': True}) + except st.FileInstallationError: + assert action == 'enable_mapping' + return package_suggestion_prompt({'file_installation_error': True}) except st.MissingItemError: flask.abort(404) - return flask.redirect(next_url) + return flask.redirect(details['next_url']) diff --git a/src/hydrilla/proxy/web_ui/root.py b/src/hydrilla/proxy/web_ui/root.py index ff7c1f7..855345e 100644 --- a/src/hydrilla/proxy/web_ui/root.py +++ b/src/hydrilla/proxy/web_ui/root.py @@ -44,6 +44,7 @@ from ...translations import translation as make_translation from ... import item_infos from .. import state as st from .. import http_messages +from . import options from . import repos from . import items from . import prompts @@ -89,10 +90,11 @@ class WebUIAppImpl(_app.WebUIApp): self.jinja_env.globals['InstalledStatus'] = st.InstalledStatus self.jinja_env.globals['ActiveStatus'] = st.ActiveStatus self.jinja_env.globals['ItemType'] = item_infos.ItemType + self.jinja_env.globals['MappingUseMode'] = st.MappingUseMode self.before_request(authenticate_by_referrer) - for blueprint in [repos.bp, items.bp, prompts.bp]: + for blueprint in [repos.bp, items.bp, prompts.bp, options.bp]: self.register_blueprint(blueprint) # Flask app is not thread-safe and has to be accompanied by an ugly lock. This diff --git a/src/hydrilla/proxy/web_ui/templates/base.html.jinja b/src/hydrilla/proxy/web_ui/templates/base.html.jinja index 918fb28..06af7d9 100644 --- a/src/hydrilla/proxy/web_ui/templates/base.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/base.html.jinja @@ -20,9 +20,9 @@ in a proprietary work, I am not going to enforce this in court. -#} -{% macro button_row(buttons_data) %} +{% macro button_row(buttons_data, common_fields={}) %}
- {% for classes, text, action in buttons_data %} + {% for classes, text, extra_fields in buttons_data %} {% if not loop.first %}
{% do classes.append('button-bordering-left') %} @@ -39,7 +39,13 @@ in a proprietary work, I am not going to enforce this in court. {% endif %}
- + {% for name, value in extra_fields.items() %} + + {% endfor %} + {% for name, value in common_fields.items() %} + + {% endfor %} + @@ -92,6 +98,10 @@ in a proprietary work, I am not going to enforce this in court. background-color: #fcc; } + #main > .error-note:first-child { + margin-top: 10px; + } + .block-with-bottom-margin, .flex-row, aside, p { display: block; margin: 0 0 10px 0; @@ -226,6 +236,7 @@ in a proprietary work, I am not going to enforce this in court. {% set navigation_bar = [ ('home', _('web_ui.base.nav.home')), + ('options.options', _('web_ui.base.nav.options')), ('items.packages', _('web_ui.base.nav.packages')), ('items.libraries', _('web_ui.base.nav.libraries')), ('repos.repos', _('web_ui.base.nav.repos')), diff --git a/src/hydrilla/proxy/web_ui/templates/items/item_viewversion.html.jinja b/src/hydrilla/proxy/web_ui/templates/items/item_viewversion.html.jinja index f1d34cc..edfb772 100644 --- a/src/hydrilla/proxy/web_ui/templates/items/item_viewversion.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/items/item_viewversion.html.jinja @@ -91,10 +91,14 @@ in a proprietary work, I am not going to enforce this in court. {% do uninstall_but_classes.append('disabled-button') %} {% endif %} {% endif %}{# else/ version_display_info.installed == InstalledStatus.FA... #} + + {% set uninstall_fields = {'action': 'uninstall_item_version'} %} + {% set install_fields = {'action': 'install_item_version'} %} + {{ button_row([ - (uninstall_but_classes, uninstall_text, 'uninstall_item_version'), - (install_but_classes, install_text, 'install_item_version') + (uninstall_but_classes, uninstall_text, uninstall_fields), + (install_but_classes, install_text, install_fields) ]) }} {% endblock main_info %} diff --git a/src/hydrilla/proxy/web_ui/templates/items/package_viewversion.html.jinja b/src/hydrilla/proxy/web_ui/templates/items/package_viewversion.html.jinja index 1eb9878..c9448e7 100644 --- a/src/hydrilla/proxy/web_ui/templates/items/package_viewversion.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/items/package_viewversion.html.jinja @@ -89,9 +89,9 @@ in a proprietary work, I am not going to enforce this in court. {{ button_row([ - (disable_but_classes, disable_text, 'disable_item'), - (unenable_but_classes, unenable_text, 'unenable_item'), - (enable_but_classes, enable_text, 'enable_item_version') + (disable_but_classes, disable_text, {'action': 'disable_item'}), + (unenable_but_classes, unenable_text, {'action': 'unenable_item'}), + (enable_but_classes, enable_text, {'action': 'enable_item_version'}) ]) }} @@ -182,9 +182,9 @@ in a proprietary work, I am not going to enforce this in court. {{ button_row([ - (unpin_but_classes, unpin_text, 'unfreeze_item'), - (pin_repo_but_classes, pin_repo_text, 'freeze_to_repo'), - (pin_ver_but_classes, pin_ver_text, 'freeze_to_version') + (unpin_but_classes, unpin_text, {'action': 'unfreeze_item'}), + (pin_repo_but_classes, pin_repo_text, {'action': 'freeze_to_repo'}), + (pin_ver_but_classes, pin_ver_text, {'action': 'freeze_to_version'}) ]) }} diff --git a/src/hydrilla/proxy/web_ui/templates/options.html.jinja b/src/hydrilla/proxy/web_ui/templates/options.html.jinja new file mode 100644 index 0000000..69fc1b0 --- /dev/null +++ b/src/hydrilla/proxy/web_ui/templates/options.html.jinja @@ -0,0 +1,86 @@ +{# +SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + +Proxy web UI settings page. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior + +Dual licensed under +* GNU General Public License v3.0 or later and +* Creative Commons Attribution Share Alike 4.0 International. + +You can choose to use either of these licenses or both. + + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's licenses. Although I request that you do not make use this code +in a proprietary work, I am not going to enforce this in court. +#} +{% extends "base.html.jinja" %} + +{% block title %} {{ _('web_ui.options.title') }} {% endblock %} + +{% block main %} +

+ {{ _('web_ui.options.heading') }} +

+ + {% set use_enabled_but_classes = ['green-button'] %} + {% set use_auto_but_classes = ['green-button'] %} + {% set use_question_but_classes = ['green-button'] %} + +

+ {% if settings.mapping_use_mode == MappingUseMode.WHEN_ENABLED %} + {% do use_enabled_but_classes.append('disabled-button') %} + {{ _('web_ui.options.packages_are_used_when_enabled') }} + {% elif settings.mapping_use_mode == MappingUseMode.QUESTION %} + {% do use_question_but_classes.append('disabled-button') %} + {{ _('web_ui.options.user_gets_asked_whether_to_enable_package') }} + {% else %} + {# settings.mapping_use_mode == MappingUseMode.AUTO #} + {% do use_auto_but_classes.append('disabled-button') %} + {{ _('web_ui.options.packages_are_used_automatically') }} + {% endif %} +

+ + {{ + button_row([ + (use_enabled_but_classes, + _('web_ui.options.use_enabled_button'), + {'action': 'use_enabled'}), + (use_question_but_classes, + _('web_ui.options.use_question_button'), + {'action': 'use_question'}), + (use_auto_but_classes, + _('web_ui.options.use_auto_button'), + {'action': 'use_auto'}) + ]) + }} + +
+ + {% set allow_but_classes = ['red-button'] %} + {% set block_but_classes = ['green-button'] %} + +

+ {% if settings.default_allow_scripts %} + {% do allow_but_classes.append('disabled-button') %} + {{ _('web_ui.options.scripts_are_allowed_by_default') }} + {% else %} + {% do block_but_classes.append('disabled-button') %} + {{ _('web_ui.options.scripts_are_blocked_by_default') }} + {% endif %} +

+ + {% set allow_but_text = _('web_ui.options.allow_scripts_button') %} + {% set block_but_text = _('web_ui.options.block_scripts_button') %} + + {{ + button_row([ + (allow_but_classes, allow_but_text, {'action': 'allow_scripts'}), + (block_but_classes, block_but_text, {'action': 'block_scripts'}) + ]) + }} +{% endblock %} diff --git a/src/hydrilla/proxy/web_ui/templates/prompts/auto_install_error.html.jinja b/src/hydrilla/proxy/web_ui/templates/prompts/auto_install_error.html.jinja index 01f5c19..82a12e5 100644 --- a/src/hydrilla/proxy/web_ui/templates/prompts/auto_install_error.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/prompts/auto_install_error.html.jinja @@ -24,19 +24,15 @@ in a proprietary work, I am not going to enforce this in court. {{ _('web_ui.prompts.auto_install_error.title') }} {% endblock %} -{% macro button_form(action, button_class, button_text) %} - - - - {% set mapping_ver_id = display_info.mapping_info.ref.id %} - - - - -
-{% endmacro %} - {% block main %} + {% if file_installation_error is defined %} + {{ error_note(_('web_ui.err.retry_install.file_installation_error')) }} + {% endif %} + + {% if repo_communication_error is defined %} + {{ error_note(_('web_ui.err.retry_install.repo_communication_error')) }} + {% endif %} +

{{ _('web_ui.prompts.auto_install_error.heading') }}

@@ -48,13 +44,14 @@ in a proprietary work, I am not going to enforce this in court. }}

-
- {% set but_text = _('web_ui.prompts.auto_install_error.disable_button') %} - {{ button_form('disable_mapping', 'red-button', but_text) }} - -
+ {% set disable_text = _('web_ui.prompts.auto_install_error.disable_button') %} + {% set retry_text = _('web_ui.prompts.auto_install_error.retry_button') %} - {% set but_text = _('web_ui.prompts.auto_install_error.retry_button') %} - {{ button_form('retry_install', 'green-button', but_text) }} -
+ {{ + button_row([ + (['red-button'], disable_text, {'action': 'disable_mapping'}), + (['green-button'], retry_text, {'action': 'retry_install'}) + ], {'mapping_ver_id': display_info.mapping_info.ref.id} + ) + }} {% endblock %} diff --git a/src/hydrilla/proxy/web_ui/templates/prompts/package_suggestion.html.jinja b/src/hydrilla/proxy/web_ui/templates/prompts/package_suggestion.html.jinja new file mode 100644 index 0000000..ea906bb --- /dev/null +++ b/src/hydrilla/proxy/web_ui/templates/prompts/package_suggestion.html.jinja @@ -0,0 +1,58 @@ +{# +SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + +Proxy web UI page that asks whether to enable a package that can be used with +current site. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior + +Dual licensed under +* GNU General Public License v3.0 or later and +* Creative Commons Attribution Share Alike 4.0 International. + +You can choose to use either of these licenses or both. + + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's licenses. Although I request that you do not make use this code +in a proprietary work, I am not going to enforce this in court. +#} +{% extends "base.html.jinja" %} + +{% block title %} + {{ _('web_ui.prompts.package_suggestion.title') }} +{% endblock %} + +{% block main %} + {% if file_installation_error is defined %} + {{ error_note(_('web_ui.err.file_installation_error')) }} + {% endif %} + + {% if repo_communication_error is defined %} + {{ error_note(_('web_ui.err.repo_communication_error')) }} + {% endif %} + +

+ {{ _('web_ui.prompts.package_suggestion.heading') }} +

+ +

+ {{ + _('web_ui.prompts.package_suggestion.do_you_want_to_enable_package_{}') + .format(display_info.mapping_info.info.long_name) + }} +

+ + {% set disable_text = _('web_ui.prompts.package_suggestion.disable_button') %} + {% set enable_text = _('web_ui.prompts.package_suggestion.enable_button') %} + + {{ + button_row([ + (['red-button'], disable_text, {'action': 'disable_mapping'}), + (['blue-button'], enable_text, {'action': 'enable_mapping'}) + ], {'mapping_ver_id': display_info.mapping_info.ref.id} + ) + }} +{% endblock %} diff --git a/src/hydrilla/proxy/web_ui/templates/repos/show_single.html.jinja b/src/hydrilla/proxy/web_ui/templates/repos/show_single.html.jinja index 448c451..2c695d0 100644 --- a/src/hydrilla/proxy/web_ui/templates/repos/show_single.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/repos/show_single.html.jinja @@ -152,7 +152,8 @@ in a proprietary work, I am not going to enforce this in court.

{% set button_text = _('web_ui.repos.single.refresh_now_button') %} - {{ button_row([[['green-button'], button_text, 'refresh_repo']]) }} + {% set extra_fields = {'action': 'refresh_repo'} %} + {{ button_row([(['green-button'], button_text, extra_fields)]) }}
@@ -171,6 +172,7 @@ in a proprietary work, I am not going to enforce this in court.
{% set button_text = _('web_ui.repos.single.remove_button') %} - {{ button_row([[['green-button'], button_text, 'remove_repo']]) }} + {% set extra_fields = {'action': 'remove_repo'} %} + {{ button_row([(['green-button'], button_text, extra_fields)]) }} {% endif %} {% endblock %} -- cgit v1.2.3