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

605 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-26 00:11 +0000

1# -*- coding: utf-8 -*- 

2""" 

3View classes for ivatar/ivataraccount/ 

4""" 

5from io import BytesIO 

6from urllib.request import urlopen 

7import base64 

8import binascii 

9from xml.sax import saxutils 

10import gzip 

11 

12from PIL import Image 

13 

14from django.db.models import ProtectedError 

15from django.core.exceptions import ObjectDoesNotExist 

16from django.contrib.auth.decorators import login_required 

17from django.contrib.auth.models import User 

18from django.utils.decorators import method_decorator 

19from django.contrib.messages.views import SuccessMessageMixin 

20from django.contrib import messages 

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

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

23from django.views.generic.detail import DetailView 

24from django.contrib.auth import authenticate, login 

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

26from django.contrib.auth.views import LoginView 

27from django.contrib.auth.views import ( 

28 PasswordResetView as PasswordResetViewOriginal, 

29) 

30from django.utils.translation import gettext_lazy as _ 

31from django.http import HttpResponseRedirect, HttpResponse 

32from django.urls import reverse_lazy, reverse 

33from django.shortcuts import render 

34from django_openid_auth.models import UserOpenID 

35 

36from openid import oidutil 

37from openid.consumer import consumer 

38 

39from ipware import get_client_ip 

40 

41from email_validator import validate_email 

42 

43from libravatar import libravatar_url 

44from ivatar.settings import ( 

45 MAX_NUM_PHOTOS, 

46 MAX_PHOTO_SIZE, 

47 JPEG_QUALITY, 

48 AVATAR_MAX_SIZE, 

49) 

50from .gravatar import get_photo as get_gravatar_photo 

51 

52from .forms import AddEmailForm, UploadPhotoForm, AddOpenIDForm 

53from .forms import UpdatePreferenceForm, UploadLibravatarExportForm 

54from .forms import DeleteAccountForm 

55from .models import UnconfirmedEmail, ConfirmedEmail, Photo 

56from .models import UnconfirmedOpenId, ConfirmedOpenId, DjangoOpenIDStore 

57from .models import UserPreference 

58from .models import file_format 

59from .read_libravatar_export import read_gzdata as libravatar_read_gzdata 

60 

61 

62def openid_logging(message, level=0): 

63 """ 

64 Helper method for openid logging 

65 """ 

66 # Normal messages are not that important 

67 # No need for coverage here 

68 if level > 0: # pragma: no cover 

69 print(message) 

70 

71 

72class CreateView(SuccessMessageMixin, FormView): 

73 """ 

74 View class for creating a new user 

75 """ 

76 

77 template_name = "new.html" 

78 form_class = UserCreationForm 

79 

80 def form_valid(self, form): 

81 form.save() 

82 user = authenticate( 

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

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

85 ) 

86 if user is not None: 

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

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

89 # email address 

90 try: 

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

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

93 user.email = valid.email 

94 user.save() 

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

96 unconfirmed = UnconfirmedEmail() 

97 unconfirmed.email = valid.email 

98 unconfirmed.user = user 

99 unconfirmed.save() 

100 unconfirmed.send_confirmation_mail( 

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

102 ) 

103 # In any exception cases, we just skip it 

104 except Exception: # pylint: disable=broad-except 

105 pass 

106 

107 login(self.request, user) 

108 pref = UserPreference.objects.create( 

109 user_id=user.pk 

110 ) # pylint: disable=no-member 

111 pref.save() 

112 return HttpResponseRedirect(reverse_lazy("profile")) 

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

114 

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

116 """ 

117 Handle get for create view 

118 """ 

119 if request.user: 

120 if request.user.is_authenticated: 

121 return HttpResponseRedirect(reverse_lazy("profile")) 

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

123 

124 

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

126class PasswordSetView(SuccessMessageMixin, FormView): 

127 """ 

128 View class for changing the password 

129 """ 

130 

131 template_name = "password_change.html" 

132 form_class = SetPasswordForm 

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

134 success_url = reverse_lazy("profile") 

135 

136 def get_form_kwargs(self): 

137 kwargs = super(PasswordSetView, self).get_form_kwargs() 

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

139 return kwargs 

140 

141 def form_valid(self, form): 

