Coverage for ivatar/ivataraccount/views.py: 77%

699 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 11:51 +0000

1""" 

2View classes for ivatar/ivataraccount/ 

3""" 

4 

5from io import BytesIO 

6from ivatar.utils import urlopen, Bluesky 

7import base64 

8import binascii 

9import contextlib 

10from xml.sax import saxutils 

11import gzip 

12import logging 

13 

14from PIL import Image 

15 

16from django.db.models import ProtectedError 

17from django.core.exceptions import ObjectDoesNotExist 

18from django.contrib.auth.decorators import login_required 

19from django.contrib.auth.models import User 

20from django.utils.decorators import method_decorator 

21from django.contrib.messages.views import SuccessMessageMixin 

22from django.contrib import messages 

23from django.views.generic.edit import FormView, UpdateView 

24from django.views.generic.base import View, TemplateView 

25from django.views.generic.detail import DetailView 

26from django.contrib.auth import authenticate, login 

27from django.contrib.auth.forms import UserCreationForm, SetPasswordForm 

28from django.contrib.auth.views import LoginView 

29from django.contrib.auth.views import ( 

30 PasswordResetView as PasswordResetViewOriginal, 

31) 

32from django.utils.crypto import get_random_string 

33from django.utils.translation import gettext_lazy as _ 

34from django.http import HttpResponseRedirect, HttpResponse 

35from django.urls import reverse_lazy, reverse 

36from django.shortcuts import render 

37from django_openid_auth.models import UserOpenID 

38 

39from openid import oidutil 

40from openid.consumer import consumer 

41 

42from ipware import get_client_ip 

43 

44from email_validator import validate_email 

45 

46from libravatar import libravatar_url 

47from django.conf import settings 

48from .gravatar import get_photo as get_gravatar_photo 

49 

50# Access settings through django.conf.settings 

51MAX_NUM_PHOTOS = getattr(settings, "MAX_NUM_PHOTOS", 5) 

52MAX_PHOTO_SIZE = getattr(settings, "MAX_PHOTO_SIZE", 10485760) 

53JPEG_QUALITY = getattr(settings, "JPEG_QUALITY", 85) 

54AVATAR_MAX_SIZE = getattr(settings, "AVATAR_MAX_SIZE", 512) 

55SOCIAL_AUTH_FEDORA_KEY = getattr(settings, "SOCIAL_AUTH_FEDORA_KEY", None) 

56SITE_NAME = getattr(settings, "SITE_NAME", "libravatar") 

57IVATAR_VERSION = getattr(settings, "IVATAR_VERSION", "2.0") 

58MAX_NUM_UNCONFIRMED_EMAILS = getattr(settings, "MAX_NUM_UNCONFIRMED_EMAILS", 5) 

59SECURE_BASE_URL = getattr(settings, "SECURE_BASE_URL", "https://avatars.linux-kernel.at/avatar/") 

60BASE_URL = getattr(settings, "BASE_URL", "http://avatars.linux-kernel.at/avatar/") 

61IVATAR_MAILGUN_API_KEY = getattr(settings, "IVATAR_MAILGUN_API_KEY", None) 

62IVATAR_MAILGUN_SENDER_DOMAIN = getattr(settings, "IVATAR_MAILGUN_SENDER_DOMAIN", None) 

63 

64from .forms import AddEmailForm, UploadPhotoForm, AddOpenIDForm 

65from .forms import UpdatePreferenceForm, UploadLibravatarExportForm 

66from .forms import DeleteAccountForm 

67from .models import UnconfirmedEmail, ConfirmedEmail, Photo 

68from .models import UnconfirmedOpenId, ConfirmedOpenId, DjangoOpenIDStore 

69from .models import UserPreference 

70from .models import file_format 

71from .read_libravatar_export import read_gzdata as libravatar_read_gzdata 

72 

73# Initialize loggers 

74logger = logging.getLogger("ivatar") 

75security_logger = logging.getLogger("ivatar.security") 

76 

77# Import OpenTelemetry with graceful degradation 

78from ..telemetry_utils import ( 

79 trace_file_upload, 

80 trace_authentication, 

81 get_telemetry_metrics, 

82) 

83 

84avatar_metrics = get_telemetry_metrics() 

85 

86 

87def openid_logging(message, level=0): 

88 """ 

89 Helper method for openid logging 

90 """ 

91 # Normal messages are not that important 

92 # No need for coverage here 

93 if level > 0: # pragma: no cover 

94 logger.debug(message) 

95 

96 

97class CreateView(SuccessMessageMixin, FormView): 

98 """ 

99 View class for creating a new user 

100 """ 

101 

102 template_name = "new.html" 

103 form_class = UserCreationForm 

104 

105 @trace_authentication("user_registration") 

106 def form_valid(self, form): 

107 form.save() 

108 user = authenticate( 

109 username=form.cleaned_data["username"], 

110 password=form.cleaned_data["password1"], 

111 ) 

112 if user is not None: 

113 # If the username looks like a mail address, automagically 

114 # add it as unconfirmed mail and set it also as user's 

115 # email address 

116 with contextlib.suppress(Exception): 

117 self._extracted_from_form_valid_(form, user) 

118 login(self.request, user) 

119 pref = UserPreference.objects.create( 

120 user_id=user.pk 

121 ) # pylint: disable=no-member 

122 pref.save() 

123 return HttpResponseRedirect(reverse_lazy("profile")) 

124 return HttpResponseRedirect(reverse_lazy("login")) # pragma: no cover 

125 

126 def _extracted_from_form_valid_(self, form, user): 

127 # This will error out if it's not a valid address 

128 valid = validate_email(form.cleaned_data["username"]) 

129 user.email = valid.email 

130 user.save() 

131 # The following will also error out if it already exists 

132 unconfirmed = UnconfirmedEmail() 

133 unconfirmed.email = valid.email 

134 unconfirmed.user = user 

135 unconfirmed.save() 

136 unconfirmed.send_confirmation_mail( 

137 url=self.request.build_absolute_uri("/")[:-1] 

138 ) 

139 

140 def get(self, request, *args, **kwargs): 

141 """ 

142 Handle get for create view 

143 """ 

144 if request.user and request.user.is_authenticated: 

145 return HttpResponseRedirect(reverse_lazy("profile")) 

146 return super().get(self, request, args, kwargs) 

147 

148 

149@method_decorator(login_required, name="dispatch") 

150class PasswordSetView(SuccessMessageMixin, FormView): 

151 """ 

152 View class for changing the password 

153 """ 

154 

155 template_name = "password_change.html" 

156 form_class = SetPasswordForm 

157 success_message = _("password changed successfully - please login again") 

158 success_url = reverse_lazy("profile") 

159 

160 def get_form_kwargs(self): 

161 kwargs = super().get_form_kwargs() 

162 kwargs["user"] = self.request.user 

163 return kwargs 

164 

165 def form_valid(self, form): 

166 form.save() 

