Commits

bchesneau  committed 076cbb5

[svn r78] add xrds document for realm identification and other stuff described here :
http://www.intertwingly.net/blog/2007/01/03/OpenID-for-non-SuperUsers

It should also solve issue #37. To have it working just add OpenID middleware to your list of middleware.

  • Participants
  • Parent commits 8a10024

Comments (0)

Files changed (8)

File django_authopenid/middleware.py

 # -*- coding: utf-8 -*-
+from django_authopenid import mimeparse
+from django.http import HttpResponseRedirect
+from django.core.urlresolvers import reverse
 
 __all__ = ["OpenIDMiddleware"]
 
     """
     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

File django_authopenid/mimeparse.py

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

File django_authopenid/templates/authopenid/yadis.xrdf

+<?xml version='1.0' encoding='UTF-8'?>
+<xrds:XRDS
+   xmlns:xrds='xri://$xrds'
+   xmlns:openid='http://openid.net/xmlns/1.0'
+   xmlns='xri://$xrd*($v*2.0)'>
+ <XRD>
+   <Service>
+     <Type>http://specs.openid.net/auth/2.0/return_to</Type>
+     {% for uri in return_to %}
+        <URI>{{ uri }}</URI>
+     {% endfor %}
+   </Service>
+ </XRD>
+</xrds:XRDS>

File django_authopenid/urls.py

 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'),

File django_authopenid/views.py

 import urllib
 
 
-from util import OpenID, DjangoOpenIDStore, from_openid_response
-from models import UserAssociation, UserPasswordQueue
-from forms import OpenidSigninForm, OpenidAuthForm, OpenidRegisterForm, \
+from django_authopenid.util import OpenID, DjangoOpenIDStore, from_openid_response
+from django_authopenid.models import UserAssociation, UserPasswordQueue
+from django_authopenid.forms import OpenidSigninForm, OpenidAuthForm, OpenidRegisterForm, \
         OpenidVerifyForm, RegistrationForm, ChangepwForm, ChangeemailForm, \
         ChangeopenidForm, DeleteForm, EmailPasswordForm
 
 def ask_openid(request, openid_url, redirect_to, on_failure=None,
         sreg_request=None):
     """ basic function to ask openid and return response """
-
     on_failure = on_failure or signin_failure
     
     trust_root = getattr(
     if request.POST:
         form = RegistrationForm(request.POST)
         if form.is_valid():
-
             next = form.cleaned_data.get('next', '')
             if not next or not is_valid_next_url(next):
                 next = getattr(settings, 'OPENID_REDIRECT_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):

File djauthaupenid_example/settings.py

     'django.contrib.sessions.middleware.SessionMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.middleware.doc.XViewMiddleware',
+    'django_authopenid.middleware.OpenIDMiddleware',
 )
 
 ROOT_URLCONF = 'djauthaupenid_example.urls'

File djauthaupenid_example/templates/home.html

+{% extends "base.html" %}
+
+{% block content %}
+    <h1>Django AuthOpenID test</h1>
+    
+    <p>To test go <a href="/account">here</a>.</p>
+{% endblock %}

File djauthaupenid_example/urls.py

 from django.conf.urls.defaults import *
 
+
 urlpatterns = patterns('',
+    (r'^$', 'django.views.generic.simple.direct_to_template', {'template': 'home.html'}),
     (r'^account/', include('django_authopenid.urls')),
     #(r'^admin/', include('django.contrib.admin.urls')),
 )