142 form.save() 

143 super().form_valid(form) 

144 return HttpResponseRedirect(reverse_lazy("login")) 

145 

146 

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

148class AddEmailView(SuccessMessageMixin, FormView): 

149 """ 

150 View class for adding email addresses 

151 """ 

152 

153 template_name = "add_email.html" 

154 form_class = AddEmailForm 

155 success_url = reverse_lazy("profile") 

156 

157 def form_valid(self, form): 

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

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

160 

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

162 return super().form_valid(form) 

163 

164 

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

166class RemoveUnconfirmedEmailView(SuccessMessageMixin, View): 

167 """ 

168 View class for removing a unconfirmed email address 

169 """ 

170 

171 @staticmethod 

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

173 """ 

174 Handle post request - removing unconfirmed email 

175 """ 

176 try: 

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

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

179 ) 

180 email.delete() 

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

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

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

184 return HttpResponseRedirect(reverse_lazy("profile")) 

185 

186 

187class ConfirmEmailView(SuccessMessageMixin, TemplateView): 

188 """ 

189 View class for confirming an unconfirmed email address 

190 """ 

191 

192 template_name = "email_confirmed.html" 

193 

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

195 # be tolerant of extra crap added by mail clients 

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

197 

198 if len(key) != 64: 

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

200 return HttpResponseRedirect(reverse_lazy("profile")) 

201 

202 try: 

203 unconfirmed = UnconfirmedEmail.objects.get( 

204 verification_key=key 

205 ) # pylint: disable=no-member 

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

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

208 return HttpResponseRedirect(reverse_lazy("profile")) 

209 

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

211 messages.error( 

212 request, 

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

214 ) 

215 return HttpResponseRedirect(reverse_lazy("profile")) 

216 

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

218 

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

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

221 ) 

222 

223 unconfirmed.delete() 

224 

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

226 # assign it to the new email 

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

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

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

230 kwargs["photos"] = external_photos 

231 kwargs["email_id"] = confirmed_id 

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

233 

234 

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

236class RemoveConfirmedEmailView(SuccessMessageMixin, View): 

237 """ 

238 View class for removing a confirmed email address 

239 """ 

240 

241 @staticmethod 

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

243 """ 

244 Handle post request - removing confirmed email 

245 """ 

246 try: 

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

248 email.delete() 

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

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

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

252 return HttpResponseRedirect(reverse_lazy("profile")) 

253 

254 

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

256class AssignPhotoEmailView(SuccessMessageMixin, TemplateView): 

257 """ 

258 View class for assigning a photo to an email address 

259 """ 

260 

261 model = Photo 

262 template_name = "assign_photo_email.html" 

263 

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

265 """ 

266 Handle post request - assign photo to email 

267 """ 

268 photo = None 

269 

270 try: 

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

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

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

274 return HttpResponseRedirect(reverse_lazy("profile")) 

275 

276 if "photoNone" in request.POST: 

277 email.photo = None 

278 else: 

279 if "photo_id" not in request.POST: 

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

281 return HttpResponseRedirect(reverse_lazy("profile")) 

282 

283 try: 

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

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

286 ) 

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

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

289 return HttpResponseRedirect(reverse_lazy("profile")) 

290 email.photo = photo 

291 email.save() 

292 

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

294 return HttpResponseRedirect(reverse_lazy("profile")) 

295 

296 def get_context_data(self, **kwargs): 

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

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

299 return data 

300 

301 

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

303class AssignPhotoOpenIDView(SuccessMessageMixin, TemplateView): 

304 """ 

305 View class for assigning a photo to an openid address 

306 """ 

307 

308 model = Photo 

309 template_name = "assign_photo_openid.html" 

310 

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

312 """ 

313 Handle post - assign photo to openid 

314 """ 

315 photo = None 

316 

317 try: 

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

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

320 ) 

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

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

323 return HttpResponseRedirect(reverse_lazy("profile")) 

324 

325 if "photoNone" in request.POST: 

326 openid.photo = None 

327 else: 

328 if "photo_id" not in request.POST: 

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

330 return HttpResponseRedirect(reverse_lazy("profile")) 

331 

332 try: 

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

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

335 ) 

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

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

338 return HttpResponseRedirect(reverse_lazy("profile")) 