167 super().form_valid(form) 

168 return HttpResponseRedirect(reverse_lazy("login")) 

169 

170 

171@method_decorator(login_required, name="dispatch") 

172class AddEmailView(SuccessMessageMixin, FormView): 

173 """ 

174 View class for adding email addresses 

175 """ 

176 

177 template_name = "add_email.html" 

178 form_class = AddEmailForm 

179 success_url = reverse_lazy("profile") 

180 

181 def form_valid(self, form): 

182 if not form.save(self.request): 

183 return render(self.request, self.template_name, {"form": form}) 

184 

185 messages.success(self.request, _("Address added successfully")) 

186 return super().form_valid(form) 

187 

188 

189@method_decorator(login_required, name="dispatch") 

190class RemoveUnconfirmedEmailView(SuccessMessageMixin, View): 

191 """ 

192 View class for removing a unconfirmed email address 

193 """ 

194 

195 @staticmethod 

196 def post(request, *args, **kwargs): # pylint: disable=unused-argument 

197 """ 

198 Handle post request - removing unconfirmed email 

199 """ 

200 try: 

201 email = UnconfirmedEmail.objects.get( # pylint: disable=no-member 

202 user=request.user, id=kwargs["email_id"] 

203 ) 

204 email.delete() 

205 messages.success(request, _("Address removed")) 

206 except UnconfirmedEmail.DoesNotExist: # pylint: disable=no-member 

207 messages.error(request, _("Address does not exist")) 

208 return HttpResponseRedirect(reverse_lazy("profile")) 

209 

210 

211class ConfirmEmailView(SuccessMessageMixin, TemplateView): 

212 """ 

213 View class for confirming an unconfirmed email address 

214 """ 

215 

216 template_name = "email_confirmed.html" 

217 

218 def get(self, request, *args, **kwargs): 

219 # be tolerant of extra crap added by mail clients 

220 key = kwargs["verification_key"].replace(" ", "") 

221 

222 if len(key) != 64: 

223 messages.error(request, _("Verification key incorrect")) 

224 return HttpResponseRedirect(reverse_lazy("profile")) 

225 

226 try: 

227 unconfirmed = UnconfirmedEmail.objects.get( 

228 verification_key=key 

229 ) # pylint: disable=no-member 

230 except UnconfirmedEmail.DoesNotExist: # pylint: disable=no-member 

231 messages.error(request, _("Verification key does not exist")) 

232 return HttpResponseRedirect(reverse_lazy("profile")) 

233 

234 if ConfirmedEmail.objects.filter(email=unconfirmed.email).count() > 0: 

235 messages.error( 

236 request, 

237 _("This mail address has been taken already and cannot be confirmed"), 

238 ) 

239 return HttpResponseRedirect(reverse_lazy("profile")) 

240 

241 # TODO: Check for a reasonable expiration time in unconfirmed email 

242 

243 (confirmed_id, external_photos) = ConfirmedEmail.objects.create_confirmed_email( 

244 unconfirmed.user, unconfirmed.email, not request.user.is_anonymous 

245 ) 

246 

247 unconfirmed.delete() 

248 

249 # if there's a single image in this user's profile, 

250 # assign it to the new email 

251 confirmed = ConfirmedEmail.objects.get(id=confirmed_id) 

252 if confirmed.user.photo_set.count() == 1: 

253 confirmed.set_photo(confirmed.user.photo_set.first()) 

254 kwargs["photos"] = external_photos 

255 kwargs["email_id"] = confirmed_id 

256 return super().get(request, *args, **kwargs) 

257 

258 

259@method_decorator(login_required, name="dispatch") 

260class RemoveConfirmedEmailView(SuccessMessageMixin, View): 

261 """ 

262 View class for removing a confirmed email address 

263 """ 

264 

265 @staticmethod 

266 def post(request, *args, **kwargs): # pylint: disable=unused-argument 

267 """ 

268 Handle post request - removing confirmed email 

269 """ 

270 try: 

271 email = ConfirmedEmail.objects.get(user=request.user, id=kwargs["email_id"]) 

272 email.delete() 

273 messages.success(request, _("Address removed")) 

274 except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member 

275 messages.error(request, _("Address does not exist")) 

276 return HttpResponseRedirect(reverse_lazy("profile")) 

277 

278 

279@method_decorator(login_required, name="dispatch") 

280class AssignPhotoEmailView(SuccessMessageMixin, TemplateView): 

281 """ 

282 View class for assigning a photo to an email address 

283 """ 

284 

285 model = Photo 

286 template_name = "assign_photo_email.html" 

287 

288 def post(self, request, *args, **kwargs): # pylint: disable=unused-argument 

289 """ 

290 Handle post request - assign photo to email 

291 """ 

292 photo = None 

293 

294 try: 

295 email = ConfirmedEmail.objects.get(user=request.user, id=kwargs["email_id"]) 

296 except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member 

297 messages.error(request, _("Invalid request")) 

298 return HttpResponseRedirect(reverse_lazy("profile")) 

299 

300 if "photoNone" in request.POST: 

301 email.photo = None 

302 email.bluesky_handle = None 

303 elif "photoBluesky" in request.POST: 

304 # Keep the existing Bluesky handle, clear the photo 

305 email.photo = None 

306 # Don't clear bluesky_handle - keep it as is 

307 else: 

308 if "photo_id" not in request.POST: 

309 messages.error(request, _("Invalid request [photo_id] missing")) 

310 return HttpResponseRedirect(reverse_lazy("profile")) 

311 

312 if request.POST["photo_id"] == "bluesky": 

313 # Handle Bluesky photo selection 

314 email.photo = None 

315 # Don't clear bluesky_handle - keep it as is 

316 else: 

317 try: 

318 photo = self.model.objects.get( # pylint: disable=no-member 

319 id=request.POST["photo_id"], user=request.user 

320 ) 

321 except self.model.DoesNotExist: # pylint: disable=no-member 

322 messages.error(request, _("Photo does not exist")) 

323 return HttpResponseRedirect(reverse_lazy("profile")) 

324 email.photo = photo 

325 email.bluesky_handle = None 

326 email.save() 

327 

328 messages.success(request, _("Successfully changed photo")) 

329 return HttpResponseRedirect(reverse_lazy("profile")) 

330 

331 def get_context_data(self, **kwargs): 

332 data = super().get_context_data(**kwargs) 

333 data["email"] = ConfirmedEmail.objects.get(pk=kwargs["email_id"]) 

334 return data 

335 

336 

337@method_decorator(login_required, name="dispatch") 

338class AssignPhotoOpenIDView(SuccessMessageMixin, TemplateView): 

339 """ 

340 View class for assigning a photo to an openid address 

341 """ 

342 

343 model = Photo 

344 template_name = "assign_photo_openid.html" 

345 

346 def post(self, request, *args, **kwargs): # pylint: disable=unused-argument 

347 """ 

348 Handle post - assign photo to openid 

349 """ 

