From e1344ae7017b28a54d7714895bd54c8431a20bc6 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Thu, 18 Aug 2022 19:18:00 +0200 Subject: allow adding, removing and altering repositories This commit also temporarily breaks package import from files :/ --- src/hydrilla/proxy/web_ui/_app.py | 5 ++ src/hydrilla/proxy/web_ui/packages.py | 23 ++---- src/hydrilla/proxy/web_ui/repos.py | 84 +++++++++++++++++++--- src/hydrilla/proxy/web_ui/root.py | 18 ++++- .../proxy/web_ui/templates/base.html.jinja | 6 ++ .../include/checkbox_tricks_style.css.jinja | 4 +- .../proxy/web_ui/templates/packages.html.jinja | 11 +-- .../templates/packages__load_from_disk.html.jinja | 5 +- .../proxy/web_ui/templates/repos.html.jinja | 7 +- .../proxy/web_ui/templates/repos__add.html.jinja | 62 ++++++++++++++++ .../web_ui/templates/repos__show_single.html.jinja | 40 ++++++----- 11 files changed, 211 insertions(+), 54 deletions(-) create mode 100644 src/hydrilla/proxy/web_ui/templates/repos__add.html.jinja (limited to 'src/hydrilla/proxy/web_ui') 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/') -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/', methods=['GET']) -def show_repo(repo_id: str) -> flask.Response: - state = t.cast(_app.WebUIApp, flask.current_app)._haketilo_state - +@bp.route('/repos/view/') +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/', 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/', 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/', 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/', 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.

{{ _('web_ui.packages.heading') }}