339 openid.photo = photo 

340 openid.save() 

341 

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

343 return HttpResponseRedirect(reverse_lazy("profile")) 

344 

345 def get_context_data(self, **kwargs): 

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

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

348 pk=kwargs["openid_id"] 

349 ) # pylint: disable=no-member 

350 return data 

351 

352 

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

354class ImportPhotoView(SuccessMessageMixin, TemplateView): 

355 """ 

356 View class to import a photo from another service 

357 Currently only Gravatar is supported 

358 """ 

359 

360 template_name = "import_photo.html" 

361 

362 def get_context_data(self, **kwargs): 

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

364 context["photos"] = [] 

365 addr = None 

366 if "email_id" in kwargs: 

367 try: 

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

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

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

371 return context 

372 

373 addr = kwargs.get("email_addr", None) 

374 

375 if addr: 

376 gravatar = get_gravatar_photo(addr) 

377 if gravatar: 

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

379 

380 libravatar_service_url = libravatar_url( 

381 email=addr, 

382 default=404, 

383 size=AVATAR_MAX_SIZE, 

384 ) 

385 if libravatar_service_url: 

386 try: 

387 urlopen(libravatar_service_url) 

388 except OSError as exc: 

389 print("Exception caught during photo import: {}".format(exc)) 

390 else: 

391 context["photos"].append( 

392 { 

393 "service_url": libravatar_service_url, 

394 "thumbnail_url": libravatar_service_url + "&s=80", 

395 "image_url": libravatar_service_url + "&s=512", 

396 "width": 80, 

397 "height": 80, 

398 "service_name": "Libravatar", 

399 } 

400 ) 

401 

402 return context 

403 

404 def post( 

405 self, request, *args, **kwargs 

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

407 """ 

408 Handle post to photo import 

409 """ 

410 

411 imported = None 

412 

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

414 addr = kwargs.get("emali_addr", request.POST.get("email_addr", None)) 

415 

416 if email_id: 

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

418 if email.exists(): 

419 addr = email.first().email 

420 else: 

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

422 return HttpResponseRedirect(reverse_lazy("profile")) 

423 

424 if "photo_Gravatar" in request.POST: 

425 photo = Photo() 

426 photo.user = request.user 

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

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

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

430 else: 

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

432 messages.error( 

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

434 ) # pragma: no cover 

435 imported = True 

436 

437 if "photo_Libravatar" in request.POST: 

438 photo = Photo() 

439 photo.user = request.user 

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

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

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

443 else: 

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

445 messages.error( 

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

447 ) # pragma: no cover 

448 imported = True 

449 if not imported: 

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

451 return HttpResponseRedirect(reverse_lazy("profile")) 

452 

453 

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

455class RawImageView(DetailView): 

456 """ 

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

458 """ 

459 

460 model = Photo 

461 

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

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

464 if not photo.user.id == request.user.id and not request.user.is_staff: 

465 return HttpResponseRedirect(reverse_lazy("home")) 

466 return HttpResponse(BytesIO(photo.data), content_type="image/%s" % photo.format) 

467 

468 

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

470class DeletePhotoView(SuccessMessageMixin, View): 

471 """ 

472 View class for deleting a photo 

473 """ 

474 

475 model = Photo 

476 

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

478 """ 

479 Handle get - delete photo 

480 """ 

481 try: 

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

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

484 ) 

485 photo.delete() 

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

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

488 return HttpResponseRedirect(reverse_lazy("profile")) 

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

490 return HttpResponseRedirect(reverse_lazy("profile")) 

491 

492 

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

494class UploadPhotoView(SuccessMessageMixin, FormView): 

495 """ 

496 View class responsible for photo upload 

497 """ 

498 

499 model = Photo 

500 template_name = "upload_photo.html" 

501 form_class = UploadPhotoForm 

502 success_message = _("Successfully uploaded") 

503 success_url = reverse_lazy("profile") 

504 

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

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

507 if num_photos >= MAX_NUM_PHOTOS: 

508 messages.error( 

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

510 ) 

511 return HttpResponseRedirect(reverse_lazy("profile")) 

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

513 

514 def form_valid(self, form): 

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

516 if photo_data.size > MAX_PHOTO_SIZE: 

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