350 photo = None 

351 

352 try: 

353 openid = ConfirmedOpenId.objects.get( # pylint: disable=no-member 

354 user=request.user, id=kwargs["openid_id"] 

355 ) 

356 except ConfirmedOpenId.DoesNotExist: # pylint: disable=no-member 

357 messages.error(request, _("Invalid request")) 

358 return HttpResponseRedirect(reverse_lazy("profile")) 

359 

360 if "photoNone" in request.POST: 

361 openid.photo = None 

362 else: 

363 if "photo_id" not in request.POST: 

364 messages.error(request, _("Invalid request [photo_id] missing")) 

365 return HttpResponseRedirect(reverse_lazy("profile")) 

366 

367 try: 

368 photo = self.model.objects.get( # pylint: disable=no-member 

369 id=request.POST["photo_id"], user=request.user 

370 ) 

371 except self.model.DoesNotExist: # pylint: disable=no-member 

372 messages.error(request, _("Photo does not exist")) 

373 return HttpResponseRedirect(reverse_lazy("profile")) 

374 openid.photo = photo 

375 openid.bluesky_handle = None 

376 openid.save() 

377 

378 messages.success(request, _("Successfully changed photo")) 

379 return HttpResponseRedirect(reverse_lazy("profile")) 

380 

381 def get_context_data(self, **kwargs): 

382 data = super().get_context_data(**kwargs) 

383 data["openid"] = ConfirmedOpenId.objects.get( 

384 pk=kwargs["openid_id"] 

385 ) # pylint: disable=no-member 

386 return data 

387 

388 

389@method_decorator(login_required, name="dispatch") 

390class AssignBlueskyHandleToEmailView(SuccessMessageMixin, TemplateView): 

391 """ 

392 View class for assigning a Bluesky handle to an email address 

393 """ 

394 

395 def post(self, request, *args, **kwargs): # pylint: disable=unused-argument 

396 """ 

397 Handle post request - assign bluesky handle to email 

398 """ 

399 

400 try: 

401 email = ConfirmedEmail.objects.get(user=request.user, id=kwargs["email_id"]) 

402 except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member 

403 messages.error(request, _("Invalid request")) 

404 return HttpResponseRedirect(reverse_lazy("profile")) 

405 

406 if "bluesky_handle" not in request.POST: 

407 messages.error(request, _("Invalid request [bluesky_handle] missing")) 

408 return HttpResponseRedirect(reverse_lazy("profile")) 

409 bluesky_handle = request.POST["bluesky_handle"] 

410 

411 try: 

412 bs = Bluesky() 

413 

414 bs.get_avatar(bluesky_handle) 

415 except Exception as e: 

416 messages.error(request, _(f"Handle '{bluesky_handle}' not found: {e}")) 

417 return HttpResponseRedirect( 

418 reverse_lazy( 

419 "assign_photo_email", kwargs={"email_id": int(kwargs["email_id"])} 

420 ) 

421 ) 

422 try: 

423 email.set_bluesky_handle(bluesky_handle) 

424 except Exception as e: 

425 messages.error(request, _(f"Error: {e}")) 

426 return HttpResponseRedirect( 

427 reverse_lazy( 

428 "assign_photo_email", kwargs={"email_id": int(kwargs["email_id"])} 

429 ) 

430 ) 

431 email.photo = None 

432 email.save() 

433 

434 messages.success(request, _("Successfully assigned Bluesky handle")) 

435 return HttpResponseRedirect(reverse_lazy("profile")) 

436 

437 def get_context_data(self, **kwargs): 

438 data = super().get_context_data(**kwargs) 

439 data["email"] = ConfirmedEmail.objects.get(pk=kwargs["email_id"]) 

440 return data 

441 

442 

443@method_decorator(login_required, name="dispatch") 

444class AssignBlueskyHandleToOpenIdView(SuccessMessageMixin, TemplateView): 

445 """ 

446 View class for assigning a Bluesky handle to an email address 

447 """ 

448 

449 def post(self, request, *args, **kwargs): # pylint: disable=unused-argument 

450 """ 

451 Handle post request - assign bluesky handle to email 

452 """ 

453 

454 try: 

455 openid = ConfirmedOpenId.objects.get( 

456 user=request.user, id=kwargs["open_id"] 

457 ) 

458 except ConfirmedOpenId.DoesNotExist: # pylint: disable=no-member 

459 messages.error(request, _("Invalid request")) 

460 return HttpResponseRedirect(reverse_lazy("profile")) 

461 

462 if "bluesky_handle" not in request.POST: 

463 messages.error(request, _("Invalid request [bluesky_handle] missing")) 

464 return HttpResponseRedirect(reverse_lazy("profile")) 

465 bluesky_handle = request.POST["bluesky_handle"] 

466 

467 try: 

468 bs = Bluesky() 

469 

470 bs.get_avatar(bluesky_handle) 

471 except Exception as e: 

472 messages.error(request, _(f"Handle '{bluesky_handle}' not found: {e}")) 

473 return HttpResponseRedirect( 

474 reverse_lazy( 

475 "assign_photo_openid", kwargs={"openid_id": int(kwargs["open_id"])} 

476 ) 

477 ) 

478 try: 

479 openid.set_bluesky_handle(bluesky_handle) 

480 except Exception as e: 

481 messages.error(request, _(f"Error: {e}")) 

482 return HttpResponseRedirect( 

483 reverse_lazy( 

484 "assign_photo_openid", kwargs={"openid_id": int(kwargs["open_id"])} 

485 ) 

486 ) 

487 openid.photo = None 

488 openid.save() 

489 

490 messages.success(request, _("Successfully assigned Bluesky handle")) 

491 return HttpResponseRedirect(reverse_lazy("profile")) 

492 

493 def get_context_data(self, **kwargs): 

494 data = super().get_context_data(**kwargs) 

495 data["openid"] = ConfirmedOpenId.objects.get(pk=kwargs["open_id"]) 

496 return data 

497 

498 

499@method_decorator(login_required, name="dispatch") 

500class ImportPhotoView(SuccessMessageMixin, TemplateView): 

501 """ 

502 View class to import a photo from another service 

503 Currently only Gravatar is supported 

504 """ 

505 

506 template_name = "import_photo.html" 

507 

508 def get_context_data(self, **kwargs): 

509 context = super().get_context_data(**kwargs) 

510 context["photos"] = [] 

511 addr = None 

512 if "email_id" in kwargs: 

513 try: 

514 addr = ConfirmedEmail.objects.get(pk=kwargs["email_id"]).email 

515 except ConfirmedEmail.ObjectDoesNotExist: # pylint: disable=no-member 

516 messages.error(self.request, _("Address does not exist")) 

517 return context 

518 

519 if addr := kwargs.get("email_addr", None): 

520 if gravatar := get_gravatar_photo(addr): 

521 context["photos"].append(gravatar) 

522 

