Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/contrib/auth/forms.py: 40%

208 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1import unicodedata 

2 

3from django import forms 

4from django.contrib.auth import authenticate, get_user_model, password_validation 

5from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, identify_hasher 

6from django.contrib.auth.models import User 

7from django.contrib.auth.tokens import default_token_generator 

8from django.contrib.sites.shortcuts import get_current_site 

9from django.core.exceptions import ValidationError 

10from django.core.mail import EmailMultiAlternatives 

11from django.template import loader 

12from django.utils.encoding import force_bytes 

13from django.utils.http import urlsafe_base64_encode 

14from django.utils.text import capfirst 

15from django.utils.translation import gettext 

16from django.utils.translation import gettext_lazy as _ 

17 

18UserModel = get_user_model() 

19 

20 

21def _unicode_ci_compare(s1, s2): 

22 """ 

23 Perform case-insensitive comparison of two identifiers, using the 

24 recommended algorithm from Unicode Technical Report 36, section 

25 2.11.2(B)(2). 

26 """ 

27 return ( 

28 unicodedata.normalize("NFKC", s1).casefold() 

29 == unicodedata.normalize("NFKC", s2).casefold() 

30 ) 

31 

32 

33class ReadOnlyPasswordHashWidget(forms.Widget): 

34 template_name = "auth/widgets/read_only_password_hash.html" 

35 read_only = True 

36 

37 def get_context(self, name, value, attrs): 

38 context = super().get_context(name, value, attrs) 

39 summary = [] 

40 if not value or value.startswith(UNUSABLE_PASSWORD_PREFIX): 

41 summary.append({"label": gettext("No password set.")}) 

42 else: 

43 try: 

44 hasher = identify_hasher(value) 

45 except ValueError: 

46 summary.append( 

47 { 

48 "label": gettext( 

49 "Invalid password format or unknown hashing algorithm." 

50 ) 

51 } 

52 ) 

53 else: 

54 for key, value_ in hasher.safe_summary(value).items(): 

55 summary.append({"label": gettext(key), "value": value_}) 

56 context["summary"] = summary 

57 return context 

58 

59 def id_for_label(self, id_): 

60 return None 

61 

62 

63class ReadOnlyPasswordHashField(forms.Field): 

64 widget = ReadOnlyPasswordHashWidget 

65 

66 def __init__(self, *args, **kwargs): 

67 kwargs.setdefault("required", False) 

68 kwargs.setdefault("disabled", True) 

69 super().__init__(*args, **kwargs) 

70 

71 

72class UsernameField(forms.CharField): 

73 def to_python(self, value): 

74 return unicodedata.normalize("NFKC", super().to_python(value)) 

75 

76 def widget_attrs(self, widget): 

77 return { 

78 **super().widget_attrs(widget), 

79 "autocapitalize": "none", 

80 "autocomplete": "username", 

81 } 

82 

83 

84class UserCreationForm(forms.ModelForm): 

85 """ 

86 A form that creates a user, with no privileges, from the given username and 

87 password. 

88 """ 

89 

90 error_messages = { 

91 "password_mismatch": _("The two password fields didn’t match."), 

92 } 

93 password1 = forms.CharField( 

94 label=_("Password"), 

95 strip=False, 

96 widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}), 

97 help_text=password_validation.password_validators_help_text_html(), 

98 ) 

99 password2 = forms.CharField( 

100 label=_("Password confirmation"), 

101 widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}), 

102 strip=False, 

103 help_text=_("Enter the same password as before, for verification."), 

104 ) 

105 

106 class Meta: 

107 model = User 

108 fields = ("username",) 

109 field_classes = {"username": UsernameField} 

110 

111 def __init__(self, *args, **kwargs): 

112 super().__init__(*args, **kwargs) 

113 if self._meta.model.USERNAME_FIELD in self.fields: 

114 self.fields[self._meta.model.USERNAME_FIELD].widget.attrs[ 

115 "autofocus" 

116 ] = True 

