diff options
Diffstat (limited to 'src/hydrilla/proxy/web_ui')
11 files changed, 211 insertions, 54 deletions
diff --git a/src/hydrilla/proxy/web_ui/_app.py b/src/hydrilla/proxy/web_ui/_app.py index d5783d1..ab15918 100644 --- a/src/hydrilla/proxy/web_ui/_app.py +++ b/src/hydrilla/proxy/web_ui/_app.py @@ -4,6 +4,8 @@ # # Available under the terms of Creative Commons Zero v1.0 Universal. +import typing as t + import flask from .. import state as st @@ -11,3 +13,6 @@ from .. import state as st class WebUIApp(flask.Flask): _haketilo_state: st.HaketiloState + +def get_haketilo_state() -> st.HaketiloState: + return t.cast(WebUIApp, flask.current_app)._haketilo_state diff --git a/src/hydrilla/proxy/web_ui/packages.py b/src/hydrilla/proxy/web_ui/packages.py index f38ea13..90876aa 100644 --- a/src/hydrilla/proxy/web_ui/packages.py +++ b/src/hydrilla/proxy/web_ui/packages.py @@ -35,7 +35,6 @@ import tempfile import zipfile import typing as t -from urllib.parse import urlparse from pathlib import Path import flask @@ -55,16 +54,12 @@ class InvalidUploadedMalcontent(HaketiloException): bp = flask.Blueprint('load_packages', __package__) @bp.route('/packages/load_from_disk', methods=['GET']) -def load_from_disk_get() -> flask.Response: +def load_from_disk_get() -> werkzeug.Response: html = flask.render_template('packages__load_from_disk.html.jinja') return flask.make_response(html, 200) @bp.route('/packages/load_from_disk', methods=['POST']) def load_from_disk_post() -> werkzeug.Response: - parsed_url = urlparse(flask.request.referrer) - if parsed_url.netloc != 'hkt.mitm.it': - return load_from_disk_get() - zip_file_storage = flask.request.files.get('packages_zipfile') if zip_file_storage is None: return load_from_disk_get() @@ -90,31 +85,27 @@ def load_from_disk_post() -> werkzeug.Response: else: malcontent_dir_path = tmpdir_child - state = t.cast(_app.WebUIApp, flask.current_app)._haketilo_state - try: - state.import_packages(malcontent_dir_path) + _app.get_haketilo_state().import_packages(malcontent_dir_path) except: raise InvalidUploadedMalcontent() return flask.redirect(flask.url_for('.packages')) @bp.route('/packages') -def packages() -> flask.Response: - state = t.cast(_app.WebUIApp, flask.current_app)._haketilo_state +def packages() -> werkzeug.Response: + store = _app.get_haketilo_state().mapping_version_store() html = flask.render_template( 'packages.html.jinja', - display_infos = state.mapping_version_store().get_display_infos() + display_infos = store.get_display_infos() ) return flask.make_response(html, 200) @bp.route('/packages/view/<string:mapping_id>') -def show_package(mapping_id: str) -> flask.Response: - state = t.cast(_app.WebUIApp, flask.current_app)._haketilo_state - +def show_package(mapping_id: str) -> werkzeug.Response: try: - store = state.mapping_version_store() + store = _app.get_haketilo_state().mapping_version_store() display_info = store.get(mapping_id).get_display_info() html = flask.render_template( diff --git a/src/hydrilla/proxy/web_ui/repos.py b/src/hydrilla/proxy/web_ui/repos.py index d4e81c0..166cf53 100644 --- a/src/hydrilla/proxy/web_ui/repos.py +++ b/src/hydrilla/proxy/web_ui/repos.py @@ -34,6 +34,7 @@ from __future__ import annotations import typing as t import flask +import werkzeug from .. import state as st from . import _app @@ -41,11 +42,36 @@ from . import _app bp = flask.Blueprint('repos', __package__) +@bp.route('/repos/add', methods=['GET']) +def add_repo_get(errors: t.Mapping[str, bool] = {}) -> werkzeug.Response: + html = flask.render_template('repos__add.html.jinja', **errors) + return flask.make_response(html, 200) + +@bp.route('/repos/add', methods=['POST']) +def add_repo_post() -> werkzeug.Response: + form_data = flask.request.form + if 'name' not in form_data or 'url' not in form_data: + return add_repo_get() + + try: + new_repo_ref = _app.get_haketilo_state().repo_store().add( + name = form_data['name'], + url = form_data['url'] + ) + except st.RepoNameInvalid: + return add_repo_get({'repo_name_invalid': True}) + except st.RepoNameTaken: + return add_repo_get({'repo_name_taken': True}) + except st.RepoUrlInvalid: + return add_repo_get({'repo_url_invalid': True}) + + return flask.redirect(flask.url_for('.show_repo', repo_id=new_repo_ref.id)) + @bp.route('/repos') -def repos() -> flask.Response: - state = t.cast(_app.WebUIApp, flask.current_app)._haketilo_state +def repos() -> werkzeug.Response: + repo_store = _app.get_haketilo_state().repo_store() - local_semirepo_info, *repo_infos = state.repo_store().get_display_infos() + local_semirepo_info, *repo_infos = repo_store.get_display_infos() html = flask.render_template( 'repos.html.jinja', @@ -54,12 +80,10 @@ def repos() -> flask.Response: ) return flask.make_response(html, 200) -@bp.route('/repos/view/<string:repo_id>', methods=['GET']) -def show_repo(repo_id: str) -> flask.Response: - state = t.cast(_app.WebUIApp, flask.current_app)._haketilo_state - +@bp.route('/repos/view/<string:repo_id>') +def show_repo(repo_id: str) -> werkzeug.Response: try: - store = state.repo_store() + store = _app.get_haketilo_state().repo_store() display_info = store.get(repo_id).get_display_info() html = flask.render_template( @@ -70,6 +94,44 @@ def show_repo(repo_id: str) -> flask.Response: except st.MissingItemError: flask.abort(404) -@bp.route('/repos/view/<string:repo_id>', methods=['POST']) -def update_repo(repo_id: str) -> flask.Response: - raise NotImplementedError() +def sanitize_altered_repo_id(repo_id: str) -> str: + repo_id = str(int(repo_id)) + if repo_id == '1': + # Protect local semi-repo. + flask.abort(403) + + return repo_id + +@bp.route('/repos/update_url/<string:repo_id>', methods=['POST']) +def update_repo_url(repo_id: str) -> werkzeug.Response: + repo_id = sanitize_altered_repo_id(repo_id) + + try: + repo_ref = _app.get_haketilo_state().repo_store().get(repo_id) + repo_ref.update() + except st.MissingItemError: + flask.abort(404) + + return flask.redirect(flask.url_for('.show_repo', repo_id=repo_id)) + +@bp.route('/repos/remove/<string:repo_id>', methods=['POST']) +def remove_repo(repo_id: str): + repo_id = sanitize_altered_repo_id(repo_id) + + try: + _app.get_haketilo_state().repo_store().get(repo_id).remove() + except st.MissingItemError: + flask.abort(404) + + return flask.redirect(flask.url_for('.repos')) + +@bp.route('/repos/refresh/<string:repo_id>', methods=['POST']) +def refresh_repo(repo_id: str): + repo_id = sanitize_altered_repo_id(repo_id) + + try: + _app.get_haketilo_state().repo_store().get(repo_id).refresh() + except st.MissingItemError: + flask.abort(404) + + return flask.redirect(flask.url_for('.show_repo', repo_id=repo_id)) diff --git a/src/hydrilla/proxy/web_ui/root.py b/src/hydrilla/proxy/web_ui/root.py index 64d6be1..0f42981 100644 --- a/src/hydrilla/proxy/web_ui/root.py +++ b/src/hydrilla/proxy/web_ui/root.py @@ -32,10 +32,13 @@ from __future__ import annotations import typing as t + from threading import Lock +from urllib.parse import urlparse import jinja2 import flask +import werkzeug from ...translations import translation as make_translation from ... import versions @@ -46,6 +49,17 @@ from . import packages from . import _app +def authenticate_by_referrer() -> t.Optional[werkzeug.Response]: + if flask.request.method == 'GET': + return None + + parsed_url = urlparse(flask.request.referrer) + if parsed_url.netloc == 'hkt.mitm.it': + return None + + flask.abort(403) + + class WebUIAppImpl(_app.WebUIApp): def __init__(self): super().__init__(__name__) @@ -60,6 +74,8 @@ class WebUIAppImpl(_app.WebUIApp): ] } + self.before_request(authenticate_by_referrer) + for blueprint in [repos.bp, packages.bp]: self.register_blueprint(blueprint) @@ -71,7 +87,7 @@ app_lock = Lock() @app.route('/') -def respond(): +def respond() -> str: return flask.render_template('root.html.jinja') diff --git a/src/hydrilla/proxy/web_ui/templates/base.html.jinja b/src/hydrilla/proxy/web_ui/templates/base.html.jinja index bca5948..c7a0c15 100644 --- a/src/hydrilla/proxy/web_ui/templates/base.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/base.html.jinja @@ -45,6 +45,12 @@ in a proprietary work, I am not going to enforce this in court. color: #555; } + .error-note { + display: block; + border-left: 5px solid #a33; + background-color: #fcc; + } + .hide { display: none !important; } diff --git a/src/hydrilla/proxy/web_ui/templates/include/checkbox_tricks_style.css.jinja b/src/hydrilla/proxy/web_ui/templates/include/checkbox_tricks_style.css.jinja index b4c8edc..471725b 100644 --- a/src/hydrilla/proxy/web_ui/templates/include/checkbox_tricks_style.css.jinja +++ b/src/hydrilla/proxy/web_ui/templates/include/checkbox_tricks_style.css.jinja @@ -30,7 +30,7 @@ input.chbx-tricks-hide-show:not(:checked)+*+* { display: none !important; } -input.chbx-tricks-hide-show:checked+*+*, -input.chbx-tricks-show-hide:not(:checked)+*+* { +input.chbx-tricks-hide-show:checked+*, +input.chbx-tricks-show-hide:not(:checked)+* { display: none !important; } diff --git a/src/hydrilla/proxy/web_ui/templates/packages.html.jinja b/src/hydrilla/proxy/web_ui/templates/packages.html.jinja index 48ef80b..bcb8dea 100644 --- a/src/hydrilla/proxy/web_ui/templates/packages.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/packages.html.jinja @@ -30,11 +30,12 @@ in a proprietary work, I am not going to enforce this in court. <h3>{{ _('web_ui.packages.heading') }}</h3> <ul id="item_list"> {% for info in display_infos %} - <li - {% if info.info.repo == '<local>' %} - class="package-entry-local" - {% endif %} - > + {% if info.info.repo == '<local>' -%} + {%- set entry_classes = 'package-entry-local' -%} + {%- else -%} + {%- set entry_classes = '' -%} + {%- endif -%} + <li class="{{ entry_classes }}"> <a href="{{ url_for('.show_package', mapping_id=info.ref.id) }}"> <div> {{ info.info.long_name }} diff --git a/src/hydrilla/proxy/web_ui/templates/packages__load_from_disk.html.jinja b/src/hydrilla/proxy/web_ui/templates/packages__load_from_disk.html.jinja index 52280b2..33a99f4 100644 --- a/src/hydrilla/proxy/web_ui/templates/packages__load_from_disk.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/packages__load_from_disk.html.jinja @@ -20,7 +20,7 @@ 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 %}Load package{% endblock %} +{% block title %} {{ _('web_ui.packages.load_from_disk.title') }} {% endblock %} {% block main %} <form method="POST" enctype="multipart/form-data"> <div> @@ -29,7 +29,8 @@ in a proprietary work, I am not going to enforce this in court. </label> </div> <div> - <input id="packages_zipfile" name="packages_zipfile" type="file" required=""> + <input id="packages_zipfile" name="packages_zipfile" type="file" + required=""> </div> <div> <button>Install packages</button> diff --git a/src/hydrilla/proxy/web_ui/templates/repos.html.jinja b/src/hydrilla/proxy/web_ui/templates/repos.html.jinja index 94b7e2b..05c948e 100644 --- a/src/hydrilla/proxy/web_ui/templates/repos.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/repos.html.jinja @@ -28,6 +28,11 @@ in a proprietary work, I am not going to enforce this in court. {% endblock %} {% block main %} <h3>{{ _('web_ui.repos.heading') }}</h3> + <div> + <a href="{{ url_for('.add_repo_get') }}" class="button"> + {{ _('web_ui.repos.add_repo_button') }} + </a> + </div> <ul id="item_list"> {% for info in display_infos %} <li @@ -51,7 +56,7 @@ in a proprietary work, I am not going to enforce this in court. </li> {% endfor %} <li> - <a href="{{ url_for('.show_repo', repo_id=1) }}"> + <a href="{{ url_for('.show_repo', repo_id=local_semirepo_info.ref.id) }}"> {{ _('web_ui.repos.local_packages_semirepo') }} <div class="small-print"> {{ diff --git a/src/hydrilla/proxy/web_ui/templates/repos__add.html.jinja b/src/hydrilla/proxy/web_ui/templates/repos__add.html.jinja new file mode 100644 index 0000000..cbe7105 --- /dev/null +++ b/src/hydrilla/proxy/web_ui/templates/repos__add.html.jinja @@ -0,0 +1,62 @@ +{# +Spdx-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + + +Proxy web UI repo creation 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.repos.add.title') }} {% endblock %} +{% block main %} + <h3>{{ _('web_ui.repos.add.heading') }}</h3> + <form method="POST"> + <div> + <label for="name_field"> + {{ _('web_ui.repos.add.name_field_label') }} + </label> + </div> + {% if repo_name_invalid is defined -%} + <aside class="error-note"> + {{ _('web_ui.repos.add.repo_name_invalid') }} + </aside> + {%- endif %} + {% if repo_name_taken is defined -%} + <aside class="error-note"> + {{ _('web_ui.repos.add.repo_name_taken') }} + </aside> + {%- endif %} + <div> + <input id="name_field" name="name" required=""> + </div> + <div> + <label for="url_field"> + {{ _('web_ui.repos.add.url_field_label') }} + </label> + </div> + {% if repo_url_invalid is defined -%} + <aside class="error-note"> + {{ _('web_ui.repos.add.repo_url_invalid') }} + </aside> + {%- endif %} + <div> + <input id="url_field" name="url" required=""> + </div> + <div> + <button class="button">{{ _('web_ui.repos.add.submit_button') }}</button> + </div> + </form> +{% 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 9abb00d..96100ce 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 @@ -27,21 +27,19 @@ in a proprietary work, I am not going to enforce this in court. {% include 'include/checkbox_tricks_style.css.jinja' %} {% endblock %} {% block main %} + {% set repo_id = display_info.ref.id -%} <h3> - {% if display_info.ref.id != '1' %} - {{ - _('web_ui.repos.single.heading.name_{}') - .format(display_info.info.long_name) - }} - {% else %} + {% if display_info.is_local_semirepo -%} {{ _('web_ui.repos.local_packages_semirepo') }} - {% endif %} + {% else -%} + {{ _('web_ui.repos.single.heading.name_{}').format(display_info.name) }} + {% endif -%} </h3> - {% if display_info.deleted == True %} + {% if display_info.deleted and not display_info.is_local_semirepo -%} <div> {{ _('web_ui.repos.single.repo_is_deleted') }} </div> - {% elif display_info.deleted == False %} + {% elif not display_info.deleted -%} <input id="show_url_edit_form" type="checkbox" class="chbx-tricks-show-hide" checked=""> <div> @@ -54,7 +52,8 @@ in a proprietary work, I am not going to enforce this in court. </label> </div> </div> - <form method="POST"> + {% set action_url = url_for('.update_repo_url', repo_id=repo_id) -%} + <form method="POST" action="{{ action_url }}"> <input type="hidden" name="action" value="update_url"> <div> <input name="url" value="{{ display_info.url }}"> @@ -69,22 +68,23 @@ in a proprietary work, I am not going to enforce this in court. </div> </form> <div> - {% if display_info.last_refreshed is None %} + {% if display_info.last_refreshed is none -%} {{ _('web_ui.repos.single.repo_never_refreshed') }} - {% else %} + {% else -%} {{ _('web_ui.repos.single.last_refreshed_{}') .format(display_info.last_refreshed.strftime('%F %H:%M')) }} - {% endif %} - <form method="POST"> + {% endif -%} + {% set action_url = url_for('.refresh_repo', repo_id=repo_id) -%} + <form method="POST" action="{{ action_url }}"> <input type="hidden" name="action" value="refresh_repo"> <button class="green-button"> {{ _('web_ui.repos.single.refresh_now_button') }} </button> </form> </div> - {% endif %} + {% endif -%} <div> {{ _('web_ui.repos.item_count_{mappings}_{resources}') @@ -94,4 +94,12 @@ in a proprietary work, I am not going to enforce this in court. ) }} </div> -{% endblock %} + {% if not display_info.is_local_semirepo -%} + {% set action_url = url_for('.remove_repo', repo_id=repo_id) -%} + <form method="POST" action="{{ action_url }}"> + <button class="green-button"> + {{ _('web_ui.repos.single.remove_button') }} + </button> + </form> + {% endif -%} +{% endblock -%} |