523 if libravatar_service_url := libravatar_url( 

524 email=addr, 

525 default=404, 

526 size=AVATAR_MAX_SIZE, 

527 ): 

528 try: 

529 urlopen(libravatar_service_url) 

530 except OSError as exc: 

531 logger.warning(f"Exception caught during photo import: {exc}") 

532 else: 

533 context["photos"].append( 

534 { 

535 "service_url": libravatar_service_url, 

536 "thumbnail_url": f"{libravatar_service_url}&s=80", 

537 "image_url": f"{libravatar_service_url}&s=512", 

538 "width": 80, 

539 "height": 80, 

540 "service_name": "Libravatar", 

541 } 

542 ) 

543 

544 return context 

545 

546 def post( 

547 self, request, *args, **kwargs 

548 ): # pylint: disable=no-self-use,unused-argument,too-many-branches,line-too-long 

549 """ 

550 Handle post to photo import 

551 """ 

552 

553 imported = None 

554 

555 email_id = kwargs.get("email_id", request.POST.get("email_id", None)) 

556 addr = kwargs.get("email", request.POST.get("email_addr", None)) 

557 

558 if email_id: 

559 email = ConfirmedEmail.objects.filter(id=email_id, user=request.user) 

560 if email.exists(): 

561 addr = email.first().email 

562 else: 

563 messages.error(request, _("Address does not exist")) 

564 return HttpResponseRedirect(reverse_lazy("profile")) 

565 

566 if "photo_Gravatar" in request.POST: 

567 photo = Photo() 

568 photo.user = request.user 

569 photo.ip_address = get_client_ip(request)[0] 

570 if photo.import_image("Gravatar", addr): 

571 messages.success(request, _("Gravatar image successfully imported")) 

572 else: 

573 # Honestly, I'm not sure how to test this... 

574 messages.error( 

575 request, _("Gravatar image import not successful") 

576 ) # pragma: no cover 

577 imported = True 

578 

579 if "photo_Libravatar" in request.POST: 

580 photo = Photo() 

581 photo.user = request.user 

582 photo.ip_address = get_client_ip(request)[0] 

583 if photo.import_image("Libravatar", addr): 

584 messages.success(request, _("Libravatar image successfully imported")) 

585 else: 

586 # Honestly, I'm not sure how to test this... 

587 messages.error( 

588 request, _("Libravatar image import not successful") 

589 ) # pragma: no cover 

590 imported = True 

591 if not imported: 

592 messages.warning(request, _("Nothing importable")) 

593 return HttpResponseRedirect(reverse_lazy("profile")) 

594 

595 

596@method_decorator(login_required, name="dispatch") 

597class RawImageView(DetailView): 

598 """ 

599 View to return (binary) raw image data, for use in <img/>-tags 

600 """ 

601 

602 model = Photo 

603 

604 def get(self, request, *args, **kwargs): 

605 photo = self.model.objects.get(pk=kwargs["pk"]) # pylint: disable=no-member 

606 if photo.user.id != request.user.id and not request.user.is_staff: 

607 return HttpResponseRedirect(reverse_lazy("home")) 

608 return HttpResponse(BytesIO(photo.data), content_type=f"image/{photo.format}") 

609 

610 

611@method_decorator(login_required, name="dispatch") 

612class DeletePhotoView(SuccessMessageMixin, View): 

613 """ 

614 View class for deleting a photo 

615 """ 

616 

617 model = Photo 

618 

619 def get(self, request, *args, **kwargs): # pylint: disable=unused-argument 

620 """ 

621 Handle get - delete photo 

622 """ 

623 try: 

624 photo = self.model.objects.get( # pylint: disable=no-member 

625 pk=kwargs["pk"], user=request.user 

626 ) 

627 photo.delete() 

628 except (self.model.DoesNotExist, ProtectedError): # pylint: disable=no-member 

629 messages.error(request, _("No such image or no permission to delete it")) 

630 return HttpResponseRedirect(reverse_lazy("profile")) 

631 messages.success(request, _("Photo deleted successfully")) 

632 return HttpResponseRedirect(reverse_lazy("profile")) 

633 

634 

635@method_decorator(login_required, name="dispatch") 

636class UploadPhotoView(SuccessMessageMixin, FormView): 

637 """ 

638 View class responsible for photo upload with enhanced security 

639 """ 

640 

641 model = Photo 

642 template_name = "upload_photo.html" 

643 form_class = UploadPhotoForm 

644 success_message = _("Successfully uploaded") 

645 success_url = reverse_lazy("profile") 

646 

647 def post(self, request, *args, **kwargs): 

648 # Check maximum number of photos 

649 num_photos = request.user.photo_set.count() 

650 if num_photos >= MAX_NUM_PHOTOS: 

651 messages.error( 

652 request, _("Maximum number of photos (%i) reached" % MAX_NUM_PHOTOS) 

653 ) 

654 return HttpResponseRedirect(reverse_lazy("profile")) 

655 

656 return super().post(request, *args, **kwargs) 

657 

658 @trace_file_upload("photo_upload") 

659 def form_valid(self, form): 

660 photo_data = self.request.FILES["photo"] 

661 

662 # Additional size check (redundant but good for security) 

663 if photo_data.size > MAX_PHOTO_SIZE: 

664 messages.error(self.request, _("Image too big")) 

665 avatar_metrics.record_file_upload( 

666 file_size=photo_data.size, 

667 content_type=photo_data.content_type, 

668 success=False, 

669 ) 

670 return HttpResponseRedirect(reverse_lazy("profile")) 

671 

672 # Enhanced security logging 

673 security_logger.info( 

674 f"Photo upload attempt by user {self.request.user.id} " 

675 f"from IP {get_client_ip(self.request)[0]}, " 

676 f"file size: {photo_data.size} bytes" 

677 ) 

678 

679 photo = form.save(self.request, photo_data) 

680 

681 if not photo: 

682 security_logger.warning( 

683 f"Photo upload failed for user {self.request.user.id} - invalid format" 

684 ) 

685 messages.error(self.request, _("Invalid Format")) 

686 avatar_metrics.record_file_upload( 

687 file_size=photo_data.size, 

688 content_type=photo_data.content_type, 

689 success=False, 

690 ) 

691 return HttpResponseRedirect(reverse_lazy("profile")) 

692 

693 # Log successful upload 

694 security_logger.info( 

695 f"Photo uploaded successfully by user {self.request.user.id}, " 

696 f"photo ID: {photo.pk}" 

697 ) 

698 

699 # Record successful file upload metrics 

700 avatar_metrics.record_file_upload( 

701 file_size=photo_data.size, 

702 content_type=photo_data.content_type, 

703 success=True, 

704 ) 

705 

706 # Override success URL -> Redirect to crop page. 

707 self.success_url = reverse_lazy("crop_photo", args=[photo.pk]) 

708 return super().form_valid(form) 

709 

710 

711@method_decorator(login_required, name="dispatch") 

712class AddOpenIDView(SuccessMessageMixin, FormView): 