117 

118 def clean_password2(self): 

119 password1 = self.cleaned_data.get("password1") 

120 password2 = self.cleaned_data.get("password2") 

121 if password1 and password2 and password1 != password2: 

122 raise ValidationError( 

123 self.error_messages["password_mismatch"], 

124 code="password_mismatch", 

125 ) 

126 return password2 

127 

128 def _post_clean(self): 

129 super()._post_clean() 

130 # Validate the password after self.instance is updated with form data 

131 # by super(). 

132 password = self.cleaned_data.get("password2") 

133 if password: 

134 try: 

135 password_validation.validate_password(password, self.instance) 

136 except ValidationError as error: 

137 self.add_error("password2", error) 

138 

139 def save(self, commit=True): 

140 user = super().save(commit=False) 

141 user.set_password(self.cleaned_data["password1"]) 

142 if commit: 

143 user.save() 

144 return user 

145 

146 

147class UserChangeForm(forms.ModelForm): 

148 password = ReadOnlyPasswordHashField( 

149 label=_("Password"), 

150 help_text=_( 

151 "Raw passwords are not stored, so there is no way to see this " 

152 "user’s password, but you can change the password using " 

153 '<a href="{}">this form</a>.' 

154 ), 

155 ) 

156 

157 class Meta: 

158 model = User 

159 fields = "__all__" 

160 field_classes = {"username": UsernameField} 

161 

162 def __init__(self, *args, **kwargs): 

163 super().__init__(*args, **kwargs) 

164 password = self.fields.get("password") 

165 if password: 

166 password.help_text = password.help_text.format("../password/") 

167 user_permissions = self.fields.get("user_permissions") 

168 if user_permissions: 

169 user_permissions.queryset = user_permissions.queryset.select_related( 

170 "content_type" 

171 ) 

172 

173 

174class AuthenticationForm(forms.Form): 

175 """ 

176 Base class for authenticating users. Extend this to get a form that accepts 

177 username/password logins. 

178 """ 

179 

180 username = UsernameField(widget=forms.TextInput(attrs={"autofocus": True})) 

181 password = forms.CharField( 

182 label=_("Password"), 

183 strip=False, 

184 widget=forms.PasswordInput(attrs={"autocomplete": "current-password"}), 

185 ) 

186 

187 error_messages = { 

188 "invalid_login": _( 

189 "Please enter a correct %(username)s and password. Note that both " 

190 "fields may be case-sensitive." 

191 ), 

192 "inactive": _("This account is inactive."), 

193 } 

194 

195 def __init__(self, request=None, *args, **kwargs): 

196 """ 

197 The 'request' parameter is set for custom auth use by subclasses. 

198 The form data comes in via the standard 'data' kwarg. 

199 """ 

200 self.request = request 

201 self.user_cache = None 

202 super().__init__(*args, **kwargs) 

203 

204 # Set the max length and label for the "username" field. 

205 self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD) 

206 username_max_length = self.username_field.max_length or 254 

207 self.fields["username"].max_length = username_max_length 

208 self.fields["username"].widget.attrs["maxlength"] = username_max_length 

209 if self.fields["username"].label is None: 

210 self.fields["username"].label = capfirst(self.username_field.verbose_name) 

211 

212 def clean(self): 

213 username = self.cleaned_data.get("username") 

214 password = self.cleaned_data.get("password") 

215 

216 if username is not None and password: 

217 self.user_cache = authenticate( 

218 self.request, username=username, password=password 

219 ) 

220 if self.user_cache is None: 

221 raise self.get_invalid_login_error() 

222 else: 

223 self.confirm_login_allowed(self.user_cache) 

224 

225 return self.cleaned_data 

226 

227 def confirm_login_allowed(self, user): 

228 """ 

229 Controls whether the given User may log in. This is a policy setting, 

230 independent of end-user authentication. This default behavior is to 

231 allow login by active users, and reject login by inactive users. 

232 

233 If the given user cannot log in, this method should raise a 

234 ``ValidationError``. 

235 

236 If the given user may log in, this method should return None. 

237 """ 

