From 35b666224a05fbd249b1c51a0a48fb95b246fe9a Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Fri, 10 Jul 2009 19:15:28 -0400 Subject: adding all files again --- django_authopenid/__init__.py | 40 ++ django_authopenid/admin.py | 9 + django_authopenid/forms.py | 435 ++++++++++++++++++++++ django_authopenid/middleware.py | 24 ++ django_authopenid/mimeparse.py | 160 ++++++++ django_authopenid/models.py | 71 ++++ django_authopenid/urls.py | 27 ++ django_authopenid/util.py | 145 ++++++++ django_authopenid/views.py | 787 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 1698 insertions(+) create mode 100644 django_authopenid/__init__.py create mode 100644 django_authopenid/admin.py create mode 100644 django_authopenid/forms.py create mode 100644 django_authopenid/middleware.py create mode 100644 django_authopenid/mimeparse.py create mode 100644 django_authopenid/models.py create mode 100644 django_authopenid/urls.py create mode 100644 django_authopenid/util.py create mode 100644 django_authopenid/views.py (limited to 'django_authopenid') diff --git a/django_authopenid/__init__.py b/django_authopenid/__init__.py new file mode 100644 index 00000000..ff040ed7 --- /dev/null +++ b/django_authopenid/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2007, 2008, Benoît Chesneau +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# * notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# * notice, this list of conditions and the following disclaimer in the +# * documentation and/or other materials provided with the +# * distribution. Neither the name of the nor the names +# * of its contributors may be used to endorse or promote products +# * derived from this software without specific prior written +# * permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +Django authentification application to *with openid using django auth contrib/. + +This application allow a user to connect to you website with : + * legacy account : username/password + * openid url +""" + +__version__ = "0.9.4" diff --git a/django_authopenid/admin.py b/django_authopenid/admin.py new file mode 100644 index 00000000..f64ee579 --- /dev/null +++ b/django_authopenid/admin.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from django.contrib import admin +from django_authopenid.models import UserAssociation + + +class UserAssociationAdmin(admin.ModelAdmin): + """User association admin class""" +admin.site.register(UserAssociation, UserAssociationAdmin) \ No newline at end of file diff --git a/django_authopenid/forms.py b/django_authopenid/forms.py new file mode 100644 index 00000000..09fa76b1 --- /dev/null +++ b/django_authopenid/forms.py @@ -0,0 +1,435 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2007, 2008, Benoît Chesneau +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# * notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# * notice, this list of conditions and the following disclaimer in the +# * documentation and/or other materials provided with the +# * distribution. Neither the name of the nor the names +# * of its contributors may be used to endorse or promote products +# * derived from this software without specific prior written +# * permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +from django import forms +from django.contrib.auth.models import User +from django.contrib.auth import authenticate +from django.utils.translation import ugettext as _ +from django.conf import settings + +import re + + +# needed for some linux distributions like debian +try: + from openid.yadis import xri +except ImportError: + from yadis import xri + +from django_authopenid.util import clean_next + +__all__ = ['OpenidSigninForm', 'OpenidAuthForm', 'OpenidVerifyForm', + 'OpenidRegisterForm', 'RegistrationForm', 'ChangepwForm', + 'ChangeemailForm', 'EmailPasswordForm', 'DeleteForm', + 'ChangeOpenidForm', 'ChangeEmailForm', 'ChangepwForm'] + +class OpenidSigninForm(forms.Form): + """ signin form """ + openid_url = forms.CharField(max_length=255, widget=forms.widgets.TextInput(attrs={'class': 'openid-login-input', 'size':80})) + next = forms.CharField(max_length=255, widget=forms.HiddenInput(), required=False) + + def clean_openid_url(self): + """ test if openid is accepted """ + if 'openid_url' in self.cleaned_data: + openid_url = self.cleaned_data['openid_url'] + if xri.identifierScheme(openid_url) == 'XRI' and getattr( + settings, 'OPENID_DISALLOW_INAMES', False + ): + raise forms.ValidationError(_('i-names are not supported')) + return self.cleaned_data['openid_url'] + + def clean_next(self): + """ validate next """ + if 'next' in self.cleaned_data and self.cleaned_data['next'] != "": + self.cleaned_data['next'] = clean_next(self.cleaned_data['next']) + return self.cleaned_data['next'] + + +attrs_dict = { 'class': 'required login' } +username_re = re.compile(r'^\w+$') +RESERVED_NAMES = (u'fuck', u'shit', u'ass', u'sex', u'add', + u'edit', u'save', u'delete', u'manage', u'update', 'remove', 'new') + +class OpenidAuthForm(forms.Form): + """ legacy account signin form """ + next = forms.CharField(max_length=255, widget=forms.HiddenInput(), + required=False) + username = forms.CharField(max_length=30, + widget=forms.widgets.TextInput(attrs=attrs_dict)) + password = forms.CharField(max_length=128, + widget=forms.widgets.PasswordInput(attrs=attrs_dict)) + + def __init__(self, data=None, files=None, auto_id='id_%s', + prefix=None, initial=None): + super(OpenidAuthForm, self).__init__(data, files, auto_id, + prefix, initial) + self.user_cache = None + + def clean_username(self): + """ validate username and test if it exists.""" + if 'username' in self.cleaned_data and \ + 'openid_url' not in self.cleaned_data: + if not username_re.search(self.cleaned_data['username']): + raise forms.ValidationError(_("Usernames can only contain \ + letters, numbers and underscores")) + try: + user = User.objects.get( + username__exact = self.cleaned_data['username'] + ) + except User.DoesNotExist: + raise forms.ValidationError(_("This username does not exist \ + in our database. Please choose another.")) + except User.MultipleObjectsReturned: + raise forms.ValidationError(u'There is already more than one \ + account registered with that username. Please try \ + another.') + return self.cleaned_data['username'] + + def clean_password(self): + """" test if password is valid for this username """ + if 'username' in self.cleaned_data and \ + 'password' in self.cleaned_data: + self.user_cache = authenticate( + username=self.cleaned_data['username'], + password=self.cleaned_data['password'] + ) + if self.user_cache is None: + raise forms.ValidationError(_("Please enter a valid \ + username and password. Note that both fields are \ + case-sensitive.")) + elif self.user_cache.is_active == False: + raise forms.ValidationError(_("This account is inactive.")) + return self.cleaned_data['password'] + + def clean_next(self): + """ validate next url """ + if 'next' in self.cleaned_data and \ + self.cleaned_data['next'] != "": + self.cleaned_data['next'] = clean_next(self.cleaned_data['next']) + return self.cleaned_data['next'] + + def get_user(self): + """ get authenticated user """ + return self.user_cache + + +class OpenidRegisterForm(forms.Form): + """ openid signin form """ + next = forms.CharField(max_length=255, widget=forms.HiddenInput(), + required=False) + username = forms.CharField(max_length=30, + widget=forms.widgets.TextInput(attrs=attrs_dict)) + email = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict, + maxlength=200)), label=u'Email address') + + def clean_username(self): + """ test if username is valid and exist in database """ + if 'username' in self.cleaned_data: + if not username_re.search(self.cleaned_data['username']): + raise forms.ValidationError(_('invalid user name')) + if self.cleaned_data['username'] in RESERVED_NAMES: + raise forms.ValidationError(_('sorry, this name can not be used, please try another')) + if len(self.cleaned_data['username']) < 3: + raise forms.ValidationError(_('username too short')) + try: + user = User.objects.get( + username__exact = self.cleaned_data['username'] + ) + except User.DoesNotExist: + return self.cleaned_data['username'] + except User.MultipleObjectsReturned: + raise forms.ValidationError(_('this name is already in use - please try anoter')) + raise forms.ValidationError(_('this name is already in use - please try anoter')) + + def clean_email(self): + """For security reason one unique email in database""" + if 'email' in self.cleaned_data: + try: + user = User.objects.get(email = self.cleaned_data['email']) + except User.DoesNotExist: + return self.cleaned_data['email'] + except User.MultipleObjectsReturned: + raise forms.ValidationError(u'There is already more than one \ + account registered with that e-mail address. Please try \ + another.') + raise forms.ValidationError(_("This email is already \ + registered in our database. Please choose another.")) + + +class OpenidVerifyForm(forms.Form): + """ openid verify form (associate an openid with an account) """ + next = forms.CharField(max_length=255, widget = forms.HiddenInput(), + required=False) + username = forms.CharField(max_length=30, + widget=forms.widgets.TextInput(attrs=attrs_dict)) + password = forms.CharField(max_length=128, + widget=forms.widgets.PasswordInput(attrs=attrs_dict)) + + def __init__(self, data=None, files=None, auto_id='id_%s', + prefix=None, initial=None): + super(OpenidVerifyForm, self).__init__(data, files, auto_id, + prefix, initial) + self.user_cache = None + + def clean_username(self): + """ validate username """ + if 'username' in self.cleaned_data: + if not username_re.search(self.cleaned_data['username']): + raise forms.ValidationError(_("Usernames can only contain \ + letters, numbers and underscores")) + try: + user = User.objects.get( + username__exact = self.cleaned_data['username'] + ) + except User.DoesNotExist: + raise forms.ValidationError(_("This username don't exist. \ + Please choose another.")) + except User.MultipleObjectsReturned: + raise forms.ValidationError(u'Somehow, that username is in \ + use for multiple accounts. Please contact us to get this \ + problem resolved.') + return self.cleaned_data['username'] + + def clean_password(self): + """ test if password is valid for this user """ + if 'username' in self.cleaned_data and \ + 'password' in self.cleaned_data: + self.user_cache = authenticate( + username = self.cleaned_data['username'], + password = self.cleaned_data['password'] + ) + if self.user_cache is None: + raise forms.ValidationError(_("Please enter a valid \ + username and password. Note that both fields are \ + case-sensitive.")) + elif self.user_cache.is_active == False: + raise forms.ValidationError(_("This account is inactive.")) + return self.cleaned_data['password'] + + def get_user(self): + """ get authenticated user """ + return self.user_cache + + +attrs_dict = { 'class': 'required' } +username_re = re.compile(r'^\w+$') + +class RegistrationForm(forms.Form): + """ legacy registration form """ + + next = forms.CharField(max_length=255, widget=forms.HiddenInput(), + required=False) + username = forms.CharField(max_length=30, + widget=forms.TextInput(attrs=attrs_dict), + label=_('choose a username')) + email = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict, + maxlength=200)), label=_('your email address')) + password1 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict), + label=_('choose password')) + password2 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict), + label=_('retype password')) + + def clean_username(self): + """ + Validates that the username is alphanumeric and is not already + in use. + + """ + if 'username' in self.cleaned_data: + if not username_re.search(self.cleaned_data['username']): + raise forms.ValidationError(u'Usernames can only contain \ + letters, numbers and underscores') + try: + user = User.objects.get( + username__exact = self.cleaned_data['username'] + ) + + except User.DoesNotExist: + return self.cleaned_data['username'] + except User.MultipleObjectsReturned: + raise forms.ValidationError(u'Somehow, that username is in \ + use for multiple accounts. Please contact us to get this \ + problem resolved.') + raise forms.ValidationError(u'This username is already taken. \ + Please choose another.') + + def clean_email(self): + """ validate if email exist in database + :return: raise error if it exist """ + if 'email' in self.cleaned_data: + try: + user = User.objects.get(email = self.cleaned_data['email']) + except User.DoesNotExist: + return self.cleaned_data['email'] + except User.MultipleObjectsReturned: + raise forms.ValidationError(u'There is already more than one \ + account registered with that e-mail address. Please try \ + another.') + raise forms.ValidationError(u'This email is already registered \ + in our database. Please choose another.') + return self.cleaned_data['email'] + + def clean_password2(self): + """ + Validates that the two password inputs match. + + """ + if 'password1' in self.cleaned_data and \ + 'password2' in self.cleaned_data and \ + self.cleaned_data['password1'] == \ + self.cleaned_data['password2']: + return self.cleaned_data['password2'] + raise forms.ValidationError(u'You must type the same password each \ + time') + + +class ChangepwForm(forms.Form): + """ change password form """ + oldpw = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict)) + password1 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict)) + password2 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict)) + + def __init__(self, data=None, user=None, *args, **kwargs): + if user is None: + raise TypeError("Keyword argument 'user' must be supplied") + super(ChangepwForm, self).__init__(data, *args, **kwargs) + self.user = user + + def clean_oldpw(self): + """ test old password """ + if not self.user.check_password(self.cleaned_data['oldpw']): + raise forms.ValidationError(_("Old password is incorrect. \ + Please enter the correct password.")) + return self.cleaned_data['oldpw'] + + def clean_password2(self): + """ + Validates that the two password inputs match. + """ + if 'password1' in self.cleaned_data and \ + 'password2' in self.cleaned_data and \ + self.cleaned_data['password1'] == self.cleaned_data['password2']: + return self.cleaned_data['password2'] + raise forms.ValidationError(_("new passwords do not match")) + + +class ChangeemailForm(forms.Form): + """ change email form """ + email = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict, + maxlength=200)), label=u'Email address') + password = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict)) + + def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, \ + initial=None, user=None): + if user is None: + raise TypeError("Keyword argument 'user' must be supplied") + super(ChangeemailForm, self).__init__(data, files, auto_id, + prefix, initial) + self.test_openid = False + self.user = user + + + def clean_email(self): + """ check if email don't exist """ + if 'email' in self.cleaned_data: + if self.user.email != self.cleaned_data['email']: + try: + user = User.objects.get(email = self.cleaned_data['email']) + except User.DoesNotExist: + return self.cleaned_data['email'] + except User.MultipleObjectsReturned: + raise forms.ValidationError(u'There is already more than one \ + account registered with that e-mail address. Please try \ + another.') + raise forms.ValidationError(u'This email is already registered \ + in our database. Please choose another.') + return self.cleaned_data['email'] + + + def clean_password(self): + """ check if we have to test a legacy account or not """ + if 'password' in self.cleaned_data: + if not self.user.check_password(self.cleaned_data['password']): + self.test_openid = True + return self.cleaned_data['password'] + +class ChangeopenidForm(forms.Form): + """ change openid form """ + openid_url = forms.CharField(max_length=255, + widget=forms.TextInput(attrs={'class': "required" })) + + def __init__(self, data=None, user=None, *args, **kwargs): + if user is None: + raise TypeError("Keyword argument 'user' must be supplied") + super(ChangeopenidForm, self).__init__(data, *args, **kwargs) + self.user = user + +class DeleteForm(forms.Form): + """ confirm form to delete an account """ + confirm = forms.CharField(widget=forms.CheckboxInput(attrs=attrs_dict)) + password = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict)) + + def __init__(self, data=None, files=None, auto_id='id_%s', + prefix=None, initial=None, user=None): + super(DeleteForm, self).__init__(data, files, auto_id, prefix, initial) + self.test_openid = False + self.user = user + + def clean_password(self): + """ check if we have to test a legacy account or not """ + if 'password' in self.cleaned_data: + if not self.user.check_password(self.cleaned_data['password']): + self.test_openid = True + return self.cleaned_data['password'] + + +class EmailPasswordForm(forms.Form): + """ send new password form """ + username = forms.CharField(max_length=30, + widget=forms.TextInput(attrs={'class': "required" })) + + def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, + initial=None): + super(EmailPasswordForm, self).__init__(data, files, auto_id, + prefix, initial) + self.user_cache = None + + + def clean_username(self): + """ get user for this username """ + if 'username' in self.cleaned_data: + try: + self.user_cache = User.objects.get( + username = self.cleaned_data['username']) + except: + raise forms.ValidationError(_("Incorrect username.")) + return self.cleaned_data['username'] diff --git a/django_authopenid/middleware.py b/django_authopenid/middleware.py new file mode 100644 index 00000000..c0572c6e --- /dev/null +++ b/django_authopenid/middleware.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from django_authopenid import mimeparse +from django.http import HttpResponseRedirect +from django.core.urlresolvers import reverse + +__all__ = ["OpenIDMiddleware"] + +class OpenIDMiddleware(object): + """ + Populate request.openid. This comes either from cookie or from + session, depending on the presence of OPENID_USE_SESSIONS. + """ + def process_request(self, request): + request.openid = request.session.get('openid', None) + + def process_response(self, request, response): + if response.status_code != 200 or len(response.content) < 200: + return response + path = request.get_full_path() + if path == "/" and request.META.has_key('HTTP_ACCEPT') and \ + mimeparse.best_match(['text/html', 'application/xrds+xml'], + request.META['HTTP_ACCEPT']) == 'application/xrds+xml': + return HttpResponseRedirect(reverse('yadis_xrdf')) + return response \ No newline at end of file diff --git a/django_authopenid/mimeparse.py b/django_authopenid/mimeparse.py new file mode 100644 index 00000000..ab02eab0 --- /dev/null +++ b/django_authopenid/mimeparse.py @@ -0,0 +1,160 @@ +"""MIME-Type Parser + +This module provides basic functions for handling mime-types. It can handle +matching mime-types against a list of media-ranges. See section 14.1 of +the HTTP specification [RFC 2616] for a complete explaination. + + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + +Contents: + - parse_mime_type(): Parses a mime-type into it's component parts. + - parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q' quality parameter. + - quality(): Determines the quality ('q') of a mime-type when compared against a list of media-ranges. + - quality_parsed(): Just like quality() except the second parameter must be pre-parsed. + - best_match(): Choose the mime-type with the highest quality ('q') from a list of candidates. +""" + +__version__ = "0.1.1" +__author__ = 'Joe Gregorio' +__email__ = "joe@bitworking.org" +__credits__ = "" + +def parse_mime_type(mime_type): + """Carves up a mime_type and returns a tuple of the + (type, subtype, params) where 'params' is a dictionary + of all the parameters for the media range. + For example, the media range 'application/xhtml;q=0.5' would + get parsed into: + + ('application', 'xhtml', {'q', '0.5'}) + """ + parts = mime_type.split(";") + params = dict([tuple([s.strip() for s in param.split("=")])\ + for param in parts[1:] ]) + (type, subtype) = parts[0].split("/") + return (type.strip(), subtype.strip(), params) + +def parse_media_range(range): + """Carves up a media range and returns a tuple of the + (type, subtype, params) where 'params' is a dictionary + of all the parameters for the media range. + For example, the media range 'application/*;q=0.5' would + get parsed into: + + ('application', '*', {'q', '0.5'}) + + In addition this function also guarantees that there + is a value for 'q' in the params dictionary, filling it + in with a proper default if necessary. + """ + (type, subtype, params) = parse_mime_type(range) + if not params.has_key('q') or not params['q'] or \ + not float(params['q']) or float(params['q']) > 1\ + or float(params['q']) < 0: + params['q'] = '1' + return (type, subtype, params) + +def quality_parsed(mime_type, parsed_ranges): + """Find the best match for a given mime_type against + a list of media_ranges that have already been + parsed by parse_media_range(). Returns the + 'q' quality parameter of the best match, 0 if no + match was found. This function bahaves the same as quality() + except that 'parsed_ranges' must be a list of + parsed media ranges. """ + best_fitness = -1 + best_match = "" + best_fit_q = 0 + (target_type, target_subtype, target_params) =\ + parse_media_range(mime_type) + for (type, subtype, params) in parsed_ranges: + param_matches = reduce(lambda x, y: x+y, [1 for (key, value) in \ + target_params.iteritems() if key != 'q' and \ + params.has_key(key) and value == params[key]], 0) + if (type == target_type or type == '*' or target_type == '*') and \ + (subtype == target_subtype or subtype == '*' or target_subtype == '*'): + fitness = (type == target_type) and 100 or 0 + fitness += (subtype == target_subtype) and 10 or 0 + fitness += param_matches + if fitness > best_fitness: + best_fitness = fitness + best_fit_q = params['q'] + + return float(best_fit_q) + +def quality(mime_type, ranges): + """Returns the quality 'q' of a mime_type when compared + against the media-ranges in ranges. For example: + + >>> quality('text/html','text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5') + 0.7 + + """ + parsed_ranges = [parse_media_range(r) for r in ranges.split(",")] + return quality_parsed(mime_type, parsed_ranges) + +def best_match(supported, header): + """Takes a list of supported mime-types and finds the best + match for all the media-ranges listed in header. The value of + header must be a string that conforms to the format of the + HTTP Accept: header. The value of 'supported' is a list of + mime-types. + + >>> best_match(['application/xbel+xml', 'text/xml'], 'text/*;q=0.5,*/*; q=0.1') + 'text/xml' + """ + parsed_header = [parse_media_range(r) for r in header.split(",")] + weighted_matches = [(quality_parsed(mime_type, parsed_header), mime_type)\ + for mime_type in supported] + weighted_matches.sort() + return weighted_matches[-1][0] and weighted_matches[-1][1] or '' + +if __name__ == "__main__": + import unittest + + class TestMimeParsing(unittest.TestCase): + + def test_parse_media_range(self): + self.assert_(('application', 'xml', {'q': '1'}) == parse_media_range('application/xml;q=1')) + self.assertEqual(('application', 'xml', {'q': '1'}), parse_media_range('application/xml')) + self.assertEqual(('application', 'xml', {'q': '1'}), parse_media_range('application/xml;q=')) + self.assertEqual(('application', 'xml', {'q': '1'}), parse_media_range('application/xml ; q=')) + self.assertEqual(('application', 'xml', {'q': '1', 'b': 'other'}), parse_media_range('application/xml ; q=1;b=other')) + self.assertEqual(('application', 'xml', {'q': '1', 'b': 'other'}), parse_media_range('application/xml ; q=2;b=other')) + + def test_rfc_2616_example(self): + accept = "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5" + self.assertEqual(1, quality("text/html;level=1", accept)) + self.assertEqual(0.7, quality("text/html", accept)) + self.assertEqual(0.3, quality("text/plain", accept)) + self.assertEqual(0.5, quality("image/jpeg", accept)) + self.assertEqual(0.4, quality("text/html;level=2", accept)) + self.assertEqual(0.7, quality("text/html;level=3", accept)) + + def test_best_match(self): + mime_types_supported = ['application/xbel+xml', 'application/xml'] + # direct match + self.assertEqual(best_match(mime_types_supported, 'application/xbel+xml'), 'application/xbel+xml') + # direct match with a q parameter + self.assertEqual(best_match(mime_types_supported, 'application/xbel+xml; q=1'), 'application/xbel+xml') + # direct match of our second choice with a q parameter + self.assertEqual(best_match(mime_types_supported, 'application/xml; q=1'), 'application/xml') + # match using a subtype wildcard + self.assertEqual(best_match(mime_types_supported, 'application/*; q=1'), 'application/xml') + # match using a type wildcard + self.assertEqual(best_match(mime_types_supported, '*/*'), 'application/xml') + + mime_types_supported = ['application/xbel+xml', 'text/xml'] + # match using a type versus a lower weighted subtype + self.assertEqual(best_match(mime_types_supported, 'text/*;q=0.5,*/*; q=0.1'), 'text/xml') + # fail to match anything + self.assertEqual(best_match(mime_types_supported, 'text/html,application/atom+xml; q=0.9'), '') + + def test_support_wildcards(self): + mime_types_supported = ['image/*', 'application/xml'] + # match using a type wildcard + self.assertEqual(best_match(mime_types_supported, 'image/png'), 'image/*') + # match using a wildcard for both requested and supported + self.assertEqual(best_match(mime_types_supported, 'image/*'), 'image/*') + + unittest.main() \ No newline at end of file diff --git a/django_authopenid/models.py b/django_authopenid/models.py new file mode 100644 index 00000000..9826c452 --- /dev/null +++ b/django_authopenid/models.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +from django.conf import settings +from django.contrib.auth.models import User +from django.db import models + +import md5, random, sys, os, time + +__all__ = ['Nonce', 'Association', 'UserAssociation', + 'UserPasswordQueueManager', 'UserPasswordQueue'] + +class Nonce(models.Model): + """ openid nonce """ + server_url = models.CharField(max_length=255) + timestamp = models.IntegerField() + salt = models.CharField(max_length=40) + + def __unicode__(self): + return u"Nonce: %s" % self.id + + +class Association(models.Model): + """ association openid url and lifetime """ + server_url = models.TextField(max_length=2047) + handle = models.CharField(max_length=255) + secret = models.TextField(max_length=255) # Stored base64 encoded + issued = models.IntegerField() + lifetime = models.IntegerField() + assoc_type = models.TextField(max_length=64) + + def __unicode__(self): + return u"Association: %s, %s" % (self.server_url, self.handle) + +class UserAssociation(models.Model): + """ + model to manage association between openid and user + """ + openid_url = models.CharField(blank=False, max_length=255) + user = models.ForeignKey(User, unique=True) + + def __unicode__(self): + return "Openid %s with user %s" % (self.openid_url, self.user) + +class UserPasswordQueueManager(models.Manager): + """ manager for UserPasswordQueue object """ + def get_new_confirm_key(self): + "Returns key that isn't being used." + # The random module is seeded when this Apache child is created. + # Use SECRET_KEY as added salt. + while 1: + confirm_key = md5.new("%s%s%s%s" % ( + random.randint(0, sys.maxint - 1), os.getpid(), + time.time(), settings.SECRET_KEY)).hexdigest() + try: + self.get(confirm_key=confirm_key) + except self.model.DoesNotExist: + break + return confirm_key + + +class UserPasswordQueue(models.Model): + """ + model for new password queue. + """ + user = models.ForeignKey(User, unique=True) + new_password = models.CharField(max_length=30) + confirm_key = models.CharField(max_length=40) + + objects = UserPasswordQueueManager() + + def __unicode__(self): + return self.user.username diff --git a/django_authopenid/urls.py b/django_authopenid/urls.py new file mode 100644 index 00000000..1ab65941 --- /dev/null +++ b/django_authopenid/urls.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from django.conf.urls.defaults import patterns, url +from django.utils.translation import ugettext as _ + +urlpatterns = patterns('django_authopenid.views', + # yadis rdf + url(r'^yadis.xrdf$', 'xrdf', name='yadis_xrdf'), + # manage account registration + url(r'^%s$' % _('signin/'), 'signin', name='user_signin'), + url(r'^%s$' % _('signout/'), 'signout', name='user_signout'), + url(r'^%s%s$' % (_('signin/'), _('complete/')), 'complete_signin', + name='user_complete_signin'), + url(r'^%s$' % _('register/'), 'register', name='user_register'), + url(r'^%s$' % _('signup/'), 'signup', name='user_signup'), + #disable current sendpw function + url(r'^%s$' % _('sendpw/'), 'signin', name='user_sendpw'), + #url(r'^%s$' % _('sendpw/'), 'sendpw', name='user_sendpw'), + #url(r'^%s%s$' % (_('password/'), _('confirm/')), 'confirmchangepw', + # name='user_confirmchangepw'), + + # manage account settings + #url(r'^$', 'account_settings', name='user_account_settings'), + #url(r'^%s$' % _('password/'), 'changepw', name='user_changepw'), + #url(r'^%s$' % _('email/'), 'changeemail', name='user_changeemail'), + #url(r'^%s$' % _('openid/'), 'changeopenid', name='user_changeopenid'), + url(r'^%s$' % _('delete/'), 'delete', name='user_delete'), +) diff --git a/django_authopenid/util.py b/django_authopenid/util.py new file mode 100644 index 00000000..841a81c7 --- /dev/null +++ b/django_authopenid/util.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +from openid.store.interface import OpenIDStore +from openid.association import Association as OIDAssociation +from openid.extensions import sreg +import openid.store + +from django.db.models.query import Q +from django.conf import settings +from django.http import str_to_unicode + + +# needed for some linux distributions like debian +try: + from openid.yadis import xri +except: + from yadis import xri + +import time, base64, md5, operator +import urllib + +from models import Association, Nonce + +__all__ = ['OpenID', 'DjangoOpenIDStore', 'from_openid_response', 'clean_next'] + +DEFAULT_NEXT = getattr(settings, 'OPENID_REDIRECT_NEXT', '/') +def clean_next(next): + if next is None: + return DEFAULT_NEXT + next = str_to_unicode(urllib.unquote(next), 'utf-8') + next = next.strip() + if next.startswith('/'): + return next + return DEFAULT_NEXT + +class OpenID: + def __init__(self, openid_, issued, attrs=None, sreg_=None): + self.openid = openid_ + self.issued = issued + self.attrs = attrs or {} + self.sreg = sreg_ or {} + self.is_iname = (xri.identifierScheme(openid_) == 'XRI') + + def __repr__(self): + return '' % self.openid + + def __str__(self): + return self.openid + +class DjangoOpenIDStore(OpenIDStore): + def __init__(self): + self.max_nonce_age = 6 * 60 * 60 # Six hours + + def storeAssociation(self, server_url, association): + assoc = Association( + server_url = server_url, + handle = association.handle, + secret = base64.encodestring(association.secret), + issued = association.issued, + lifetime = association.issued, + assoc_type = association.assoc_type + ) + assoc.save() + + def getAssociation(self, server_url, handle=None): + assocs = [] + if handle is not None: + assocs = Association.objects.filter( + server_url = server_url, handle = handle + ) + else: + assocs = Association.objects.filter( + server_url = server_url + ) + if not assocs: + return None + associations = [] + for assoc in assocs: + association = OIDAssociation( + assoc.handle, base64.decodestring(assoc.secret), assoc.issued, + assoc.lifetime, assoc.assoc_type + ) + if association.getExpiresIn() == 0: + self.removeAssociation(server_url, assoc.handle) + else: + associations.append((association.issued, association)) + if not associations: + return None + return associations[-1][1] + + def removeAssociation(self, server_url, handle): + assocs = list(Association.objects.filter( + server_url = server_url, handle = handle + )) + assocs_exist = len(assocs) > 0 + for assoc in assocs: + assoc.delete() + return assocs_exist + + def useNonce(self, server_url, timestamp, salt): + if abs(timestamp - time.time()) > openid.store.nonce.SKEW: + return False + + query = [ + Q(server_url__exact=server_url), + Q(timestamp__exact=timestamp), + Q(salt__exact=salt), + ] + try: + ononce = Nonce.objects.get(reduce(operator.and_, query)) + except Nonce.DoesNotExist: + ononce = Nonce( + server_url=server_url, + timestamp=timestamp, + salt=salt + ) + ononce.save() + return True + + ononce.delete() + + return False + + def cleanupNonce(self): + Nonce.objects.filter(timestamp nor the names +# * of its contributors may be used to endorse or promote products +# * derived from this software without specific prior written +# * permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from django.http import HttpResponseRedirect, get_host +from django.shortcuts import render_to_response as render +from django.template import RequestContext, loader, Context +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.auth import login, logout +from django.contrib.auth.decorators import login_required +from django.core.urlresolvers import reverse +from django.utils.encoding import smart_unicode +from django.utils.html import escape +from django.utils.translation import ugettext as _ +from django.contrib.sites.models import Site +from django.utils.http import urlquote_plus +from django.core.mail import send_mail + +from openid.consumer.consumer import Consumer, \ + SUCCESS, CANCEL, FAILURE, SETUP_NEEDED +from openid.consumer.discover import DiscoveryFailure +from openid.extensions import sreg +# needed for some linux distributions like debian +try: + from openid.yadis import xri +except ImportError: + from yadis import xri + +import re +import urllib + + +from django_authopenid.util import OpenID, DjangoOpenIDStore, from_openid_response, clean_next +from django_authopenid.models import UserAssociation, UserPasswordQueue +from django_authopenid.forms import OpenidSigninForm, OpenidAuthForm, OpenidRegisterForm, \ + OpenidVerifyForm, RegistrationForm, ChangepwForm, ChangeemailForm, \ + ChangeopenidForm, DeleteForm, EmailPasswordForm + +def get_url_host(request): + if request.is_secure(): + protocol = 'https' + else: + protocol = 'http' + host = escape(get_host(request)) + return '%s://%s' % (protocol, host) + +def get_full_url(request): + return get_url_host(request) + request.get_full_path() + + + +def ask_openid(request, openid_url, redirect_to, on_failure=None, + sreg_request=None): + """ basic function to ask openid and return response """ + request.encoding = 'UTF-8' + on_failure = on_failure or signin_failure + + trust_root = getattr( + settings, 'OPENID_TRUST_ROOT', get_url_host(request) + '/' + ) + if xri.identifierScheme(openid_url) == 'XRI' and getattr( + settings, 'OPENID_DISALLOW_INAMES', False + ): + msg = _("i-names are not supported") + return on_failure(request, msg) + consumer = Consumer(request.session, DjangoOpenIDStore()) + try: + auth_request = consumer.begin(openid_url) + except DiscoveryFailure: + msg = _(u"非法OpenID地址: %s" % openid_url) + return on_failure(request, msg) + + if sreg_request: + auth_request.addExtension(sreg_request) + redirect_url = auth_request.redirectURL(trust_root, redirect_to) + return HttpResponseRedirect(redirect_url) + +def complete(request, on_success=None, on_failure=None, return_to=None): + """ complete openid signin """ + on_success = on_success or default_on_success + on_failure = on_failure or default_on_failure + + consumer = Consumer(request.session, DjangoOpenIDStore()) + # make sure params are encoded in utf8 + params = dict((k,smart_unicode(v)) for k, v in request.GET.items()) + openid_response = consumer.complete(params, return_to) + + + if openid_response.status == SUCCESS: + return on_success(request, openid_response.identity_url, + openid_response) + elif openid_response.status == CANCEL: + return on_failure(request, 'The request was canceled') + elif openid_response.status == FAILURE: + return on_failure(request, openid_response.message) + elif openid_response.status == SETUP_NEEDED: + return on_failure(request, 'Setup needed') + else: + assert False, "Bad openid status: %s" % openid_response.status + +def default_on_success(request, identity_url, openid_response): + """ default action on openid signin success """ + request.session['openid'] = from_openid_response(openid_response) + return HttpResponseRedirect(clean_next(request.GET.get('next'))) + +def default_on_failure(request, message): + """ default failure action on signin """ + return render('openid_failure.html', { + 'message': message + }) + + +def not_authenticated(func): + """ decorator that redirect user to next page if + he is already logged.""" + def decorated(request, *args, **kwargs): + if request.user.is_authenticated(): + next = request.GET.get("next", "/") + return HttpResponseRedirect(next) + return func(request, *args, **kwargs) + return decorated + +@not_authenticated +def signin(request): + """ + signin page. It manage the legacy authentification (user/password) + and authentification with openid. + + url: /signin/ + + template : authopenid/signin.htm + """ + request.encoding = 'UTF-8' + on_failure = signin_failure + next = clean_next(request.GET.get('next')) + + form_signin = OpenidSigninForm(initial={'next':next}) + form_auth = OpenidAuthForm(initial={'next':next}) + + if request.POST: + + if 'bsignin' in request.POST.keys(): + + form_signin = OpenidSigninForm(request.POST) + if form_signin.is_valid(): + next = clean_next(form_signin.cleaned_data.get('next')) + sreg_req = sreg.SRegRequest(optional=['nickname', 'email']) + redirect_to = "%s%s?%s" % ( + get_url_host(request), + reverse('user_complete_signin'), + urllib.urlencode({'next':next}) + ) + + return ask_openid(request, + form_signin.cleaned_data['openid_url'], + redirect_to, + on_failure=signin_failure, + sreg_request=sreg_req) + + elif 'blogin' in request.POST.keys(): + # perform normal django authentification + form_auth = OpenidAuthForm(request.POST) + if form_auth.is_valid(): + user_ = form_auth.get_user() + login(request, user_) + next = clean_next(form_auth.cleaned_data.get('next')) + return HttpResponseRedirect(next) + + + return render('authopenid/signin.html', { + 'form1': form_auth, + 'form2': form_signin, + 'msg': request.GET.get('msg',''), + 'sendpw_url': reverse('user_sendpw'), + }, context_instance=RequestContext(request)) + +def complete_signin(request): + """ in case of complete signin with openid """ + return complete(request, signin_success, signin_failure, + get_url_host(request) + reverse('user_complete_signin')) + + +def signin_success(request, identity_url, openid_response): + """ + openid signin success. + + If the openid is already registered, the user is redirected to + url set par next or in settings with OPENID_REDIRECT_NEXT variable. + If none of these urls are set user is redirectd to /. + + if openid isn't registered user is redirected to register page. + """ + + openid_ = from_openid_response(openid_response) + request.session['openid'] = openid_ + try: + rel = UserAssociation.objects.get(openid_url__exact = str(openid_)) + except: + # try to register this new user + return register(request) + user_ = rel.user + if user_.is_active: + user_.backend = "django.contrib.auth.backends.ModelBackend" + login(request, user_) + + next = clean_next(request.GET.get('next')) + return HttpResponseRedirect(next) + +def is_association_exist(openid_url): + """ test if an openid is already in database """ + is_exist = True + try: + uassoc = UserAssociation.objects.get(openid_url__exact = openid_url) + except: + is_exist = False + return is_exist + +@not_authenticated +def register(request): + """ + register an openid. + + If user is already a member he can associate its openid with + its account. + + A new account could also be created and automaticaly associated + to the openid. + + url : /complete/ + + template : authopenid/complete.html + """ + + is_redirect = False + next = clean_next(request.GET.get('next')) + openid_ = request.session.get('openid', None) + if not openid_: + return HttpResponseRedirect(reverse('user_signin') + next) + + nickname = openid_.sreg.get('nickname', '') + email = openid_.sreg.get('email', '') + + form1 = OpenidRegisterForm(initial={ + 'next': next, + 'username': nickname, + 'email': email, + }) + form2 = OpenidVerifyForm(initial={ + 'next': next, + 'username': nickname, + }) + + if request.POST: + just_completed = False + if 'bnewaccount' in request.POST.keys(): + form1 = OpenidRegisterForm(request.POST) + if form1.is_valid(): + next = clean_next(form1.cleaned_data.get('next')) + is_redirect = True + tmp_pwd = User.objects.make_random_password() + user_ = User.objects.create_user(form1.cleaned_data['username'], + form1.cleaned_data['email'], tmp_pwd) + + # make association with openid + uassoc = UserAssociation(openid_url=str(openid_), + user_id=user_.id) + uassoc.save() + + # login + user_.backend = "django.contrib.auth.backends.ModelBackend" + login(request, user_) + elif 'bverify' in request.POST.keys(): + form2 = OpenidVerifyForm(request.POST) + if form2.is_valid(): + is_redirect = True + next = clean_next(form2.cleaned_data.get('next')) + user_ = form2.get_user() + + uassoc = UserAssociation(openid_url=str(openid_), + user_id=user_.id) + uassoc.save() + login(request, user_) + + # redirect, can redirect only if forms are valid. + if is_redirect: + return HttpResponseRedirect(next) + + return render('authopenid/complete.html', { + 'form1': form1, + 'form2': form2, + 'nickname': nickname, + 'email': email + }, context_instance=RequestContext(request)) + +def signin_failure(request, message): + """ + falure with openid signin. Go back to signin page. + + template : "authopenid/signin.html" + """ + next = clean_next(request.GET.get('next')) + form_signin = OpenidSigninForm(initial={'next': next}) + form_auth = OpenidAuthForm(initial={'next': next}) + + return render('authopenid/signin.html', { + 'msg': message, + 'form1': form_auth, + 'form2': form_signin, + }, context_instance=RequestContext(request)) + +@not_authenticated +def signup(request): + """ + signup page. Create a legacy account + + url : /signup/" + + templates: authopenid/signup.html, authopenid/confirm_email.txt + """ + action_signin = reverse('user_signin') + next = clean_next(request.GET.get('next')) + form = RegistrationForm(initial={'next':next}) + form_signin = OpenidSigninForm(initial={'next':next}) + + if request.POST: + form = RegistrationForm(request.POST) + if form.is_valid(): + next = clean_next(form.cleaned_data.get('next')) + user_ = User.objects.create_user( form.cleaned_data['username'], + form.cleaned_data['email'], form.cleaned_data['password1']) + + user_.backend = "django.contrib.auth.backends.ModelBackend" + login(request, user_) + + # send email + current_domain = Site.objects.get_current().domain + subject = _("Welcome") + message_template = loader.get_template( + 'authopenid/confirm_email.txt' + ) + message_context = Context({ + 'site_url': 'http://%s/' % current_domain, + 'username': form.cleaned_data['username'], + 'password': form.cleaned_data['password1'] + }) + message = message_template.render(message_context) + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, + [user_.email]) + + return HttpResponseRedirect(next) + + return render('authopenid/signup.html', { + 'form': form, + 'form2': form_signin, + }, context_instance=RequestContext(request)) + +@login_required +def signout(request): + """ + signout from the website. Remove openid from session and kill it. + + url : /signout/" + """ + try: + del request.session['openid'] + except KeyError: + pass + next = clean_next(request.GET.get('next')) + logout(request) + + return HttpResponseRedirect(next) + +def xrdf(request): + url_host = get_url_host(request) + return_to = [ + "%s%s" % (url_host, reverse('user_complete_signin')) + ] + return render('authopenid/yadis.xrdf', { + 'return_to': return_to + }, context_instance=RequestContext(request)) + +@login_required +def account_settings(request): + """ + index pages to changes some basic account settings : + - change password + - change email + - associate a new openid + - delete account + + url : / + + template : authopenid/settings.html + """ + msg = request.GET.get('msg', '') + is_openid = True + + try: + uassoc = UserAssociation.objects.get( + user__username__exact=request.user.username + ) + except: + is_openid = False + + + return render('authopenid/settings.html', { + 'msg': msg, + 'is_openid': is_openid + }, context_instance=RequestContext(request)) + +@login_required +def changepw(request): + """ + change password view. + + url : /changepw/ + template: authopenid/changepw.html + """ + + user_ = request.user + + if request.POST: + form = ChangepwForm(request.POST, user=user_) + if form.is_valid(): + user_.set_password(form.cleaned_data['password1']) + user_.save() + msg = _("Password changed.") + redirect = "%s?msg=%s" % ( + reverse('user_account_settings'), + urlquote_plus(msg)) + return HttpResponseRedirect(redirect) + else: + form = ChangepwForm(user=user_) + + return render('authopenid/changepw.html', {'form': form }, + context_instance=RequestContext(request)) + +@login_required +def changeemail(request): + """ + changeemail view. It require password or openid to allow change. + + url: /changeemail/ + + template : authopenid/changeemail.html + """ + msg = request.GET.get('msg', '') + extension_args = {} + user_ = request.user + + redirect_to = get_url_host(request) + reverse('user_changeemail') + + if request.POST: + form = ChangeemailForm(request.POST, user=user_) + if form.is_valid(): + if not form.test_openid: + user_.email = form.cleaned_data['email'] + user_.save() + msg = _("Email changed.") + redirect = "%s?msg=%s" % (reverse('user_account_settings'), + urlquote_plus(msg)) + return HttpResponseRedirect(redirect) + else: + request.session['new_email'] = form.cleaned_data['email'] + return ask_openid(request, form.cleaned_data['password'], + redirect_to, on_failure=emailopenid_failure) + elif not request.POST and 'openid.mode' in request.GET: + return complete(request, emailopenid_success, + emailopenid_failure, redirect_to) + else: + form = ChangeemailForm(initial={'email': user_.email}, + user=user_) + + return render('authopenid/changeemail.html', { + 'form': form, + 'msg': msg + }, context_instance=RequestContext(request)) + + +def emailopenid_success(request, identity_url, openid_response): + openid_ = from_openid_response(openid_response) + + user_ = request.user + try: + uassoc = UserAssociation.objects.get( + openid_url__exact=identity_url + ) + except: + return emailopenid_failure(request, + _("No OpenID %s found associated in our database" % identity_url)) + + if uassoc.user.username != request.user.username: + return emailopenid_failure(request, + _("The OpenID %s isn't associated to current user logged in" % + identity_url)) + + new_email = request.session.get('new_email', '') + if new_email: + user_.email = new_email + user_.save() + del request.session['new_email'] + msg = _("Email Changed.") + + redirect = "%s?msg=%s" % (reverse('user_account_settings'), + urlquote_plus(msg)) + return HttpResponseRedirect(redirect) + + +def emailopenid_failure(request, message): + redirect_to = "%s?msg=%s" % ( + reverse('user_changeemail'), urlquote_plus(message)) + return HttpResponseRedirect(redirect_to) + +@login_required +def changeopenid(request): + """ + change openid view. Allow user to change openid + associated to its username. + + url : /changeopenid/ + + template: authopenid/changeopenid.html + """ + + extension_args = {} + openid_url = '' + has_openid = True + msg = request.GET.get('msg', '') + + user_ = request.user + + try: + uopenid = UserAssociation.objects.get(user=user_) + openid_url = uopenid.openid_url + except: + has_openid = False + + redirect_to = get_url_host(request) + reverse('user_changeopenid') + if request.POST and has_openid: + form = ChangeopenidForm(request.POST, user=user_) + if form.is_valid(): + return ask_openid(request, form.cleaned_data['openid_url'], + redirect_to, on_failure=changeopenid_failure) + elif not request.POST and has_openid: + if 'openid.mode' in request.GET: + return complete(request, changeopenid_success, + changeopenid_failure, redirect_to) + + form = ChangeopenidForm(initial={'openid_url': openid_url }, user=user_) + return render('authopenid/changeopenid.html', { + 'form': form, + 'has_openid': has_openid, + 'msg': msg + }, context_instance=RequestContext(request)) + +def changeopenid_success(request, identity_url, openid_response): + openid_ = from_openid_response(openid_response) + is_exist = True + try: + uassoc = UserAssociation.objects.get(openid_url__exact=identity_url) + except: + is_exist = False + + if not is_exist: + try: + uassoc = UserAssociation.objects.get( + user__username__exact=request.user.username + ) + uassoc.openid_url = identity_url + uassoc.save() + except: + uassoc = UserAssociation(user=request.user, + openid_url=identity_url) + uassoc.save() + elif uassoc.user.username != request.user.username: + return changeopenid_failure(request, + _('This OpenID is already associated with another account.')) + + request.session['openids'] = [] + request.session['openids'].append(openid_) + + msg = _("OpenID %s is now associated with your account." % identity_url) + redirect = "%s?msg=%s" % ( + reverse('user_account_settings'), + urlquote_plus(msg)) + return HttpResponseRedirect(redirect) + + +def changeopenid_failure(request, message): + redirect_to = "%s?msg=%s" % ( + reverse('user_changeopenid'), + urlquote_plus(message)) + return HttpResponseRedirect(redirect_to) + +@login_required +def delete(request): + """ + delete view. Allow user to delete its account. Password/openid are required to + confirm it. He should also check the confirm checkbox. + + url : /delete + + template : authopenid/delete.html + """ + + extension_args = {} + + user_ = request.user + + redirect_to = get_url_host(request) + reverse('user_delete') + if request.POST: + form = DeleteForm(request.POST, user=user_) + if form.is_valid(): + if not form.test_openid: + user_.delete() + return signout(request) + else: + return ask_openid(request, form.cleaned_data['password'], + redirect_to, on_failure=deleteopenid_failure) + elif not request.POST and 'openid.mode' in request.GET: + return complete(request, deleteopenid_success, deleteopenid_failure, + redirect_to) + + form = DeleteForm(user=user_) + + msg = request.GET.get('msg','') + return render('authopenid/delete.html', { + 'form': form, + 'msg': msg, + }, context_instance=RequestContext(request)) + +def deleteopenid_success(request, identity_url, openid_response): + openid_ = from_openid_response(openid_response) + + user_ = request.user + try: + uassoc = UserAssociation.objects.get( + openid_url__exact=identity_url + ) + except: + return deleteopenid_failure(request, + _("No OpenID %s found associated in our database" % identity_url)) + + if uassoc.user.username == user_.username: + user_.delete() + return signout(request) + else: + return deleteopenid_failure(request, + _("The OpenID %s isn't associated to current user logged in" % + identity_url)) + + msg = _("Account deleted.") + redirect = "/?msg=%s" % (urlquote_plus(msg)) + return HttpResponseRedirect(redirect) + + +def deleteopenid_failure(request, message): + redirect_to = "%s?msg=%s" % (reverse('user_delete'), urlquote_plus(message)) + return HttpResponseRedirect(redirect_to) + + +def sendpw(request): + """ + send a new password to the user. It return a mail with + a new pasword and a confirm link in. To activate the + new password, the user should click on confirm link. + + url : /sendpw/ + + templates : authopenid/sendpw_email.txt, authopenid/sendpw.html + """ + + msg = request.GET.get('msg','') + if request.POST: + form = EmailPasswordForm(request.POST) + if form.is_valid(): + new_pw = User.objects.make_random_password() + confirm_key = UserPasswordQueue.objects.get_new_confirm_key() + try: + uqueue = UserPasswordQueue.objects.get( + user=form.user_cache + ) + except: + uqueue = UserPasswordQueue( + user=form.user_cache + ) + uqueue.new_password = new_pw + uqueue.confirm_key = confirm_key + uqueue.save() + # send email + current_domain = Site.objects.get_current().domain + subject = _("Request for new password") + message_template = loader.get_template( + 'authopenid/sendpw_email.txt') + message_context = Context({ + 'site_url': 'http://%s' % current_domain, + 'confirm_key': confirm_key, + 'username': form.user_cache.username, + 'password': new_pw, + 'url_confirm': reverse('user_confirmchangepw'), + }) + message = message_template.render(message_context) + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, + [form.user_cache.email]) + msg = _("A new password has been sent to your email address.") + else: + form = EmailPasswordForm() + + return render('authopenid/sendpw.html', { + 'form': form, + 'msg': msg + }, context_instance=RequestContext(request)) + + +def confirmchangepw(request): + """ + view to set new password when the user click on confirm link + in its mail. Basically it check if the confirm key exist, then + replace old password with new password and remove confirm + ley from the queue. Then it redirect the user to signin + page. + + url : /sendpw/confirm/?key + + """ + confirm_key = request.GET.get('key', '') + if not confirm_key: + return HttpResponseRedirect('/') + + try: + uqueue = UserPasswordQueue.objects.get( + confirm_key__exact=confirm_key + ) + except: + msg = _("Could not change password. Confirmation key '%s'\ + is not registered." % confirm_key) + redirect = "%s?msg=%s" % ( + reverse('user_sendpw'), urlquote_plus(msg)) + return HttpResponseRedirect(redirect) + + try: + user_ = User.objects.get(id=uqueue.user.id) + except: + msg = _("Can not change password. User don't exist anymore \ + in our database.") + redirect = "%s?msg=%s" % (reverse('user_sendpw'), + urlquote_plus(msg)) + return HttpResponseRedirect(redirect) + + user_.set_password(uqueue.new_password) + user_.save() + uqueue.delete() + msg = _("Password changed for %s. You may now sign in." % + user_.username) + redirect = "%s?msg=%s" % (reverse('user_signin'), + urlquote_plus(msg)) + + return HttpResponseRedirect(redirect) -- cgit v1.2.3-1-g7c22