Coverage for app/venv/lib/python3.14/site-packages/weblate/accounts/middleware.py: 58%
69 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
1# Copyright © Michal Čihař <michal@weblate.org>
2#
3# SPDX-License-Identifier: GPL-3.0-or-later
4from __future__ import annotations
6import re
7from typing import TYPE_CHECKING
9from django.conf import settings
10from django.contrib import auth
11from django.contrib.auth.decorators import login_required
12from django.contrib.auth.models import AnonymousUser
13from django.utils.functional import SimpleLazyObject
14from django.utils.translation import activate, get_language, get_language_from_request
15from django_otp.middleware import OTPMiddleware
17from weblate.accounts.models import set_lang_cookie
18from weblate.accounts.utils import adjust_session_expiry
19from weblate.auth.models import get_anonymous
21if TYPE_CHECKING: 21 ↛ 22line 21 didn't jump to line 22 because the condition on line 21 was never true
22 from weblate.auth.models import AuthenticatedHttpRequest
25def get_user(request: AuthenticatedHttpRequest):
26 """
27 Based on django.contrib.auth.middleware.get_user.
29 Adds handling of anonymous user which is stored in database.
30 """
31 if not hasattr(request, "weblate_cached_user"): 31 ↛ 40line 31 didn't jump to line 40 because the condition on line 31 was always true
32 user = auth.get_user(request)
33 if isinstance(user, AnonymousUser): 33 ↛ 39line 33 didn't jump to line 39 because the condition on line 33 was always true
34 user = get_anonymous()
35 # Make sure user permissions are fetched again, needed as
36 # get_anonymous() is reusing same instance.
37 user.clear_cache()
39 request.weblate_cached_user = user
40 return request.weblate_cached_user
43class AuthenticationMiddleware(OTPMiddleware):
44 """
45 Copy of django.contrib.auth.middleware.AuthenticationMiddleware.
47 It subclasses OTPMiddleware to get access to _verify_user.
48 """
50 def __init__(self, get_response=None) -> None:
51 self.get_response = get_response
53 def __call__(self, request: AuthenticatedHttpRequest):
54 from weblate.lang.models import Language
56 # Django uses lazy object here, but we need the user in pretty
57 # much every request, so there is no reason to delay this
58 request.user = user = get_user(request)
59 self._verify_user(request, user)
61 # Get language to use in this request
62 if user.is_authenticated and user.profile.language: 62 ↛ 63line 62 didn't jump to line 63 because the condition on line 62 was never true
63 language = user.profile.language
64 else:
65 language = get_language_from_request(request)
67 request.accepted_language = SimpleLazyObject(
68 lambda: Language.objects.get_request_language(request)
69 )
71 # Extend session expiry for authenticated users
72 if user.is_authenticated: 72 ↛ 73line 72 didn't jump to line 73 because the condition on line 72 was never true
73 adjust_session_expiry(request=request, user=user, is_login=False)
75 # Based on django.middleware.locale.LocaleMiddleware
76 activate(language)
77 request.LANGUAGE_CODE = get_language()
79 # Invoke the request
80 response = self.get_response(request)
82 # Update the language cookie if needed
83 if user.is_authenticated and user.profile.language != request.COOKIES.get( 83 ↛ 86line 83 didn't jump to line 86 because the condition on line 83 was never true
84 settings.LANGUAGE_COOKIE_NAME
85 ):
86 set_lang_cookie(response, user.profile)
88 return response
91class RequireLoginMiddleware:
92 """
93 Middleware that applies the login_required decorator to matching URL patterns.
95 To use, add the class to MIDDLEWARE and
96 define LOGIN_REQUIRED_URLS and LOGIN_REQUIRED_URLS_EXCEPTIONS in your
97 settings.py. For example:
98 ------
99 LOGIN_REQUIRED_URLS = (
100 r'/topsecret/(.*)$',
101 )
102 LOGIN_REQUIRED_URLS_EXCEPTIONS = (
103 r'/topsecret/login(.*)$',
104 r'/topsecret/logout(.*)$',
105 )
106 ------
107 LOGIN_REQUIRED_URLS is where you define URL patterns; each pattern must
108 be a valid regex.
110 LOGIN_REQUIRED_URLS_EXCEPTIONS is, conversely, where you explicitly
111 define any exceptions (like login and logout URLs).
112 """
114 def __init__(self, get_response=None) -> None:
115 self.get_response = get_response
116 self.required = self.get_setting_re(settings.LOGIN_REQUIRED_URLS)
117 self.exceptions = self.get_setting_re(settings.LOGIN_REQUIRED_URLS_EXCEPTIONS)
119 def get_setting_re(self, setting):
120 """Grab regexp list from settings and compiles them."""
121 return tuple(
122 re.compile(url.replace("{URL_PREFIX}", settings.URL_PREFIX))
123 for url in setting
124 )
126 def process_view(
127 self, request: AuthenticatedHttpRequest, view_func, view_args, view_kwargs
128 ):
129 """Check request whether it needs to enforce login for this URL."""
130 # No need to process URLs if not configured
131 if not self.required: 131 ↛ 135line 131 didn't jump to line 135 because the condition on line 131 was always true
132 return None
134 # No need to process URLs if user already signed in
135 if request.user.is_authenticated:
136 return None
138 # Let gitexporter handle authentication
139 # - it doesn't go through standard Django authentication
140 # - once HTTP_AUTHORIZATION is set, it enforces it
141 if "weblate.gitexport" in settings.INSTALLED_APPS:
142 import weblate.gitexport.views
144 if request.path.startswith(f"{settings.URL_PREFIX}/git/"):
145 if request.headers.get("authorization"):
146 return None
147 return weblate.gitexport.views.response_authenticate()
149 # An exception match should immediately return None
150 for url in self.exceptions:
151 if url.match(request.path):
152 return None
154 # Requests matching a restricted URL pattern are returned
155 # wrapped with the login_required decorator
156 for url in self.required:
157 if url.match(request.path):
158 return login_required(view_func)(request, *view_args, **view_kwargs)
160 # Explicitly return None for all non-matching requests
161 return None
163 def __call__(self, request: AuthenticatedHttpRequest):
164 return self.get_response(request)