Coverage for seedboxsync/front/utils.py: 80%
60 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 17:14 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 17:14 +0000
1#
2# Copyright (C) 2015-2026 Guillaume Kulakowski <guillaume@kulakowski.fr>
3#
4# For the full copyright and license information, please view the LICENSE
5# file that was distributed with this source code.
6#
7"""SeedboxSync utils and helpers for frontend."""
9from typing import Any
10from urllib.parse import urlsplit
11from flask import request, session
12from flask.signals import message_flashed
13from flask_wtf import FlaskForm
14from seedboxsync.core import Config, current_app
15from seedboxsync.core.database.models import SeedboxSync
16from seedboxsync.front.cache import cache
19def toast(message: str, title: str = "", category: str = "message") -> None:
20 """Flashes a message to the next request. In order to remove the
21 flashed message from the session and to display it to the user,
22 the template has to call :func:`get_flashed_messages`.
24 .. versionchanged:: 0.3
25 `category` parameter added.
27 :param message: the message to be flashed.
28 :param title: the message's title to be flashed.
29 :param category: the category for the message. The following values
30 are recommended: ``'message'`` for any kind of message,
31 ``'error'`` for errors, ``'info'`` for information
32 messages and ``'warning'`` for warnings. However any
33 kind of string can be used as category.
34 """
35 toasts = session.get("_toasts", [])
36 toasts.append((category, message, title))
37 session["_toasts"] = toasts
38 app = current_app._get_current_object() # type: ignore
39 message_flashed.send(
40 app,
41 _async_wrapper=current_app.ensure_sync,
42 message=message,
43 title=title,
44 category=category,
45 )
48def is_safe_redirect_url(target: str) -> bool:
49 """
50 Check whether a target URL is safe for local redirection.
52 Only absolute local paths are allowed. External URLs, scheme-relative
53 URLs, and paths containing backslashes are rejected.
55 Args:
56 target: The target URL string to validate.
58 Returns:
59 True if the target is a safe local absolute path, False otherwise.
60 """
61 if not target.startswith("/"):
62 return False
64 # Reject scheme-relative and browser-specific absolute URL forms.
65 if target.startswith("//") or "\\" in target:
66 return False
68 try:
69 url = urlsplit(target)
70 except ValueError:
71 return False
73 return not url.scheme and not url.netloc
76def save_settings_form(form: FlaskForm) -> None:
77 """
78 Persist submitted settings form values into runtime memory and database.
80 Extracts field values, formats boolean toggles, applies specific
81 feature-flag overrides, updates the active Flask app configuration mapping,
82 and updates database records in a batch query.
84 Args:
85 form (FlaskForm): Validated WTForms form instance containing new config values.
86 """
87 seedbox_timeout_enabled = request.form.get("seedbox_timeout_enabled", "0") == "1"
88 seedbox_chmod_enabled = request.form.get("seedbox_chmod_enabled", "0") == "1"
89 config_to_db: list[dict[str, str]] = []
90 config_to_update: dict[str, Any] = {}
92 # Load data from form
93 for field in form:
94 key = field.name
96 if key in {"csrf_token", "submit"}:
97 continue
99 if key.endswith(("_enabled", "_disabled")): # Boolean
100 value = bool(int(field.data))
101 db_value = int(field.data)
102 else:
103 value = field.data
104 db_value = field.data
106 current_app.logger.debug(f"Updated config[{Config.CONFIG_NAMESPACE}{key.upper()}] = {value}")
107 config_to_update[f"{Config.CONFIG_NAMESPACE}{key.upper()}"] = value
108 config_to_db.append({"key": f"{Config.DB_CONFIG_PREFIX}{key}", "value": str(db_value)})
110 # Override seedbox_timeout & seedbox_chmod
111 if "seedbox_timeout" in form and not seedbox_timeout_enabled:
112 current_app.logger.debug(f"Override config[{Config.CONFIG_NAMESPACE}SEEDBOX_TIMEOUT] = False")
113 config_to_update[f"{Config.CONFIG_NAMESPACE}SEEDBOX_TIMEOUT"] = False
114 config_to_db.append({"key": f"{Config.DB_CONFIG_PREFIX}seedbox_timeout", "value": "0"})
115 form["seedbox_timeout"].data = "0"
116 if "seedbox_chmod" in form and not seedbox_chmod_enabled:
117 current_app.logger.debug(f"Override config[{Config.CONFIG_NAMESPACE}SEEDBOX_CHMOD] = False")
118 config_to_update[f"{Config.CONFIG_NAMESPACE}SEEDBOX_CHMOD"] = False
119 config_to_db.append({"key": f"{Config.DB_CONFIG_PREFIX}seedbox_chmod", "value": "0"})
120 form["seedbox_chmod"].data = "0"
122 # Synchronize core Flask-Login & Flask-Wtf configuration flags
123 login_disabled_key = f"{Config.CONFIG_NAMESPACE}LOGIN_DISABLED"
124 if login_disabled_key in config_to_update:
125 current_app.config["LOGIN_DISABLED"] = config_to_update[login_disabled_key]
126 wtf_csrt_disabled_key = f"{Config.CONFIG_NAMESPACE}WTF_CSRF_ENABLED"
127 if wtf_csrt_disabled_key in config_to_update:
128 current_app.config["WTF_CSRF_ENABLED"] = config_to_update[wtf_csrt_disabled_key]
130 # Update config in Flask app
131 current_app.config.from_mapping(config_to_update)
133 # Save in database
134 SeedboxSync.replace_many(config_to_db).execute() # type: ignore[no-untyped-call]
136 # Clear cache to ensure new settings take effect
137 cache.clear()