713 """ 

714 View class for adding OpenID 

715 """ 

716 

717 template_name = "add_openid.html" 

718 form_class = AddOpenIDForm 

719 success_url = reverse_lazy("profile") 

720 

721 def form_valid(self, form): 

722 if openid_id := form.save(self.request.user): 

723 # At this point we have an unconfirmed OpenID, but 

724 # we do not add the message, that we successfully added it, 

725 # since this is misleading 

726 return HttpResponseRedirect( 

727 reverse_lazy("openid_redirection", args=[openid_id]) 

728 ) 

729 else: 

730 return render(self.request, self.template_name, {"form": form}) 

731 

732 

733@method_decorator(login_required, name="dispatch") 

734class RemoveUnconfirmedOpenIDView(View): 

735 """ 

736 View class for removing a unconfirmed OpenID 

737 """ 

738 

739 model = UnconfirmedOpenId 

740 

741 def post(self, request, *args, **kwargs): # pylint: disable=unused-argument 

742 """ 

743 Handle post - remove unconfirmed openid 

744 """ 

745 try: 

746 openid = self.model.objects.get( # pylint: disable=no-member 

747 user=request.user, id=kwargs["openid_id"] 

748 ) 

749 openid.delete() 

750 messages.success(request, _("ID removed")) 

751 except ( 

752 self.model.DoesNotExist 

753 ): # pragma: no cover pylint: disable=no-member,line-too-long 

754 messages.error(request, _("ID does not exist")) 

755 return HttpResponseRedirect(reverse_lazy("profile")) 

756 

757 

758@method_decorator(login_required, name="dispatch") 

759class RemoveConfirmedOpenIDView(View): 

760 """ 

761 View class for removing a confirmed OpenID 

762 """ 

763 

764 model = ConfirmedOpenId 

765 

766 def post(self, request, *args, **kwargs): # pylint: disable=unused-argument 

767 """ 

768 Handle post - remove confirmed openid 

769 """ 

770 try: 

771 openid = self.model.objects.get( # pylint: disable=no-member 

772 user=request.user, id=kwargs["openid_id"] 

773 ) 

774 try: 

775 openidobj = ( 

776 UserOpenID.objects.get( # pylint: disable=no-member,line-too-long 

777 user_id=request.user.id, claimed_id=openid.openid 

778 ) 

779 ) 

780 openidobj.delete() 

781 except Exception as exc: # pylint: disable=broad-except 

782 # Why it is not there? 

783 logger.warning(f"How did we get here: {exc}") 

784 openid.delete() 

785 messages.success(request, _("ID removed")) 

786 except self.model.DoesNotExist: # pylint: disable=no-member 

787 messages.error(request, _("ID does not exist")) 

788 return HttpResponseRedirect(reverse_lazy("profile")) 

789 

790 

791@method_decorator(login_required, name="dispatch") 

792class RedirectOpenIDView(View): 

793 """ 

794 Redirect view for OpenID 

795 """ 

796 

797 model = UnconfirmedOpenId 

798 

799 def get(self, request, *args, **kwargs): # pylint: disable=unused-argument 

800 """ 

801 Handle get for OpenID redirect view 

802 """ 

803 try: 

804 unconfirmed = self.model.objects.get( # pylint: disable=no-member 

805 user=request.user, id=kwargs["openid_id"] 

806 ) 

807 except ( 

808 self.model.DoesNotExist 

809 ): # pragma: no cover pylint: disable=no-member,line-too-long 

810 messages.error(request, _("ID does not exist")) 

811 return HttpResponseRedirect(reverse_lazy("profile")) 

812 

813 user_url = unconfirmed.openid 

814 session = {"id": request.session.session_key} 

815 

816 oidutil.log = openid_logging 

817 openid_consumer = consumer.Consumer(session, DjangoOpenIDStore()) 

818 

819 try: 

820 auth_request = openid_consumer.begin(user_url) 

821 except consumer.DiscoveryFailure as exc: 

822 messages.error(request, _(f"OpenID discovery failed: {exc}")) 

823 return HttpResponseRedirect(reverse_lazy("profile")) 

824 except UnicodeDecodeError as exc: # pragma: no cover 

825 msg = _( 

826 "OpenID discovery failed (userid=%(userid)s) for " 

827 "%(userurl)s: %(message)s" 

828 % { 

829 "userid": request.user.id, 

830 "userurl": user_url.encode("utf-8"), 

831 "message": exc, 

832 } 

833 ) 

834 logger.error(f"message: {msg}") 

835 messages.error(request, msg) 

836 

837 if auth_request is None: # pragma: no cover 

838 messages.error(request, _("OpenID discovery failed")) 

839 return HttpResponseRedirect(reverse_lazy("profile")) 

840 

841 realm = request.build_absolute_uri("/")[:-1] # pragma: no cover 

842 return_url = realm + reverse( # pragma: no cover 

843 "confirm_openid", args=[kwargs["openid_id"]] 

844 ) 

845 return HttpResponseRedirect( # pragma: no cover 

846 auth_request.redirectURL(realm, return_url) 

847 ) 

848 

849 

850@method_decorator(login_required, name="dispatch") 

851class ConfirmOpenIDView(View): # pragma: no cover 

852 """ 

853 Confirm OpenID view 

854 """ 

855 

856 model = UnconfirmedOpenId 

857 model_confirmed = ConfirmedOpenId 

858 

859 def do_request(self, data, *args, **kwargs): # pylint: disable=unused-argument 

860 """ 

861 Handle request, called by get() or post() 

862 """ 

863 session = {"id": self.request.session.session_key} 

864 current_url = self.request.build_absolute_uri("/")[:-1] + self.request.path 

865 openid_consumer = consumer.Consumer(session, DjangoOpenIDStore()) 

866 info = openid_consumer.complete(data, current_url) 

867 if info.status == consumer.FAILURE: 

868 messages.error( 

869 self.request, _('Confirmation failed: "') + str(info.message) + '"' 

870 ) 

871 return HttpResponseRedirect(reverse_lazy("profile")) 

872 

873 if info.status == consumer.CANCEL: 

874 messages.error(self.request, _("Cancelled by user")) 

875 return HttpResponseRedirect(reverse_lazy("profile")) 

876 

877 if info.status != consumer.SUCCESS: 

878 messages.error(self.request, _("Unknown verification error")) 

879 return HttpResponseRedirect(reverse_lazy("profile")) 

880 

881 try: 

882 unconfirmed = self.model.objects.get( # pylint: disable=no-member 

883 user=self.request.user, id=kwargs["openid_id"] 

884 ) 

885 except self.model.DoesNotExist: # pylint: disable=no-member 

886 messages.error(self.request, _("ID does not exist")) 

887 return HttpResponseRedirect(reverse_lazy("profile")) 

888 

889 # TODO: Check for a reasonable expiration time 

890 confirmed = self.model_confirmed() 

891 confirmed.user = unconfirmed.user 