518 return HttpResponseRedirect(reverse_lazy("profile")) 

519 

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

521 

522 if not photo: 

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

524 return HttpResponseRedirect(reverse_lazy("profile")) 

525 

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

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

528 return super().form_valid(form) 

529 

530 

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

532class AddOpenIDView(SuccessMessageMixin, FormView): 

533 """ 

534 View class for adding OpenID 

535 """ 

536 

537 template_name = "add_openid.html" 

538 form_class = AddOpenIDForm 

539 success_url = reverse_lazy("profile") 

540 

541 def form_valid(self, form): 

542 openid_id = form.save(self.request.user) 

543 if not openid_id: 

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

545 

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

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

548 # since this is misleading 

549 return HttpResponseRedirect( 

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

551 ) 

552 

553 

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

555class RemoveUnconfirmedOpenIDView(View): 

556 """ 

557 View class for removing a unconfirmed OpenID 

558 """ 

559 

560 model = UnconfirmedOpenId 

561 

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

563 """ 

564 Handle post - remove unconfirmed openid 

565 """ 

566 try: 

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

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

569 ) 

570 openid.delete() 

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

572 except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member,line-too-long 

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

574 return HttpResponseRedirect(reverse_lazy("profile")) 

575 

576 

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

578class RemoveConfirmedOpenIDView(View): 

579 """ 

580 View class for removing a confirmed OpenID 

581 """ 

582 

583 model = ConfirmedOpenId 

584 

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

586 """ 

587 Handle post - remove confirmed openid 

588 """ 

589 try: 

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

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

592 ) 

593 try: 

594 openidobj = ( 

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

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

597 ) 

598 ) 

599 openidobj.delete() 

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

601 # Why it is not there? 

602 print("How did we get here: %s" % exc) 

603 openid.delete() 

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

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

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

607 return HttpResponseRedirect(reverse_lazy("profile")) 

608 

609 

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

611class RedirectOpenIDView(View): 

612 """ 

613 Redirect view for OpenID 

614 """ 

615 

616 model = UnconfirmedOpenId 

617 

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

619 """ 

620 Handle get for OpenID redirect view 

621 """ 

622 try: 

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

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

625 ) 

626 except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member,line-too-long 

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

628 return HttpResponseRedirect(reverse_lazy("profile")) 

629 

630 user_url = unconfirmed.openid 

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

632 

633 oidutil.log = openid_logging 

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

635 

636 try: 

637 auth_request = openid_consumer.begin(user_url) 

638 except consumer.DiscoveryFailure as exc: 

639 messages.error(request, _("OpenID discovery failed: %s" % exc)) 

640 return HttpResponseRedirect(reverse_lazy("profile")) 

641 except UnicodeDecodeError as exc: # pragma: no cover 

642 msg = _( 

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

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

645 % { 

646 "userid": request.user.id, 

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

648 "message": exc, 

649 } 

650 ) 

651 print("message: %s" % msg) 

652 messages.error(request, msg) 

653 

654 if auth_request is None: # pragma: no cover 

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

656 return HttpResponseRedirect(reverse_lazy("profile")) 

657 

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

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

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

661 ) 

662 return HttpResponseRedirect( # pragma: no cover 

663 auth_request.redirectURL(realm, return_url) 

664 ) 

665 

666 

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

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

669 """ 

670 Confirm OpenID view 

671 """ 

672 

673 model = UnconfirmedOpenId 

674 model_confirmed = ConfirmedOpenId 

675 

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

677 """ 

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

679 """ 

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

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

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

683 info = openid_consumer.complete(data, current_url) 

684 if info.status == consumer.FAILURE: 

685 messages.error( 

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

687 ) 

688 return HttpResponseRedirect(reverse_lazy("profile")) 

689 

690 if info.status == consumer.CANCEL: 

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

692 return HttpResponseRedirect(reverse_lazy("profile")) 

693 

694 if info.status != consumer.SUCCESS: 

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

696 return HttpResponseRedirect(reverse_lazy("profile")) 

697 

698 try: 

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

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

701 ) 

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

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

704 return HttpResponseRedirect(reverse_lazy("profile")) 

705 

706 # TODO: Check for a reasonable expiration time 

707 confirmed = self.model_confirmed() 