238 if not user.is_active: 

239 raise ValidationError( 

240 self.error_messages["inactive"], 

241 code="inactive", 

242 ) 

243 

244 def get_user(self): 

245 return self.user_cache 

246 

247 def get_invalid_login_error(self): 

248 return ValidationError( 

249 self.error_messages["invalid_login"], 

250 code="invalid_login", 

251 params={"username": self.username_field.verbose_name}, 

252 ) 

253 

254 

255class PasswordResetForm(forms.Form): 

256 email = forms.EmailField( 

257 label=_("Email"), 

258 max_length=254, 

259 widget=forms.EmailInput(attrs={"autocomplete": "email"}), 

260 ) 

261 

262 def send_mail( 

263 self, 

264 subject_template_name, 

265 email_template_name, 

266 context, 

267 from_email, 

268 to_email, 

269 html_email_template_name=None, 

270 ): 

271 """ 

272 Send a django.core.mail.EmailMultiAlternatives to `to_email`. 

273 """ 

274 subject = loader.render_to_string(subject_template_name, context) 

275 # Email subject *must not* contain newlines 

276 subject = "".join(subject.splitlines()) 

277 body = loader.render_to_string(email_template_name, context) 

278 

279 email_message = EmailMultiAlternatives(subject, body, from_email, [to_email]) 

280 if html_email_template_name is not None: 

281 html_email = loader.render_to_string(html_email_template_name, context) 

282 email_message.attach_alternative(html_email, "text/html") 

283 

284 email_message.send() 

285 

286 def get_users(self, email): 

287 """Given an email, return matching user(s) who should receive a reset. 

288 

289 This allows subclasses to more easily customize the default policies 

290 that prevent inactive users and users with unusable passwords from 

291 resetting their password. 

292 """ 

293 email_field_name = UserModel.get_email_field_name() 

294 active_users = UserModel._default_manager.filter( 

295 **{ 

296 "%s__iexact" % email_field_name: email, 

297 "is_active": True, 

298 } 

299 ) 

300 return ( 

301 u 

302 for u in active_users 

303 if u.has_usable_password() 

304 and _unicode_ci_compare(email, getattr(u, email_field_name)) 

305 ) 

306 

307 def save( 

308 self, 

309 domain_override=None, 

310 subject_template_name="registration/password_reset_subject.txt", 

311 email_template_name="registration/password_reset_email.html", 

312 use_https=False, 

313 token_generator=default_token_generator, 

314 from_email=None, 

315 request=None, 

316 html_email_template_name=None, 

317 extra_email_context=None, 

318 ): 

319 """ 

320 Generate a one-use only link for resetting password and send it to the 

321 user. 

322 """ 

323 email = self.cleaned_data["email"] 

324 if not domain_override: 

325 current_site = get_current_site(request) 

326 site_name = current_site.name 

327 domain = current_site.domain 

328 else: 

329 site_name = domain = domain_override 

330 email_field_name = UserModel.get_email_field_name() 

331 for user in self.get_users(email): 

332 user_email = getattr(user, email_field_name) 

333 context = { 

334 "email": user_email, 

335 "domain": domain, 

336 "site_name": site_name, 

337 "uid": urlsafe_base64_encode(force_bytes(user.pk)), 

338 "user": user, 

339 "token": token_generator.make_token(user), 

340 "protocol": "https" if use_https else "http", 

341 **(extra_email_context or {}), 

342 } 

343 self.send_mail( 

344 subject_template_name, 

345 email_template_name, 

346 context, 

347 from_email, 

348 user_email, 

349 html_email_template_name=html_email_template_name, 

350 ) 

351 

352 

353class SetPasswordForm(forms.Form): 

354 """ 

355 A form that lets a user change set their password without entering the old 

356 password 

357 """ 

358 