892 confirmed.ip_address = get_client_ip(self.request)[0] 

893 confirmed.openid = unconfirmed.openid 

894 confirmed.save() 

895 

896 unconfirmed.delete() 

897 

898 # If there is a single image in this user's profile 

899 # assign it to the new id 

900 if self.request.user.photo_set.count() == 1: 

901 confirmed.set_photo(self.request.user.photo_set.first()) 

902 

903 # Also allow user to login using this OpenID (if not already taken) 

904 if not UserOpenID.objects.filter( # pylint: disable=no-member 

905 claimed_id=confirmed.openid 

906 ).exists(): 

907 user_openid = UserOpenID() 

908 user_openid.user = self.request.user 

909 user_openid.claimed_id = confirmed.openid 

910 user_openid.display_id = confirmed.openid 

911 user_openid.save() 

912 return HttpResponseRedirect(reverse_lazy("profile")) 

913 

914 def get(self, request, *args, **kwargs): 

915 """ 

916 Handle get - confirm openid 

917 """ 

918 return self.do_request(request.GET, *args, **kwargs) 

919 

920 def post(self, request, *args, **kwargs): 

921 """ 

922 Handle post - confirm openid 

923 """ 

924 return self.do_request(request.POST, *args, **kwargs) 

925 

926 

927@method_decorator(login_required, name="dispatch") 

928class CropPhotoView(TemplateView): 

929 """ 

930 View class for cropping photos 

931 """ 

932 

933 template_name = "crop_photo.html" 

934 success_url = reverse_lazy("profile") 

935 model = Photo 

936 

937 def get(self, request, *args, **kwargs): 

938 photo = self.model.objects.get( 

939 pk=kwargs["pk"], user=request.user 

940 ) # pylint: disable=no-member 

941 email = request.GET.get("email") 

942 openid = request.GET.get("openid") 

943 return render( 

944 self.request, 

945 self.template_name, 

946 { 

947 "photo": photo, 

948 "email": email, 

949 "openid": openid, 

950 }, 

951 ) 

952 

953 def post(self, request, *args, **kwargs): # pylint: disable=unused-argument 

954 """ 

955 Handle post - crop photo 

956 """ 

957 photo = self.model.objects.get( 

958 pk=kwargs["pk"], user=request.user 

959 ) # pylint: disable=no-member 

960 dimensions = { 

961 "x": int(float(request.POST["x"])), 

962 "y": int(float(request.POST["y"])), 

963 "w": int(float(request.POST["w"])), 

964 "h": int(float(request.POST["h"])), 

965 } 

966 email = openid = None 

967 if "email" in request.POST: 

968 with contextlib.suppress(ConfirmedEmail.DoesNotExist): 

969 email = ConfirmedEmail.objects.get(email=request.POST["email"]) 

970 if "openid" in request.POST: 

971 with contextlib.suppress(ConfirmedOpenId.DoesNotExist): 

972 openid = ConfirmedOpenId.objects.get( # pylint: disable=no-member 

973 openid=request.POST["openid"] 

974 ) 

975 return photo.perform_crop(request, dimensions, email, openid) 

976 

977 

978@method_decorator(login_required, name="dispatch") # pylint: disable=too-many-ancestors 

979class UserPreferenceView(FormView, UpdateView): 

980 """ 

981 View class for user preferences view/update 

982 """ 

983 

984 template_name = "preferences.html" 

985 model = UserPreference 

986 form_class = UpdatePreferenceForm 

987 success_url = reverse_lazy("user_preference") 

988 

989 def post(self, request, *args, **kwargs): # pylint: disable=unused-argument 

990 """ 

991 Process POST-ed data from this form 

992 """ 

993 userpref = None 

994 try: 

995 userpref = self.request.user.userpreference 

996 except ObjectDoesNotExist: 

997 userpref = UserPreference(user=self.request.user) 

998 userpref.theme = request.POST["theme"] 

999 userpref.save() 

1000 try: 

1001 if request.POST["email"] != self.request.user.email: 

1002 addresses = list( 

1003 self.request.user.confirmedemail_set.all().values_list( 

1004 "email", flat=True 

1005 ) 

1006 ) 

1007 if request.POST["email"] not in addresses: 

1008 messages.error( 

1009 self.request, 

1010 _(f'Mail address not allowed: {request.POST["email"]}'), 

1011 ) 

1012 else: 

1013 self.request.user.email = request.POST["email"] 

1014 self.request.user.save() 

1015 messages.info(self.request, _("Mail address changed.")) 

1016 except Exception as e: # pylint: disable=broad-except 

1017 messages.error(self.request, _(f"Error setting new mail address: {e}")) 

1018 

1019 try: 

1020 if request.POST["first_name"] or request.POST["last_name"]: 

1021 if request.POST["first_name"] != self.request.user.first_name: 

1022 self.request.user.first_name = request.POST["first_name"] 

1023 messages.info(self.request, _("First name changed.")) 

1024 if request.POST["last_name"] != self.request.user.last_name: 

1025 self.request.user.last_name = request.POST["last_name"] 

1026 messages.info(self.request, _("Last name changed.")) 

1027 self.request.user.save() 

1028 except Exception as e: # pylint: disable=broad-except 

1029 messages.error(self.request, _(f"Error setting names: {e}")) 

1030 

1031 return HttpResponseRedirect(reverse_lazy("user_preference")) 

1032 

1033 def get(self, request, *args, **kwargs): 

1034 return render( 

1035 self.request, 

1036 self.template_name, 

1037 { 

1038 "THEMES": UserPreference.THEMES, 

1039 }, 

1040 ) 

1041 

1042 def get_object(self, queryset=None): 

1043 (obj, created) = UserPreference.objects.get_or_create( 

1044 user=self.request.user 

1045 ) # pylint: disable=no-member,unused-variable 

1046 return obj 

1047 

1048 

1049@method_decorator(login_required, name="dispatch") 

1050class UploadLibravatarExportView(SuccessMessageMixin, FormView): 

1051 """ 

1052 View class responsible for libravatar user data export upload 

1053 """ 

1054 

1055 template_name = "upload_libravatar_export.html" 

1056 form_class = UploadLibravatarExportForm 

1057 success_message = _("Successfully uploaded") 

1058 success_url = reverse_lazy("profile") 

1059 model = User 

1060 

1061 def post(self, request, *args, **kwargs): # pylint: disable=unused-argument 

1062 """ 

1063 Handle post request - choose items to import 

1064 """ 

1065 if "save" in kwargs: # pylint: disable=too-many-nested-blocks 

1066 if kwargs["save"] == "save": 

1067 for arg in request.POST: 

1068 if arg.startswith("email_"): 

1069 email = request.POST[arg] 

1070 if not ConfirmedEmail.objects.filter( 

1071 email=email 

1072 ) and not UnconfirmedEmail.objects.filter( 

1073 email=email 

1074 ): # pylint: disable=no-member 

1075 try: 