708 confirmed.user = unconfirmed.user 

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

710 confirmed.openid = unconfirmed.openid 

711 confirmed.save() 

712 

713 unconfirmed.delete() 

714 

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

716 # assign it to the new id 

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

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

719 

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

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

722 claimed_id=confirmed.openid 

723 ).exists(): 

724 user_openid = UserOpenID() 

725 user_openid.user = self.request.user 

726 user_openid.claimed_id = confirmed.openid 

727 user_openid.display_id = confirmed.openid 

728 user_openid.save() 

729 return HttpResponseRedirect(reverse_lazy("profile")) 

730 

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

732 """ 

733 Handle get - confirm openid 

734 """ 

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

736 

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

738 """ 

739 Handle post - confirm openid 

740 """ 

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

742 

743 

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

745class CropPhotoView(TemplateView): 

746 """ 

747 View class for cropping photos 

748 """ 

749 

750 template_name = "crop_photo.html" 

751 success_url = reverse_lazy("profile") 

752 model = Photo 

753 

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

755 photo = self.model.objects.get( 

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

757 ) # pylint: disable=no-member 

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

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

760 return render( 

761 self.request, 

762 self.template_name, 

763 { 

764 "photo": photo, 

765 "email": email, 

766 "openid": openid, 

767 }, 

768 ) 

769 

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

771 """ 

772 Handle post - crop photo 

773 """ 

774 photo = self.model.objects.get( 

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

776 ) # pylint: disable=no-member 

777 dimensions = { 

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

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

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

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

782 } 

783 email = openid = None 

784 if "email" in request.POST: 

785 try: 

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

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

788 pass # Ignore automatic assignment 

789 

790 if "openid" in request.POST: 

791 try: 

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

793 openid=request.POST["openid"] 

794 ) 

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

796 pass # Ignore automatic assignment 

797 

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

799 

800 

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

802class UserPreferenceView(FormView, UpdateView): 

803 """ 

804 View class for user preferences view/update 

805 """ 

806 

807 template_name = "preferences.html" 

808 model = UserPreference 

809 form_class = UpdatePreferenceForm 

810 success_url = reverse_lazy("user_preference") 

811 

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

813 """ 

814 Process POST-ed data from this form 

815 """ 

816 userpref = None 

817 try: 

818 userpref = self.request.user.userpreference 

819 except ObjectDoesNotExist: 

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

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

822 userpref.save() 

823 try: 

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

825 addresses = list( 

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

827 "email", flat=True 

828 ) 

829 ) 

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

831 messages.error( 

832 self.request, 

833 _("Mail address not allowed: %s" % request.POST["email"]), 

834 ) 

835 else: 

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

837 self.request.user.save() 

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

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

840 messages.error(self.request, _("Error setting new mail address: %s" % e)) 

841 

842 try: 

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

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

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

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

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

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

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

850 self.request.user.save() 

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

852 messages.error(self.request, _("Error setting names: %s" % e)) 

853 

854 return HttpResponseRedirect(reverse_lazy("user_preference")) 

855 

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

857 return render( 

858 self.request, 

859 self.template_name, 

860 { 

861 "THEMES": UserPreference.THEMES, 

862 }, 

863 ) 

864 

865 def get_object(self, queryset=None): 

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

867 user=self.request.user 

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

869 return obj 

870 

871 

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

873class UploadLibravatarExportView(SuccessMessageMixin, FormView): 

874 """ 

875 View class responsible for libravatar user data export upload 

876 """ 

877 

878 template_name = "upload_libravatar_export.html" 

879 form_class = UploadLibravatarExportForm 

880 success_message = _("Successfully uploaded") 

881 success_url = reverse_lazy("profile") 

882 model = User 

883 

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

885 """ 

886 Handle post request - choose items to import 

887 """ 

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

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

890 for arg in request.POST: 

891 if arg.startswith("email_"): 

892 email = request.POST[arg] 

893 if not ConfirmedEmail.objects.filter( 

894 email=email 

895 ) and not UnconfirmedEmail.objects.filter( 

896 email=email 

897 ): # pylint: disable=no-member 

898 try: 

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

900 user=request.user, email=email 

901 ) 

902 unconfirmed.save() 

903 unconfirmed.send_confirmation_mail( 

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

905 ) 