359 error_messages = { 

360 "password_mismatch": _("The two password fields didn’t match."), 

361 } 

362 new_password1 = forms.CharField( 

363 label=_("New password"), 

364 widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}), 

365 strip=False, 

366 help_text=password_validation.password_validators_help_text_html(), 

367 ) 

368 new_password2 = forms.CharField( 

369 label=_("New password confirmation"), 

370 strip=False, 

371 widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}), 

372 ) 

373 

374 def __init__(self, user, *args, **kwargs): 

375 self.user = user 

376 super().__init__(*args, **kwargs) 

377 

378 def clean_new_password2(self): 

379 password1 = self.cleaned_data.get("new_password1") 

380 password2 = self.cleaned_data.get("new_password2") 

381 if password1 and password2: 

382 if password1 != password2: 

383 raise ValidationError( 

384 self.error_messages["password_mismatch"], 

385 code="password_mismatch", 

386 ) 

387 password_validation.validate_password(password2, self.user) 

388 return password2 

389 

390 def save(self, commit=True): 

391 password = self.cleaned_data["new_password1"] 

392 self.user.set_password(password) 

393 if commit: 

394 self.user.save() 

395 return self.user 

396 

397 

398class PasswordChangeForm(SetPasswordForm): 

399 """ 

400 A form that lets a user change their password by entering their old 

401 password. 

402 """ 

403 

404 error_messages = { 

405 **SetPasswordForm.error_messages, 

406 "password_incorrect": _( 

407 "Your old password was entered incorrectly. Please enter it again." 

408 ), 

409 } 

410 old_password = forms.CharField( 

411 label=_("Old password"), 

412 strip=False, 

413 widget=forms.PasswordInput( 

414 attrs={"autocomplete": "current-password", "autofocus": True} 

415 ), 

416 ) 

417 

418 field_order = ["old_password", "new_password1", "new_password2"] 

419 

420 def clean_old_password(self): 

421 """ 

422 Validate that the old_password field is correct. 

423 """ 

424 old_password = self.cleaned_data["old_password"] 

425 if not self.user.check_password(old_password): 

426 raise ValidationError( 

427 self.error_messages["password_incorrect"], 

428 code="password_incorrect", 

429 ) 

430 return old_password 

431 

432 

433class AdminPasswordChangeForm(forms.Form): 

434 """ 

435 A form used to change the password of a user in the admin interface. 

436 """ 

437 

438 error_messages = { 

439 "password_mismatch": _("The two password fields didn’t match."), 

440 } 

441 required_css_class = "required" 

442 password1 = forms.CharField( 

443 label=_("Password"), 

444 widget=forms.PasswordInput( 

445 attrs={"autocomplete": "new-password", "autofocus": True} 

446 ), 

447 strip=False, 

448 help_text=password_validation.password_validators_help_text_html(), 

449 ) 

450 password2 = forms.CharField( 

451 label=_("Password (again)"), 

452 widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}), 

453 strip=False, 

454 help_text=_("Enter the same password as before, for verification."), 

455 ) 

456 

457 def __init__(self, user, *args, **kwargs): 

458 self.user = user 

459 super().__init__(*args, **kwargs) 

460 

461 def clean_password2(self): 

462 password1 = self.cleaned_data.get("password1") 

463 password2 = self.cleaned_data.get("password2") 

464 if password1 and password2 and password1 != password2: 

465 raise ValidationError( 

466 self.error_messages["password_mismatch"], 

467 code="password_mismatch", 

468 ) 

469 password_validation.validate_password(password2, self.user) 

470 return password2 

471 

472 def save(self, commit=True): 

473 """Save the new password.""" 

474 password = self.cleaned_data["password1"] 

475 self.user.set_password(password) 

476 if commit: 

477 self.user.save() 

478 return self.user 

479 

480 @property 

481 def changed_data(self): 

482 data = super().changed_data 

483 for name in self.fields: 

484 if name not in data: 

485 return [] 

486 return ["password"]