1076 unconfirmed = UnconfirmedEmail.objects.create( # pylint: disable=no-member 

1077 user=request.user, email=email 

1078 ) 

1079 unconfirmed.save() 

1080 unconfirmed.send_confirmation_mail( 

1081 url=request.build_absolute_uri("/")[:-1] 

1082 ) 

1083 messages.info( 

1084 request, 

1085 "%s: %s" 

1086 % ( 

1087 email, 

1088 _( 

1089 "address added successfully,\ 

1090 confirmation mail sent" 

1091 ), 

1092 ), 

1093 ) 

1094 except Exception as exc: # pylint: disable=broad-except 

1095 # DEBUG 

1096 print( 

1097 f"Exception during adding mail address ({email}): {exc}" 

1098 ) 

1099 

1100 if arg.startswith("photo"): 

1101 try: 

1102 data = base64.decodebytes(bytes(request.POST[arg], "utf-8")) 

1103 except binascii.Error as exc: 

1104 logger.warning(f"Cannot decode photo: {exc}") 

1105 continue 

1106 try: 

1107 pilobj = Image.open(BytesIO(data)) 

1108 out = BytesIO() 

1109 pilobj.save(out, pilobj.format, quality=JPEG_QUALITY) 

1110 out.seek(0) 

1111 photo = Photo() 

1112 photo.user = request.user 

1113 photo.ip_address = get_client_ip(request)[0] 

1114 photo.format = file_format(pilobj.format) 

1115 photo.data = out.read() 

1116 photo.save() 

1117 except Exception as exc: # pylint: disable=broad-except 

1118 logger.error(f"Exception during save: {exc}") 

1119 continue 

1120 

1121 return HttpResponseRedirect(reverse_lazy("profile")) 

1122 return super().post(request, args, kwargs) 

1123 

1124 def form_valid(self, form): 

1125 data = self.request.FILES["export_file"] 

1126 try: 

1127 items = libravatar_read_gzdata(data.read()) 

1128 # DEBUG print(items) 

1129 return render( 

1130 self.request, 

1131 "choose_libravatar_export.html", 

1132 { 

1133 "emails": items["emails"], 

1134 "photos": items["photos"], 

1135 }, 

1136 ) 

1137 except Exception as e: 

1138 messages.error(self.request, _(f"Unable to parse file: {e}")) 

1139 return HttpResponseRedirect(reverse_lazy("upload_export")) 

1140 

1141 

1142@method_decorator(login_required, name="dispatch") 

1143class ResendConfirmationMailView(View): 

1144 """ 

1145 View class for resending confirmation mail 

1146 """ 

1147 

1148 model = UnconfirmedEmail 

1149 

1150 def get(self, request, *args, **kwargs): # pylint: disable=unused-argument 

1151 """ 

1152 Handle post - resend confirmation mail for unconfirmed e-mail address 

1153 """ 

1154 try: 

1155 email = self.model.objects.get( # pylint: disable=no-member 

1156 user=request.user, id=kwargs["email_id"] 

1157 ) 

1158 except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member 

1159 messages.error(request, _("ID does not exist")) 

1160 else: 

1161 try: 

1162 email.send_confirmation_mail(url=request.build_absolute_uri("/")[:-1]) 

1163 messages.success( 

1164 request, f'{_("Confirmation mail sent to")}: {email.email}' 

1165 ) 

1166 except Exception as exc: # pylint: disable=broad-except 

1167 messages.error( 

1168 request, 

1169 f'{_("Unable to send confirmation email for")} {email.email}: {exc}', 

1170 ) 

1171 return HttpResponseRedirect(reverse_lazy("profile")) 

1172 

1173 

1174class IvatarLoginView(LoginView): 

1175 """ 

1176 View class for login 

1177 """ 

1178 

1179 template_name = "login.html" 

1180 

1181 @trace_authentication("login_attempt") 

1182 def get(self, request, *args, **kwargs): 

1183 """ 

1184 Handle get for login view 

1185 """ 

1186 if request.user: 

1187 if request.user.is_authenticated: 

1188 # Respect the 'next' parameter if present 

1189 next_url = request.GET.get("next") 

1190 if next_url: 

1191 return HttpResponseRedirect(next_url) 

1192 return HttpResponseRedirect(reverse_lazy("profile")) 

1193 return super().get(self, request, args, kwargs) 

1194 

1195 @trace_authentication("login_post") 

1196 def post(self, request, *args, **kwargs): 

1197 """ 

1198 Handle login form submission 

1199 """ 

1200 return super().post(request, *args, **kwargs) 

1201 

1202 def get_context_data(self, **kwargs): 

1203 context = super().get_context_data(**kwargs) 

1204 context["with_fedora"] = SOCIAL_AUTH_FEDORA_KEY is not None 

1205 return context 

1206 

1207 

1208@method_decorator(login_required, name="dispatch") 

1209class ProfileView(TemplateView): 

1210 """ 

1211 View class for profile 

1212 """ 

1213 

1214 template_name = "profile.html" 

1215 

1216 def get(self, request, *args, **kwargs): 

1217 if "profile_username" in kwargs: 

1218 if not request.user.is_staff: 

1219 return HttpResponseRedirect(reverse_lazy("profile")) 

1220 with contextlib.suppress(Exception): 

1221 u = User.objects.get(username=kwargs["profile_username"]) 

1222 request.user = u 

1223 self._confirm_claimed_openid() 

1224 return super().get(self, request, args, kwargs) 

1225 

1226 def get_context_data(self, **kwargs): 

1227 """ 

1228 Provide additional context data, like if max_photos is reached 

1229 already or not. 

1230 """ 

1231 context = super().get_context_data(**kwargs) 

1232 context["max_photos"] = False 

1233 if self.request.user: 

1234 if self.request.user.photo_set.all().count() >= MAX_NUM_PHOTOS: 

1235 context["max_photos"] = True 

1236 return context 

1237 

1238 def _confirm_claimed_openid(self): 

1239 openids = self.request.user.useropenid_set.all() 

1240 # If there is only one OpenID, we eventually need to add it to 

1241 # the user account 

1242 if openids.count() == 1: 

1243 # Already confirmed, skip 

1244 if ConfirmedOpenId.objects.filter( # pylint: disable=no-member 

1245 openid=openids.first().claimed_id 

1246 ).exists(): 

1247 return 

1248 # For whatever reason, this is in unconfirmed state, skip 

1249 if UnconfirmedOpenId.objects.filter( # pylint: disable=no-member 

1250 openid=openids.first().claimed_id 

1251 ).exists(): 

1252 return 

1253 logger.debug(f"need to confirm: {openids.first()}") 

1254 confirmed = ConfirmedOpenId() 

1255 confirmed.user = self.request.user 

1256 confirmed.ip_address = get_client_ip(self.request)[0] 

1257 confirmed.openid = openids.first().claimed_id 

1258 confirmed.save() 

1259 

1260 

