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

1# Copyright © Michal Čihař <michal@weblate.org> 

2# 

3# SPDX-License-Identifier: GPL-3.0-or-later 

4from __future__ import annotations 

5 

6import re 

7from typing import TYPE_CHECKING 

8 

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 

16 

17from weblate.accounts.models import set_lang_cookie 

18from weblate.accounts.utils import adjust_session_expiry 

19from weblate.auth.models import get_anonymous 

20 

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 

23 

24 

25def get_user(request: AuthenticatedHttpRequest): 

26 """ 

27 Based on django.contrib.auth.middleware.get_user. 

28 

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

38 

39 request.weblate_cached_user = user 

40 return request.weblate_cached_user 

41 

42 

43class AuthenticationMiddleware(OTPMiddleware): 

44 """ 

45 Copy of django.contrib.auth.middleware.AuthenticationMiddleware. 

46 

47 It subclasses OTPMiddleware to get access to _verify_user. 

48 """ 

49 

50 def __init__(self, get_response=None) -> None: 

51 self.get_response = get_response 

52 

53 def __call__(self, request: AuthenticatedHttpRequest): 

54 from weblate.lang.models import Language 

55 

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) 

60 

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) 

66 

67 request.accepted_language = SimpleLazyObject( 

68 lambda: Language.objects.get_request_language(request) 

69 ) 

70 

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) 

74 

75 # Based on django.middleware.locale.LocaleMiddleware 

76 activate(language) 

77 request.LANGUAGE_CODE = get_language() 

78 

79 # Invoke the request 

80 response = self.get_response(request) 

81 

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) 

87 

88 return response 

89 

90 

91class RequireLoginMiddleware: 

92 """ 

93 Middleware that applies the login_required decorator to matching URL patterns. 

94 

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. 

109 

110 LOGIN_REQUIRED_URLS_EXCEPTIONS is, conversely, where you explicitly 

111 define any exceptions (like login and logout URLs). 

112 """ 

113 

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) 

118 

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 ) 

125 

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 

133 

134 # No need to process URLs if user already signed in 

135 if request.user.is_authenticated: 

136 return None 

137 

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 

143 

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

148 

149 # An exception match should immediately return None 

150 for url in self.exceptions: 

151 if url.match(request.path): 

152 return None 

153 

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) 

159 

160 # Explicitly return None for all non-matching requests 

161 return None 

162 

163 def __call__(self, request: AuthenticatedHttpRequest): 

164 return self.get_response(request)