906 messages.info( 

907 request, 

908 "%s: %s" 

909 % ( 

910 email, 

911 _( 

912 "address added successfully,\ 

913 confirmation mail sent" 

914 ), 

915 ), 

916 ) 

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

918 # DEBUG 

919 print( 

920 "Exception during adding mail address (%s): %s" 

921 % (email, exc) 

922 ) 

923 

924 if arg.startswith("photo"): 

925 try: 

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

927 except binascii.Error as exc: 

928 print("Cannot decode photo: %s" % exc) 

929 continue 

930 try: 

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

932 out = BytesIO() 

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

934 out.seek(0) 

935 photo = Photo() 

936 photo.user = request.user 

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

938 photo.format = file_format(pilobj.format) 

939 photo.data = out.read() 

940 photo.save() 

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

942 print("Exception during save: %s" % exc) 

943 continue 

944 

945 return HttpResponseRedirect(reverse_lazy("profile")) 

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

947 

948 def form_valid(self, form): 

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

950 try: 

951 items = libravatar_read_gzdata(data.read()) 

952 # DEBUG print(items) 

953 return render( 

954 self.request, 

955 "choose_libravatar_export.html", 

956 { 

957 "emails": items["emails"], 

958 "photos": items["photos"], 

959 }, 

960 ) 

961 except Exception as e: 

962 messages.error(self.request, _("Unable to parse file: %s" % e)) 

963 return HttpResponseRedirect(reverse_lazy("upload_export")) 

964 

965 

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

967class ResendConfirmationMailView(View): 

968 """ 

969 View class for resending confirmation mail 

970 """ 

971 

972 model = UnconfirmedEmail 

973 

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

975 """ 

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

977 """ 

978 try: 

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

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

981 ) 

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

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

984 else: 

985 try: 

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

987 messages.success( 

988 request, "%s: %s" % (_("Confirmation mail sent to"), email.email) 

989 ) 

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

991 messages.error( 

992 request, 

993 "%s %s: %s" 

994 % (_("Unable to send confirmation email for"), email.email, exc), 

995 ) 

996 return HttpResponseRedirect(reverse_lazy("profile")) 

997 

998 

999class IvatarLoginView(LoginView): 

1000 """ 

1001 View class for login 

1002 """ 

1003 

1004 template_name = "login.html" 

1005 

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

1007 """ 

1008 Handle get for login view 

1009 """ 

1010 if request.user: 

1011 if request.user.is_authenticated: 

1012 return HttpResponseRedirect(reverse_lazy("profile")) 

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

1014 

1015 

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

1017class ProfileView(TemplateView): 

1018 """ 

1019 View class for profile 

1020 """ 

1021 

1022 template_name = "profile.html" 

1023 

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

1025 if "profile_username" in kwargs: 

1026 if not request.user.is_staff: 

1027 return HttpResponseRedirect(reverse_lazy("profile")) 

1028 try: 

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

1030 request.user = u 

1031 except Exception: # pylint: disable=broad-except 

1032 pass 

1033 

1034 self._confirm_claimed_openid() 

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

1036 

1037 def get_context_data(self, **kwargs): 

1038 """ 

1039 Provide additional context data, like if max_photos is reached 

1040 already or not. 

1041 """ 

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

1043 context["max_photos"] = False 

1044 if self.request.user: 

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

1046 context["max_photos"] = True 

1047 return context 

1048 

1049 def _confirm_claimed_openid(self): 

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

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

1052 # the user account 

1053 if openids.count() == 1: 

1054 # Already confirmed, skip 

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

1056 openid=openids.first().claimed_id 

1057 ).exists(): 

1058 return 

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

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

1061 openid=openids.first().claimed_id 

1062 ).exists(): 

1063 return 

1064 print("need to confirm: %s" % openids.first()) 

1065 confirmed = ConfirmedOpenId() 

1066 confirmed.user = self.request.user 

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

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

1069 confirmed.save() 

1070 

1071 

1072class PasswordResetView(PasswordResetViewOriginal): 

1073 """ 

1074 View class for password reset 

1075 """ 

1076 

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