1261class PasswordResetView(PasswordResetViewOriginal): 

1262 """ 

1263 View class for password reset 

1264 """ 

1265 

1266 def post(self, request, *args, **kwargs): 

1267 """ 

1268 Since we have the mail addresses in ConfirmedEmail model, 

1269 we need to set the email on the user object in order for the 

1270 PasswordResetView class to pick up the correct user. 

1271 In case we have the mail address in the User object, we still 

1272 need to assign a random password in order for PasswordResetView 

1273 class to pick up the user - else it will silently do nothing. 

1274 """ 

1275 if "email" in request.POST: 

1276 user = None 

1277 

1278 # Try to find the user via the normal user class 

1279 # TODO: How to handle the case that multiple user accounts 

1280 # could have the same password set? 

1281 user = User.objects.filter(email=request.POST["email"]).first() 

1282 

1283 # If we didn't find the user in the previous step, 

1284 # try the ConfirmedEmail class instead. 

1285 # If we find the user there, we need to set the mail 

1286 # attribute on the user object accordingly 

1287 if not user: 

1288 with contextlib.suppress(ObjectDoesNotExist): 

1289 confirmed_email = ConfirmedEmail.objects.get( 

1290 email=request.POST["email"] 

1291 ) 

1292 user = confirmed_email.user 

1293 user.email = confirmed_email.email 

1294 user.save() 

1295 # If we found the user, set a random password. Else, the 

1296 # ResetPasswordView class will silently ignore the password 

1297 # reset request 

1298 if user: 

1299 if not user.password or user.password.startswith("!"): 

1300 random_pass = get_random_string(12) 

1301 user.set_password(random_pass) 

1302 user.save() 

1303 

1304 # Whatever happens above, let the original function handle the rest 

1305 return super().post(self, request, args, kwargs) 

1306 

1307 

1308@method_decorator(login_required, name="dispatch") 

1309class DeleteAccountView(SuccessMessageMixin, FormView): 

1310 """ 

1311 View class for account deletion 

1312 """ 

1313 

1314 template_name = "delete.html" 

1315 form_class = DeleteAccountForm 

1316 success_url = reverse_lazy("home") 

1317 

1318 def get(self, request, *args, **kwargs): 

1319 return super().get(self, request, args, kwargs) 

1320 

1321 def post(self, request, *args, **kwargs): 

1322 """ 

1323 Handle account deletion 

1324 """ 

1325 if request.user.password: 

1326 if "password" in request.POST: 

1327 if not request.user.check_password(request.POST["password"]): 

1328 messages.error(request, _("Incorrect password")) 

1329 return HttpResponseRedirect(reverse_lazy("delete")) 

1330 else: 

1331 messages.error(request, _("No password given")) 

1332 return HttpResponseRedirect(reverse_lazy("delete")) 

1333 

1334 # should delete all confirmed/unconfirmed/photo objects 

1335 request.user.delete() 

1336 return super().post(self, request, args, kwargs) 

1337 

1338 

1339@method_decorator(login_required, name="dispatch") 

1340class ExportView(SuccessMessageMixin, TemplateView): 

1341 """ 

1342 View class responsible for libravatar user data export 

1343 """ 

1344 

1345 template_name = "export.html" 

1346 model = User 

1347 

1348 def get(self, request, *args, **kwargs): 

1349 return super().get(self, request, args, kwargs) 

1350 

1351 def post(self, request, *args, **kwargs): 

1352 """ 

1353 Handle real export 

1354 """ 

1355 SCHEMA_ROOT = "https://www.libravatar.org/schemas/export/0.2" 

1356 SCHEMA_XSD = f"{SCHEMA_ROOT}/export.xsd" 

1357 

1358 def xml_header(): 

1359 return ( 

1360 """<?xml version="1.0" encoding="UTF-8"?>""" 

1361 '''<user xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"''' 

1362 ''' xsi:schemaLocation="%s %s"''' 

1363 """ xmlns="%s">\n""" % (SCHEMA_ROOT, SCHEMA_XSD, SCHEMA_ROOT) 

1364 ) 

1365 

1366 def xml_footer(): 

1367 return "</user>\n" 

1368 

1369 def xml_account(user): 

1370 escaped_username = saxutils.quoteattr(user.username) 

1371 escaped_password = saxutils.quoteattr(user.password) 

1372 return " <account username={} password={}/>\n".format( 

1373 escaped_username, 

1374 escaped_password, 

1375 ) 

1376 

1377 def xml_email(user): 

1378 returnstring = " <emails>\n" 

1379 for email in user.confirmedemail_set.all(): 

1380 returnstring += ( 

1381 ' <email photo_id="' 

1382 + str(email.photo_id) 

1383 + '">' 

1384 + str(email.email) 

1385 + "</email>" 

1386 + "\n" 

1387 ) 

1388 returnstring += " </emails>\n" 

1389 return returnstring 

1390 

1391 def xml_openid(user): 

1392 returnstring = " <openids>\n" 

1393 for openid in user.confirmedopenid_set.all(): 

1394 returnstring += ( 

1395 ' <openid photo_id="' 

1396 + str(openid.photo_id) 

1397 + '">' 

1398 + str(openid.openid) 

1399 + "</openid>" 

1400 + "\n" 

1401 ) 

1402 returnstring += " </openids>\n" 

1403 return returnstring 

1404 

1405 def xml_photos(user): 

1406 s = " <photos>\n" 

1407 for photo in user.photo_set.all(): 

1408 encoded_photo = base64.b64encode(photo.data) 

1409 if encoded_photo: 

1410 s += ( 

1411 """ <photo id="%s" encoding="base64" format=%s>""" 

1412 """%s""" 

1413 """</photo>\n""" 

1414 % (photo.id, saxutils.quoteattr(photo.format), encoded_photo) 

1415 ) 

1416 s += " </photos>\n" 

1417 return s 

1418 

1419 user = request.user 

1420 

1421 photos = [] 

1422 for photo in user.photo_set.all(): 

1423 photo_details = {"data": photo.data, "format": photo.format} 

1424 photos.append(photo_details) 

1425 

1426 bytesobj = BytesIO() 

1427 data = gzip.GzipFile(fileobj=bytesobj, mode="w") 

1428 data.write(bytes(xml_header(), "utf-8")) 

1429 data.write(bytes(xml_account(user), "utf-8")) 

1430 data.write(bytes(xml_email(user), "utf-8")) 

1431 data.write(bytes(xml_openid(user), "utf-8")) 

1432 data.write(bytes(xml_photos(user), "utf-8")) 

1433 data.write(bytes(xml_footer(), "utf-8")) 

1434 data.close() 

1435 bytesobj.seek(0) 

1436 

1437 response = HttpResponse(content_type="application/gzip") 

1438 response["Content-Disposition"] = ( 

1439 f'attachment; filename="libravatar-export_{user.username}.xml.gz"' 

1440 ) 

1441 response.write(bytesobj.read()) 

1442 return response