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

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.""" 

8 

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 

17 

18 

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`. 

23 

24 .. versionchanged:: 0.3 

25 `category` parameter added. 

26 

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 ) 

46 

47 

48def is_safe_redirect_url(target: str) -> bool: 

49 """ 

50 Check whether a target URL is safe for local redirection. 

51 

52 Only absolute local paths are allowed. External URLs, scheme-relative 

53 URLs, and paths containing backslashes are rejected. 

54 

55 Args: 

56 target: The target URL string to validate. 

57 

58 Returns: 

59 True if the target is a safe local absolute path, False otherwise. 

60 """ 

61 if not target.startswith("/"): 

62 return False 

63 

64 # Reject scheme-relative and browser-specific absolute URL forms. 

65 if target.startswith("//") or "\\" in target: 

66 return False 

67 

68 try: 

69 url = urlsplit(target) 

70 except ValueError: 

71 return False 

72 

73 return not url.scheme and not url.netloc 

74 

75 

76def save_settings_form(form: FlaskForm) -> None: 

77 """ 

78 Persist submitted settings form values into runtime memory and database. 

79 

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. 

83 

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] = {} 

91 

92 # Load data from form 

93 for field in form: 

94 key = field.name 

95 

96 if key in {"csrf_token", "submit"}: 

97 continue 

98 

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 

105 

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)}) 

109 

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" 

121 

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] 

129 

130 # Update config in Flask app 

131 current_app.config.from_mapping(config_to_update) 

132 

133 # Save in database 

134 SeedboxSync.replace_many(config_to_db).execute() # type: ignore[no-untyped-call] 

135 

136 # Clear cache to ensure new settings take effect 

137 cache.clear()