1078 """ 

1079 Since we have the mail addresses in ConfirmedEmail model, 

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

1081 PasswordResetView class to pick up the correct user. 

1082 In case we have the mail address in the User objecct, we still 

1083 need to assign a random password in order for PasswordResetView 

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

1085 """ 

1086 if "email" in request.POST: 

1087 user = None 

1088 

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

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

1091 # could have the same password set? 

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

1093 

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

1095 # try the ConfirmedEmail class instead. 

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

1097 # attribute on the user object accordingly 

1098 if not user: 

1099 try: 

1100 confirmed_email = ConfirmedEmail.objects.get( 

1101 email=request.POST["email"] 

1102 ) 

1103 user = confirmed_email.user 

1104 user.email = confirmed_email.email 

1105 user.save() 

1106 except ObjectDoesNotExist: 

1107 pass 

1108 

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

1110 # ResetPasswordView class will silently ignore the password 

1111 # reset request 

1112 if user: 

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

1114 random_pass = User.objects.make_random_password() 

1115 user.set_password(random_pass) 

1116 user.save() 

1117 

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

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

1120 

1121 

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

1123class DeleteAccountView(SuccessMessageMixin, FormView): 

1124 """ 

1125 View class for account deletion 

1126 """ 

1127 

1128 template_name = "delete.html" 

1129 form_class = DeleteAccountForm 

1130 success_url = reverse_lazy("home") 

1131 

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

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

1134 

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

1136 """ 

1137 Handle account deletion 

1138 """ 

1139 if request.user.password: 

1140 if "password" in request.POST: 

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

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

1143 return HttpResponseRedirect(reverse_lazy("delete")) 

1144 else: 

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

1146 return HttpResponseRedirect(reverse_lazy("delete")) 

1147 

1148 raise _("No password given") 

1149 # should delete all confirmed/unconfirmed/photo objects 

1150 request.user.delete() 

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

1152 

1153 

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

1155class ExportView(SuccessMessageMixin, TemplateView): 

1156 """ 

1157 View class responsible for libravatar user data export 

1158 """ 

1159 

1160 template_name = "export.html" 

1161 model = User 

1162 

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

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

1165 

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

1167 """ 

1168 Handle real export 

1169 """ 

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

1171 SCHEMA_XSD = "%s/export.xsd" % SCHEMA_ROOT 

1172 

1173 def xml_header(): 

1174 return ( 

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

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

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

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

1179 ) 

1180 

1181 def xml_footer(): 

1182 return "</user>\n" 

1183 

1184 def xml_account(user): 

1185 escaped_username = saxutils.quoteattr(user.username) 

1186 escaped_password = saxutils.quoteattr(user.password) 

1187 return " <account username=%s password=%s/>\n" % ( 

1188 escaped_username, 

1189 escaped_password, 

1190 ) 

1191 

1192 def xml_email(user): 

1193 returnstring = " <emails>\n" 

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

1195 returnstring += ( 

1196 ' <email photo_id="' 

1197 + str(email.photo_id) 

1198 + '">' 

1199 + str(email.email) 

1200 + "</email>" 

1201 + "\n" 

1202 ) 

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

1204 return returnstring 

1205 

1206 def xml_openid(user): 

1207 returnstring = " <openids>\n" 

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

1209 returnstring += ( 

1210 ' <openid photo_id="' 

1211 + str(openid.photo_id) 

1212 + '">' 

1213 + str(openid.openid) 

1214 + "</openid>" 

1215 + "\n" 

1216 ) 

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

1218 return returnstring 

1219 

1220 def xml_photos(user): 

1221 s = " <photos>\n" 

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

1223 encoded_photo = base64.b64encode(photo.data) 

1224 if encoded_photo: 

1225 s += ( 

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

1227 """%s""" 

1228 """</photo>\n""" 

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

1230 ) 

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

1232 return s 

1233 

1234 user = request.user 

1235 

1236 photos = [] 

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

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

1239 photos.append(photo_details) 

1240 

1241 bytesobj = BytesIO() 

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

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

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

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

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

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

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

1249 data.close() 

1250 bytesobj.seek(0) 

1251 

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

1253 response["Content-Disposition"] = ( 

1254 'attachment; filename="libravatar-export_%s.xml.gz"' % user.username 

1255 ) 

1256 response.write(bytesobj.read()) 

1257 return response