[snowy] Update django-piston to 247:72a636fb7f12 (OAuth 1.0a)



commit d525901789cdf92cd6259c13fcccc2f82b8cfbf2
Author: Olivier Le Thanh Duong <olivier lethanh be>
Date:   Tue Jun 1 03:12:59 2010 +0200

    Update django-piston to 247:72a636fb7f12 (OAuth 1.0a)
    
    This version of django-piston implement OAuth 1.0a
    CAUTION : This update will break existing installation since
    it add a new column in the token model
    
    https://bugzilla.gnome.org/show_bug.cgi?id=620126

 lib/piston/authentication.py    |   36 +++-
 lib/piston/doc.py               |    2 +-
 lib/piston/emitters.py          |   45 +++-
 lib/piston/fixtures/models.json |   46 +++++
 lib/piston/fixtures/oauth.json  |   27 +++
 lib/piston/forms.py             |    2 +-
 lib/piston/handler.py           |   49 +++--
 lib/piston/models.py            |  136 ++++++++-----
 lib/piston/oauth.py             |  426 +++++++++++++++++++++++++--------------
 lib/piston/resource.py          |  218 +++++++++++++-------
 lib/piston/signals.py           |   14 ++
 lib/piston/store.py             |   11 +-
 lib/piston/test.py              |   62 ++++++
 lib/piston/tests.py             |  175 ++++++++++++++++
 lib/piston/utils.py             |  145 +++++++++++---
 lib/piston/validate_jsonp.py    |  210 +++++++++++++++++++
 16 files changed, 1252 insertions(+), 352 deletions(-)
---
diff --git a/lib/piston/authentication.py b/lib/piston/authentication.py
index c0bd1cc..7d09707 100644
--- a/lib/piston/authentication.py
+++ b/lib/piston/authentication.py
@@ -60,7 +60,7 @@ class HttpBasicAuthentication(object):
         
         request.user = self.auth_func(username=username, password=password) \
             or AnonymousUser()
-        
+                
         return not request.user in (False, None, AnonymousUser())
         
     def challenge(self):
@@ -69,6 +69,20 @@ class HttpBasicAuthentication(object):
         resp.status_code = 401
         return resp
 
+    def __repr__(self):
+        return u'<HTTPBasic: realm=%s>' % self.realm
+
+class HttpBasicSimple(HttpBasicAuthentication):
+    def __init__(self, realm, username, password):
+        self.user = User.objects.get(username=username)
+        self.password = password
+
+        super(HttpBasicSimple, self).__init__(auth_func=self.hash, realm=realm)
+    
+    def hash(self, username, password):
+        if username == self.user.username and password == self.password:
+            return self.user
+
 def load_data_store():
     '''Load data store for OAuth Consumers, Tokens, Nonces and Resources
     '''
@@ -97,9 +111,19 @@ def initialize_server_request(request):
     """
     Shortcut for initialization.
     """
+    if request.method == "POST": #and \
+#       request.META['CONTENT_TYPE'] == "application/x-www-form-urlencoded":
+        params = dict(request.REQUEST.items())
+    else:
+        params = { }
+
+    # Seems that we want to put HTTP_AUTHORIZATION into 'Authorization'
+    # for oauth.py to understand. Lovely.
+    request.META['Authorization'] = request.META.get('HTTP_AUTHORIZATION', '')
+
     oauth_request = oauth.OAuthRequest.from_request(
         request.method, request.build_absolute_uri(), 
-        headers=request.META, parameters=dict(request.REQUEST.items()),
+        headers=request.META, parameters=params,
         query_string=request.environ.get('QUERY_STRING', ''))
         
     if oauth_request:
@@ -143,8 +167,8 @@ def oauth_request_token(request):
 def oauth_auth_view(request, token, callback, params):
     form = forms.OAuthAuthenticationForm(initial={
         'oauth_token': token.key,
-        'oauth_callback': callback,
-        })
+        'oauth_callback': token.get_callback_url() or callback,
+      })
 
     return render_to_response('piston/authorize_token.html',
             { 'form': form }, RequestContext(request))
@@ -165,7 +189,7 @@ def oauth_user_auth(request):
         callback = oauth_server.get_callback(oauth_request)
     except:
         callback = None
-        
+    
     if request.method == "GET":
         params = oauth_request.get_normalized_parameters()
 
@@ -182,6 +206,7 @@ def oauth_user_auth(request):
                 args = '?'+token.to_string(only_key=True)
             else:
                 args = '?error=%s' % 'Access not granted by user.'
+                print "FORM ERROR", form.errors
             
             if not callback:
                 callback = getattr(settings, 'OAUTH_CALLBACK_VIEW')
@@ -235,6 +260,7 @@ class OAuthAuthentication(object):
 
             if consumer and token:
                 request.user = token.user
+                request.consumer = consumer
                 request.throttle_extra = token.consumer.id
                 return True
             
diff --git a/lib/piston/doc.py b/lib/piston/doc.py
index 63f89ec..08b3343 100644
--- a/lib/piston/doc.py
+++ b/lib/piston/doc.py
@@ -105,7 +105,7 @@ class HandlerDocumentation(object):
         
     @property
     def is_anonymous(self):
-        return handler.is_anonymous
+        return self.handler.is_anonymous
 
     def get_model(self):
         return getattr(self, 'model', None)
diff --git a/lib/piston/emitters.py b/lib/piston/emitters.py
index 92db0a7..912228e 100644
--- a/lib/piston/emitters.py
+++ b/lib/piston/emitters.py
@@ -31,6 +31,7 @@ from django.http import HttpResponse
 from django.core import serializers
 
 from utils import HttpStatusCode, Mimer
+from validate_jsonp import is_valid_jsonp_callback_value
 
 try:
     import cStringIO as StringIO
@@ -52,8 +53,15 @@ class Emitter(object):
     conveniently returns a serialized `dict`. This is
     usually the only method you want to use in your
     emitter. See below for examples.
+
+    `RESERVED_FIELDS` was introduced when better resource
+    method detection came, and we accidentially caught these
+    as the methods on the handler. Issue58 says that's no good.
     """
     EMITTERS = { }
+    RESERVED_FIELDS = set([ 'read', 'update', 'create', 
+                            'delete', 'model', 'anonymous',
+                            'allowed_methods', 'fields', 'exclude' ])
 
     def __init__(self, payload, typemapper, handler, fields=(), anonymous=True):
         self.typemapper = typemapper
@@ -65,17 +73,18 @@ class Emitter(object):
         if isinstance(self.data, Exception):
             raise
     
-    def method_fields(self, data, fields):
-        if not data:
+    def method_fields(self, handler, fields):
+        if not handler:
             return { }
 
-        has = dir(data)
         ret = dict()
             
-        for field in fields:
-            if field in has and callable(field):
-                ret[field] = getattr(data, field)
-        
+        for field in fields - Emitter.RESERVED_FIELDS:
+            t = getattr(handler, str(field), None)
+
+            if t and callable(t):
+                ret[field] = t
+
         return ret
     
     def construct(self):
@@ -111,6 +120,8 @@ class Emitter(object):
                 f = thing.__emittable__
                 if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
                     ret = _any(f())
+            elif repr(thing).startswith("<django.db.models.fields.related.RelatedManager"):
+                ret = _any(thing.all())
             else:
                 ret = smart_unicode(thing, strings_only=True)
 
@@ -176,7 +187,7 @@ class Emitter(object):
                     get_fields = set(fields)
 
                 met_fields = self.method_fields(handler, get_fields)
-                
+                           
                 for f in data._meta.local_fields:
                     if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
                         if not f.rel:
@@ -370,11 +381,11 @@ class JSONEmitter(Emitter):
     JSON emitter, understands timestamps.
     """
     def render(self, request):
-        cb = request.GET.get('callback')
+        cb = request.GET.get('callback', None)
         seria = simplejson.dumps(self.construct(), cls=DateTimeAwareJSONEncoder, ensure_ascii=False, indent=4)
 
         # Callback
-        if cb:
+        if cb and is_valid_jsonp_callback_value(cb):
             return '%s(%s)' % (cb, seria)
 
         return seria
@@ -392,7 +403,7 @@ class YAMLEmitter(Emitter):
 
 if yaml:  # Only register yaml if it was import successfully.
     Emitter.register('yaml', YAMLEmitter, 'application/x-yaml; charset=utf-8')
-    Mimer.register(yaml.load, ('application/x-yaml',))
+    Mimer.register(lambda s: dict(yaml.load(s)), ('application/x-yaml',))
 
 class PickleEmitter(Emitter):
     """
@@ -402,7 +413,17 @@ class PickleEmitter(Emitter):
         return pickle.dumps(self.construct())
         
 Emitter.register('pickle', PickleEmitter, 'application/python-pickle')
-Mimer.register(pickle.loads, ('application/python-pickle',))
+
+"""
+WARNING: Accepting arbitrary pickled data is a huge security concern.
+The unpickler has been disabled by default now, and if you want to use
+it, please be aware of what implications it will have.
+
+Read more: http://nadiana.com/python-pickle-insecure
+
+Uncomment the line below to enable it. You're doing so at your own risk.
+"""
+# Mimer.register(pickle.loads, ('application/python-pickle',))
 
 class DjangoEmitter(Emitter):
     """
diff --git a/lib/piston/fixtures/models.json b/lib/piston/fixtures/models.json
new file mode 100644
index 0000000..9520005
--- /dev/null
+++ b/lib/piston/fixtures/models.json
@@ -0,0 +1,46 @@
+[
+    {
+        "pk": 2, 
+        "model": "auth.user", 
+        "fields": {
+            "username": "pistontestuser", 
+            "first_name": "Piston", 
+            "last_name": "User", 
+            "is_active": true, 
+            "is_superuser": false, 
+            "is_staff": false, 
+            "last_login": "2009-08-03 13:11:53", 
+            "groups": [], 
+            "user_permissions": [], 
+            "password": "sha1$b6c1f$83d5879f3854f6e9d27f393e3bcb4b8db05cf671", 
+            "email": "pistontestuser example com", 
+            "date_joined": "2009-08-03 13:11:53"
+        }
+    },
+    {
+        "pk": 3, 
+        "model": "auth.user", 
+        "fields": {
+            "username": "pistontestconsumer", 
+            "first_name": "Piston", 
+            "last_name": "Consumer", 
+            "is_active": true, 
+            "is_superuser": false, 
+            "is_staff": false, 
+            "last_login": "2009-08-03 13:11:53", 
+            "groups": [], 
+            "user_permissions": [], 
+            "password": "sha1$b6c1f$83d5879f3854f6e9d27f393e3bcb4b8db05cf671", 
+            "email": "pistontestconsumer example com", 
+            "date_joined": "2009-08-03 13:11:53"
+        }
+    },
+    {
+        "pk": 1, 
+        "model": "sites.site", 
+        "fields": {
+            "domain": "example.com", 
+            "name": "example.com"
+        }
+    }
+]
diff --git a/lib/piston/fixtures/oauth.json b/lib/piston/fixtures/oauth.json
new file mode 100644
index 0000000..1c6b913
--- /dev/null
+++ b/lib/piston/fixtures/oauth.json
@@ -0,0 +1,27 @@
+[
+    {
+        "pk": 1, 
+        "model": "piston.consumer", 
+        "fields": {
+            "status": "accepted", 
+            "name": "Piston Test Consumer", 
+            "secret": "T5XkNMkcjffDpC9mNQJbyQnJXGsenYbz", 
+            "user": 2, 
+            "key": "8aZSFj3W54h8J8sCpx", 
+            "description": "A test consumer record for Piston unit tests."
+        }
+    },
+    {
+        "pk": 1, 
+        "model": "piston.token", 
+        "fields": {
+            "is_approved": true, 
+            "timestamp": 1249347414, 
+            "token_type": 2, 
+            "secret": "qSWZq36t7yvkBquetYBkd8JxnuCu9jKk", 
+            "user": 2, 
+            "key": "Y7358vL5hDBbeP3HHL", 
+            "consumer": 1
+        }
+    }
+]
diff --git a/lib/piston/forms.py b/lib/piston/forms.py
index 0cf9d4b..351df7c 100644
--- a/lib/piston/forms.py
+++ b/lib/piston/forms.py
@@ -23,7 +23,7 @@ class ModelForm(forms.ModelForm):
 
 class OAuthAuthenticationForm(forms.Form):
     oauth_token = forms.CharField(widget=forms.HiddenInput)
-    oauth_callback = forms.CharField(widget=forms.HiddenInput)
+    oauth_callback = forms.CharField(widget=forms.HiddenInput, required=False)
     authorize_access = forms.BooleanField(required=True)
     csrf_signature = forms.CharField(widget=forms.HiddenInput)
 
diff --git a/lib/piston/handler.py b/lib/piston/handler.py
index 07e1a65..11161fe 100644
--- a/lib/piston/handler.py
+++ b/lib/piston/handler.py
@@ -1,5 +1,8 @@
+import warnings
+
 from utils import rc
 from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
+from django.conf import settings
 
 typemapper = { }
 handler_tracker = [ ]
@@ -11,12 +14,22 @@ class HandlerMetaClass(type):
     """
     def __new__(cls, name, bases, attrs):
         new_cls = type.__new__(cls, name, bases, attrs)
-        
+
+        def already_registered(model, anon):
+            for k, (m, a) in typemapper.iteritems():
+                if model == m and anon == a:
+                    return k
+
         if hasattr(new_cls, 'model'):
+            if already_registered(new_cls.model, new_cls.is_anonymous):
+                if not getattr(settings, 'PISTON_IGNORE_DUPE_MODELS', False):
+                    warnings.warn("Handler already registered for model %s, "
+                        "you may experience inconsistent results." % new_cls.model.__name__)
+
             typemapper[new_cls] = (new_cls.model, new_cls.is_anonymous)
         else:
             typemapper[new_cls] = (None, new_cls.is_anonymous)
-        
+
         if name not in ('BaseHandler', 'AnonymousBaseHandler'):
             handler_tracker.append(new_cls)
 
@@ -27,43 +40,43 @@ class BaseHandler(object):
     Basehandler that gives you CRUD for free.
     You are supposed to subclass this for specific
     functionality.
-    
+
     All CRUD methods (`read`/`update`/`create`/`delete`)
     receive a request as the first argument from the
     resource. Use this for checking `request.user`, etc.
     """
     __metaclass__ = HandlerMetaClass
-    
+
     allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
     anonymous = is_anonymous = False
     exclude = ( 'id', )
     fields =  ( )
-    
+
     def flatten_dict(self, dct):
         return dict([ (str(k), dct.get(k)) for k in dct.keys() ])
-    
+
     def has_model(self):
         return hasattr(self, 'model') or hasattr(self, 'queryset')
 
     def queryset(self, request):
         return self.model.objects.all()
-    
+
     def value_from_tuple(tu, name):
         for int_, n in tu:
             if n == name:
                 return int_
         return None
-    
+
     def exists(self, **kwargs):
         if not self.has_model():
             raise NotImplementedError
-        
+
         try:
             self.model.objects.get(**kwargs)
             return True
         except self.model.DoesNotExist:
             return False
-    
+
     def read(self, request, *args, **kwargs):
         if not self.has_model():
             return rc.NOT_IMPLEMENTED
@@ -79,13 +92,13 @@ class BaseHandler(object):
                 return rc.BAD_REQUEST
         else:
             return self.queryset(request).filter(*args, **kwargs)
-    
+
     def create(self, request, *args, **kwargs):
         if not self.has_model():
             return rc.NOT_IMPLEMENTED
-        
-        attrs = self.flatten_dict(request.POST)
-        
+
+        attrs = self.flatten_dict(request.data)
+
         try:
             inst = self.queryset(request).get(**attrs)
             return rc.DUPLICATE_ENTRY
@@ -95,7 +108,7 @@ class BaseHandler(object):
             return inst
         except self.model.MultipleObjectsReturned:
             return rc.DUPLICATE_ENTRY
-    
+
     def update(self, request, *args, **kwargs):
         if not self.has_model():
             return rc.NOT_IMPLEMENTED
@@ -113,13 +126,13 @@ class BaseHandler(object):
         except MultipleObjectsReturned: # should never happen, since we're using a PK
             return rc.BAD_REQUEST
 
-        attrs = self.flatten_dict(request.POST)
+        attrs = self.flatten_dict(request.data)
         for k,v in attrs.iteritems():
             setattr( inst, k, v )
 
         inst.save()
         return rc.ALL_OK
-    
+
     def delete(self, request, *args, **kwargs):
         if not self.has_model():
             raise NotImplementedError
@@ -134,7 +147,7 @@ class BaseHandler(object):
             return rc.DUPLICATE_ENTRY
         except self.model.DoesNotExist:
             return rc.NOT_HERE
-        
+
 class AnonymousBaseHandler(BaseHandler):
     """
     Anonymous handler.
diff --git a/lib/piston/models.py b/lib/piston/models.py
index b03eaba..9c93af8 100644
--- a/lib/piston/models.py
+++ b/lib/piston/models.py
@@ -1,19 +1,30 @@
-import urllib
+import urllib, time, urlparse
+
+# Django imports
+from django.db.models.signals import post_save, post_delete
 from django.db import models
 from django.contrib.auth.models import User
 from django.contrib import admin
-from django.conf import settings
 from django.core.mail import send_mail, mail_admins
-from django.template import loader
 
-from managers import TokenManager, ConsumerManager, ResourceManager, KEY_SIZE, SECRET_SIZE
+# Piston imports
+from managers import TokenManager, ConsumerManager, ResourceManager
+from signals import consumer_post_save, consumer_post_delete
+
+KEY_SIZE = 18
+SECRET_SIZE = 32
+VERIFIER_SIZE = 10
 
 CONSUMER_STATES = (
-    ('pending', 'Pending approval'),
+    ('pending', 'Pending'),
     ('accepted', 'Accepted'),
     ('canceled', 'Canceled'),
+    ('rejected', 'Rejected')
 )
 
+def generate_random(length=SECRET_SIZE):
+    return User.objects.make_random_password(length=length)
+
 class Nonce(models.Model):
     token_key = models.CharField(max_length=KEY_SIZE)
     consumer_key = models.CharField(max_length=KEY_SIZE)
@@ -24,18 +35,6 @@ class Nonce(models.Model):
 
 admin.site.register(Nonce)
 
-class Resource(models.Model):
-    name = models.CharField(max_length=255)
-    url = models.TextField(max_length=2047)
-    is_readonly = models.BooleanField(default=True)
-    
-    objects = ResourceManager()
-
-    def __unicode__(self):
-        return u"Resource %s with url %s" % (self.name, self.url)
-
-admin.site.register(Resource)
-
 class Consumer(models.Model):
     name = models.CharField(max_length=255)
     description = models.TextField()
@@ -51,39 +50,26 @@ class Consumer(models.Model):
     def __unicode__(self):
         return u"Consumer %s with key %s" % (self.name, self.key)
 
-    def save(self, **kwargs):
-        super(Consumer, self).save(**kwargs)
-        
-        if self.id and self.user:
-            subject = "API Consumer"
-            rcpt = [ self.user.email, ]
-
-            if self.status == "accepted":
-                template = "api/mails/consumer_accepted.txt"
-                subject += " was accepted!"
-            elif self.status == "canceled":
-                template = "api/mails/consumer_canceled.txt"
-                subject += " has been canceled"
-            else:
-                template = "api/mails/consumer_pending.txt"
-                subject += " application received"
-                
-                for admin in settings.ADMINS:
-                    bcc.append(admin[1])
-
-            body = loader.render_to_string(template, 
-                    { 'consumer': self, 'user': self.user })
-                    
-            send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, 
-                        rcpt, fail_silently=True)
-            
-            if self.status == 'pending':
-                mail_admins(subject, body, fail_silently=True)
-                        
-            if settings.DEBUG:
-                print "Mail being sent, to=%s" % rcpt
-                print "Subject: %s" % subject
-                print body
+    def generate_random_codes(self):
+        """
+        Used to generate random key/secret pairings. Use this after you've
+        added the other data in place of save(). 
+
+        c = Consumer()
+        c.name = "My consumer" 
+        c.description = "An app that makes ponies from the API."
+        c.user = some_user_object
+        c.generate_random_codes()
+        """
+        key = User.objects.make_random_password(length=KEY_SIZE)
+        secret = generate_random(SECRET_SIZE)
+
+        while Consumer.objects.filter(key__exact=key, secret__exact=secret).count():
+            secret = generate_random(SECRET_SIZE)
+
+        self.key = key
+        self.secret = secret
+        self.save()
 
 admin.site.register(Consumer)
 
@@ -94,13 +80,17 @@ class Token(models.Model):
     
     key = models.CharField(max_length=KEY_SIZE)
     secret = models.CharField(max_length=SECRET_SIZE)
+    verifier = models.CharField(max_length=VERIFIER_SIZE)
     token_type = models.IntegerField(choices=TOKEN_TYPES)
-    timestamp = models.IntegerField()
+    timestamp = models.IntegerField(default=long(time.time()))
     is_approved = models.BooleanField(default=False)
     
     user = models.ForeignKey(User, null=True, blank=True, related_name='tokens')
     consumer = models.ForeignKey(Consumer)
     
+    callback = models.CharField(max_length=255, null=True, blank=True)
+    callback_confirmed = models.BooleanField(default=False)
+    
     objects = TokenManager()
     
     def __unicode__(self):
@@ -109,10 +99,52 @@ class Token(models.Model):
     def to_string(self, only_key=False):
         token_dict = {
             'oauth_token': self.key, 
-            'oauth_token_secret': self.secret
+            'oauth_token_secret': self.secret,
+            'oauth_callback_confirmed': 'true',
         }
+
+        if self.verifier:
+            token_dict.update({ 'oauth_verifier': self.verifier })
+
         if only_key:
             del token_dict['oauth_token_secret']
+
         return urllib.urlencode(token_dict)
 
+    def generate_random_codes(self):
+        key = User.objects.make_random_password(length=KEY_SIZE)
+        secret = generate_random(SECRET_SIZE)
+
+        while Token.objects.filter(key__exact=key, secret__exact=secret).count():
+            secret = generate_random(SECRET_SIZE)
+
+        self.key = key
+        self.secret = secret
+        self.save()
+        
+    # -- OAuth 1.0a stuff
+
+    def get_callback_url(self):
+        if self.callback and self.verifier:
+            # Append the oauth_verifier.
+            parts = urlparse.urlparse(self.callback)
+            scheme, netloc, path, params, query, fragment = parts[:6]
+            if query:
+                query = '%s&oauth_verifier=%s' % (query, self.verifier)
+            else:
+                query = 'oauth_verifier=%s' % self.verifier
+            return urlparse.urlunparse((scheme, netloc, path, params,
+                query, fragment))
+        return self.callback
+    
+    def set_callback(self, callback):
+        if callback != "oob": # out of band, says "we can't do this!"
+            self.callback = callback
+            self.callback_confirmed = True
+            self.save()
+        
 admin.site.register(Token)
+
+# Attach our signals
+post_save.connect(consumer_post_save, sender=Consumer)
+post_delete.connect(consumer_post_delete, sender=Consumer)
diff --git a/lib/piston/oauth.py b/lib/piston/oauth.py
index 1e50f1c..3a42e20 100644
--- a/lib/piston/oauth.py
+++ b/lib/piston/oauth.py
@@ -1,50 +1,81 @@
+"""
+The MIT License
+
+Copyright (c) 2007 Leah Culver
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
 import cgi
 import urllib
 import time
 import random
 import urlparse
 import hmac
-import base64
+import binascii
+
 
 VERSION = '1.0' # Hi Blaine!
 HTTP_METHOD = 'GET'
 SIGNATURE_METHOD = 'PLAINTEXT'
 
-# Generic exception class
-class OAuthError(RuntimeError):
-    def get_message(self): 
-        return self._message
-
-    def set_message(self, message): 
-        self._message = message
-
-    message = property(get_message, set_message)
 
+class OAuthError(RuntimeError):
+    """Generic exception class."""
     def __init__(self, message='OAuth error occured.'):
         self.message = message
 
-# optional WWW-Authenticate header (401 error)
 def build_authenticate_header(realm=''):
-    return { 'WWW-Authenticate': 'OAuth realm="%s"' % realm }
+    """Optional WWW-Authenticate header (401 error)"""
+    return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
 
-# url escape
 def escape(s):
-    # escape '/' too
+    """Escape a URL including any /."""
     return urllib.quote(s, safe='~')
 
-# util function: current timestamp
-# seconds since epoch (UTC)
+def _utf8_str(s):
+    """Convert unicode to utf-8."""
+    if isinstance(s, unicode):
+        return s.encode("utf-8")
+    else:
+        return str(s)
+
 def generate_timestamp():
+    """Get seconds since epoch (UTC)."""
     return int(time.time())
 
-# util function: nonce
-# pseudorandom number
 def generate_nonce(length=8):
-    return ''.join(str(random.randint(0, 9)) for i in range(length))
+    """Generate pseudorandom number."""
+    return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+def generate_verifier(length=8):
+    """Generate pseudorandom number."""
+    return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
 
-# OAuthConsumer is a data type that represents the identity of the Consumer
-# via its shared secret with the Service Provider.
 class OAuthConsumer(object):
+    """Consumer of OAuth authentication.
+
+    OAuthConsumer is a data type that represents the identity of the Consumer
+    via its shared secret with the Service Provider.
+
+    """
     key = None
     secret = None
 
@@ -52,39 +83,79 @@ class OAuthConsumer(object):
         self.key = key
         self.secret = secret
 
-# OAuthToken is a data type that represents an End User via either an access
-# or request token.     
+
 class OAuthToken(object):
-    # access tokens and request tokens
+    """OAuthToken is a data type that represents an End User via either an access
+    or request token.
+    
+    key -- the token
+    secret -- the token secret
+
+    """
     key = None
     secret = None
+    callback = None
+    callback_confirmed = None
+    verifier = None
 
-    '''
-    key = the token
-    secret = the token secret
-    '''
     def __init__(self, key, secret):
         self.key = key
         self.secret = secret
 
-    def to_string(self):
-        return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
+    def set_callback(self, callback):
+        self.callback = callback
+        self.callback_confirmed = 'true'
+
+    def set_verifier(self, verifier=None):
+        if verifier is not None:
+            self.verifier = verifier
+        else:
+            self.verifier = generate_verifier()
+
+    def get_callback_url(self):
+        if self.callback and self.verifier:
+            # Append the oauth_verifier.
+            parts = urlparse.urlparse(self.callback)
+            scheme, netloc, path, params, query, fragment = parts[:6]
+            if query:
+                query = '%s&oauth_verifier=%s' % (query, self.verifier)
+            else:
+                query = 'oauth_verifier=%s' % self.verifier
+            return urlparse.urlunparse((scheme, netloc, path, params,
+                query, fragment))
+        return self.callback
 
-    # return a token from something like:
-    # oauth_token_secret=digg&oauth_token=digg
-    @staticmethod   
+    def to_string(self):
+        data = {
+            'oauth_token': self.key,
+            'oauth_token_secret': self.secret,
+        }
+        if self.callback_confirmed is not None:
+            data['oauth_callback_confirmed'] = self.callback_confirmed
+        return urllib.urlencode(data)
+ 
     def from_string(s):
+        """ Returns a token from something like:
+        oauth_token_secret=xxx&oauth_token=xxx
+        """
         params = cgi.parse_qs(s, keep_blank_values=False)
         key = params['oauth_token'][0]
         secret = params['oauth_token_secret'][0]
-        return OAuthToken(key, secret)
+        token = OAuthToken(key, secret)
+        try:
+            token.callback_confirmed = params['oauth_callback_confirmed'][0]
+        except KeyError:
+            pass # 1.0, no callback confirmed.
+        return token
+    from_string = staticmethod(from_string)
 
     def __str__(self):
         return self.to_string()
 
-# OAuthRequest represents the request and can be serialized
+
 class OAuthRequest(object):
-    '''
+    """OAuthRequest represents the request and can be serialized.
+
     OAuth parameters:
         - oauth_consumer_key 
         - oauth_token
@@ -93,9 +164,10 @@ class OAuthRequest(object):
         - oauth_timestamp 
         - oauth_nonce
         - oauth_version
+        - oauth_verifier
         ... any additional parameters, as defined by the Service Provider.
-    '''
-    parameters = None # oauth parameters
+    """
+    parameters = None # OAuth parameters.
     http_method = HTTP_METHOD
     http_url = None
     version = VERSION
@@ -115,93 +187,107 @@ class OAuthRequest(object):
             raise OAuthError('Parameter not found: %s' % parameter)
 
     def _get_timestamp_nonce(self):
-        return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce')
+        return self.get_parameter('oauth_timestamp'), self.get_parameter(
+            'oauth_nonce')
 
-    # get any non-oauth parameters
     def get_nonoauth_parameters(self):
+        """Get any non-OAuth parameters."""
         parameters = {}
         for k, v in self.parameters.iteritems():
-            # ignore oauth parameters
+            # Ignore oauth parameters.
             if k.find('oauth_') < 0:
                 parameters[k] = v
         return parameters
 
-    # serialize as a header for an HTTPAuth request
     def to_header(self, realm=''):
+        """Serialize as a header for an HTTPAuth request."""
         auth_header = 'OAuth realm="%s"' % realm
-        # add the oauth parameters
+        # Add the oauth parameters.
         if self.parameters:
             for k, v in self.parameters.iteritems():
-                auth_header += ', %s="%s"' % (k, escape(str(v)))
+                if k[:6] == 'oauth_':
+                    auth_header += ', %s="%s"' % (k, escape(str(v)))
         return {'Authorization': auth_header}
 
-    # serialize as post data for a POST request
     def to_postdata(self):
-        return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems())
+        """Serialize as post data for a POST request."""
+        return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
+            for k, v in self.parameters.iteritems()])
 
-    # serialize as a url for a GET request
     def to_url(self):
+        """Serialize as a URL for a GET request."""
         return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
 
-    # return a string that consists of all the parameters that need to be signed
     def get_normalized_parameters(self):
+        """Return a string that contains the parameters that must be signed."""
         params = self.parameters
         try:
-            # exclude the signature if it exists
+            # Exclude the signature if it exists.
             del params['oauth_signature']
         except:
             pass
-        key_values = params.items()
-        # sort lexicographically, first after key, then after value
+        # Escape key values before sorting.
+        key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
+            for k,v in params.items()]
+        # Sort lexicographically, first after key, then after value.
         key_values.sort()
-        # combine key value pairs in string and escape
-        return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values)
+        # Combine key value pairs into a string.
+        return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
 
-    # just uppercases the http method
     def get_normalized_http_method(self):
+        """Uppercases the http method."""
         return self.http_method.upper()
 
-    # parses the url and rebuilds it to be scheme://host/path
     def get_normalized_http_url(self):
+        """Parses the URL and rebuilds it to be scheme://host/path."""
         parts = urlparse.urlparse(self.http_url)
-        url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path
-        return url_string
-        
-    # set the signature parameter to the result of build_signature
+        scheme, netloc, path = parts[:3]
+        # Exclude default port numbers.
+        if scheme == 'http' and netloc[-3:] == ':80':
+            netloc = netloc[:-3]
+        elif scheme == 'https' and netloc[-4:] == ':443':
+            netloc = netloc[:-4]
+        return '%s://%s%s' % (scheme, netloc, path)
+
     def sign_request(self, signature_method, consumer, token):
-        # set the signature method
-        self.set_parameter('oauth_signature_method', signature_method.get_name())
-        # set the signature
-        self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token))
+        """Set the signature parameter to the result of build_signature."""
+        # Set the signature method.
+        self.set_parameter('oauth_signature_method',
+            signature_method.get_name())
+        # Set the signature.
+        self.set_parameter('oauth_signature',
+            self.build_signature(signature_method, consumer, token))
 
     def build_signature(self, signature_method, consumer, token):
-        # call the build signature method within the signature method
+        """Calls the build signature method within the signature method."""
         return signature_method.build_signature(self, consumer, token)
 
-    @staticmethod
-    def from_request(http_method, http_url, headers=None, parameters=None, query_string=None):
-        # combine multiple parameter sources
+    def from_request(http_method, http_url, headers=None, parameters=None,
+            query_string=None):
+        """Combines multiple parameter sources."""
         if parameters is None:
             parameters = {}
 
-        # headers
-        if headers and 'HTTP_AUTHORIZATION' in headers:
-            auth_header = headers['HTTP_AUTHORIZATION']
-            # check that the authorization header is OAuth
-            if auth_header.index('OAuth') > -1:
+        # Headers
+        if headers and 'Authorization' in headers:
+            auth_header = headers['Authorization']
+            # Check that the authorization header is OAuth.
+            if auth_header[:6] == 'OAuth ':
+                auth_header = auth_header[6:]
                 try:
-                    # get the parameters from the header
+                    # Get the parameters from the header.
                     header_params = OAuthRequest._split_header(auth_header)
                     parameters.update(header_params)
                 except:
-                    raise OAuthError('Unable to parse OAuth parameters from Authorization header.')
+                    raise OAuthError('Unable to parse OAuth parameters from '
+                        'Authorization header.')
 
-        # GET or POST query string
+        # GET or POST query string.
         if query_string:
             query_params = OAuthRequest._split_url_string(query_string)
             parameters.update(query_params)
 
-        # URL parameters
+        # URL parameters.
         param_str = urlparse.urlparse(http_url)[4] # query
         url_params = OAuthRequest._split_url_string(param_str)
         parameters.update(url_params)
@@ -210,9 +296,11 @@ class OAuthRequest(object):
             return OAuthRequest(http_method, http_url, parameters)
 
         return None
+    from_request = staticmethod(from_request)
 
-    @staticmethod
-    def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
+    def from_consumer_and_token(oauth_consumer, token=None,
+            callback=None, verifier=None, http_method=HTTP_METHOD,
+            http_url=None, parameters=None):
         if not parameters:
             parameters = {}
 
@@ -228,50 +316,57 @@ class OAuthRequest(object):
 
         if token:
             parameters['oauth_token'] = token.key
+            parameters['oauth_callback'] = token.callback
+            # 1.0a support for verifier.
+            parameters['oauth_verifier'] = verifier
+        elif callback:
+            # 1.0a support for callback in the request token request.
+            parameters['oauth_callback'] = callback
 
         return OAuthRequest(http_method, http_url, parameters)
+    from_consumer_and_token = staticmethod(from_consumer_and_token)
 
-    @staticmethod
-    def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
+    def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
+            http_url=None, parameters=None):
         if not parameters:
             parameters = {}
 
         parameters['oauth_token'] = token.key
 
         if callback:
-            parameters['oauth_callback'] = escape(callback)
+            parameters['oauth_callback'] = callback
 
         return OAuthRequest(http_method, http_url, parameters)
+    from_token_and_callback = staticmethod(from_token_and_callback)
 
-    # util function: turn Authorization: header into parameters, has to do some unescaping
-    @staticmethod
     def _split_header(header):
+        """Turn Authorization: header into parameters."""
         params = {}
-        header = header.replace('OAuth ', '', 1)
         parts = header.split(',')
         for param in parts:
-            # ignore realm parameter
+            # Ignore realm parameter.
             if param.find('realm') > -1:
                 continue
-            # remove whitespace
+            # Remove whitespace.
             param = param.strip()
-            # split key-value
+            # Split key-value.
             param_parts = param.split('=', 1)
-            # remove quotes and unescape the value
+            # Remove quotes and unescape the value.
             params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
         return params
-    
-    # util function: turn url string into parameters, has to do some unescaping
-    @staticmethod
+    _split_header = staticmethod(_split_header)
+
     def _split_url_string(param_str):
+        """Turn URL string into parameters."""
         parameters = cgi.parse_qs(param_str, keep_blank_values=False)
         for k, v in parameters.iteritems():
             parameters[k] = urllib.unquote(v[0])
         return parameters
+    _split_url_string = staticmethod(_split_url_string)
 
-# OAuthServer is a worker to check a requests validity against a data store
 class OAuthServer(object):
-    timestamp_threshold = 300 # in seconds, five minutes
+    """A worker to check the validity of a request against a data store."""
+    timestamp_threshold = 300 # In seconds, five minutes.
     version = VERSION
     signature_methods = None
     data_store = None
@@ -280,7 +375,7 @@ class OAuthServer(object):
         self.data_store = data_store
         self.signature_methods = signature_methods or {}
 
-    def set_data_store(self, oauth_data_store):
+    def set_data_store(self, data_store):
         self.data_store = data_store
 
     def get_data_store(self):
@@ -290,57 +385,64 @@ class OAuthServer(object):
         self.signature_methods[signature_method.get_name()] = signature_method
         return self.signature_methods
 
-    # process a request_token request
-    # returns the request token on success
     def fetch_request_token(self, oauth_request):
+        """Processes a request_token request and returns the
+        request token on success.
+        """
         try:
-            # get the request token for authorization
+            # Get the request token for authorization.
             token = self._get_token(oauth_request, 'request')
         except OAuthError:
-            # no token required for the initial token request
+            # No token required for the initial token request.
             version = self._get_version(oauth_request)
             consumer = self._get_consumer(oauth_request)
+            try:
+                callback = self.get_callback(oauth_request)
+            except OAuthError:
+                callback = None # 1.0, no callback specified.
             self._check_signature(oauth_request, consumer, None)
-            # fetch a new token
-            token = self.data_store.fetch_request_token(consumer)
+            # Fetch a new token.
+            token = self.data_store.fetch_request_token(consumer, callback)
         return token
 
-    # process an access_token request
-    # returns the access token on success
     def fetch_access_token(self, oauth_request):
+        """Processes an access_token request and returns the
+        access token on success.
+        """
         version = self._get_version(oauth_request)
         consumer = self._get_consumer(oauth_request)
-        # get the request token
+        verifier = self._get_verifier(oauth_request)
+        # Get the request token.
         token = self._get_token(oauth_request, 'request')
         self._check_signature(oauth_request, consumer, token)
-        new_token = self.data_store.fetch_access_token(consumer, token)
+        new_token = self.data_store.fetch_access_token(consumer, token, verifier)
         return new_token
 
-    # verify an api call, checks all the parameters
     def verify_request(self, oauth_request):
+        """Verifies an api call and checks all the parameters."""
         # -> consumer and token
         version = self._get_version(oauth_request)
         consumer = self._get_consumer(oauth_request)
-        # get the access token
+        # Get the access token.
         token = self._get_token(oauth_request, 'access')
         self._check_signature(oauth_request, consumer, token)
         parameters = oauth_request.get_nonoauth_parameters()
         return consumer, token, parameters
 
-    # authorize a request token
     def authorize_token(self, token, user):
+        """Authorize a request token."""
         return self.data_store.authorize_request_token(token, user)
-    
-    # get the callback url
+
     def get_callback(self, oauth_request):
+        """Get the callback URL."""
         return oauth_request.get_parameter('oauth_callback')
-
-    # optional support for the authenticate header   
+ 
     def build_authenticate_header(self, realm=''):
+        """Optional support for the authenticate header."""
         return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
 
-    # verify the correct version request for this server
     def _get_version(self, oauth_request):
+        """Verify the correct version request for this server."""
         try:
             version = oauth_request.get_parameter('oauth_version')
         except:
@@ -349,37 +451,40 @@ class OAuthServer(object):
             raise OAuthError('OAuth version %s not supported.' % str(version))
         return version
 
-    # figure out the signature with some defaults
     def _get_signature_method(self, oauth_request):
+        """Figure out the signature with some defaults."""
         try:
-            signature_method = oauth_request.get_parameter('oauth_signature_method')
+            signature_method = oauth_request.get_parameter(
+                'oauth_signature_method')
         except:
             signature_method = SIGNATURE_METHOD
         try:
-            # get the signature method object
+            # Get the signature method object.
             signature_method = self.signature_methods[signature_method]
         except:
             signature_method_names = ', '.join(self.signature_methods.keys())
-            raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
+            raise OAuthError('Signature method %s not supported try one of the '
+                'following: %s' % (signature_method, signature_method_names))
 
         return signature_method
 
     def _get_consumer(self, oauth_request):
         consumer_key = oauth_request.get_parameter('oauth_consumer_key')
-        if not consumer_key:
-            raise OAuthError('Invalid consumer key.')
         consumer = self.data_store.lookup_consumer(consumer_key)
         if not consumer:
             raise OAuthError('Invalid consumer.')
         return consumer
 
-    # try to find the token for the provided request token key
     def _get_token(self, oauth_request, token_type='access'):
+        """Try to find the token for the provided request token key."""
         token_field = oauth_request.get_parameter('oauth_token')
         token = self.data_store.lookup_token(token_type, token_field)
         if not token:
             raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
         return token
+    
+    def _get_verifier(self, oauth_request):
+        return oauth_request.get_parameter('oauth_verifier')
 
     def _check_signature(self, oauth_request, consumer, token):
         timestamp, nonce = oauth_request._get_timestamp_nonce()
@@ -390,29 +495,35 @@ class OAuthServer(object):
             signature = oauth_request.get_parameter('oauth_signature')
         except:
             raise OAuthError('Missing signature.')
-        # validate the signature
-        valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature)
+        # Validate the signature.
+        valid_sig = signature_method.check_signature(oauth_request, consumer,
+            token, signature)
         if not valid_sig:
-            key, base = signature_method.build_signature_base_string(oauth_request, consumer, token)
-            raise OAuthError('Invalid signature. Expected signature base string: %s' % base)
+            key, base = signature_method.build_signature_base_string(
+                oauth_request, consumer, token)
+            raise OAuthError('Invalid signature. Expected signature base '
+                'string: %s' % base)
         built = signature_method.build_signature(oauth_request, consumer, token)
 
     def _check_timestamp(self, timestamp):
-        # verify that timestamp is recentish
+        """Verify that timestamp is recentish."""
         timestamp = int(timestamp)
         now = int(time.time())
         lapsed = now - timestamp
         if lapsed > self.timestamp_threshold:
-            raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold))
+            raise OAuthError('Expired timestamp: given %d and now %s has a '
+                'greater difference than threshold %d' %
+                (timestamp, now, self.timestamp_threshold))
 
     def _check_nonce(self, consumer, token, nonce):
-        # verify that the nonce is uniqueish
+        """Verify that the nonce is uniqueish."""
         nonce = self.data_store.lookup_nonce(consumer, token, nonce)
         if nonce:
             raise OAuthError('Nonce already used: %s' % str(nonce))
 
-# OAuthClient is a worker to attempt to execute a request
+
 class OAuthClient(object):
+    """OAuthClient is a worker to attempt to execute a request."""
     consumer = None
     token = None
 
@@ -427,62 +538,65 @@ class OAuthClient(object):
         return self.token
 
     def fetch_request_token(self, oauth_request):
-        # -> OAuthToken
+        """-> OAuthToken."""
         raise NotImplementedError
 
     def fetch_access_token(self, oauth_request):
-        # -> OAuthToken
+        """-> OAuthToken."""
         raise NotImplementedError
 
     def access_resource(self, oauth_request):
-        # -> some protected resource
+        """-> Some protected resource."""
         raise NotImplementedError
 
-# OAuthDataStore is a database abstraction used to lookup consumers and tokens
+
 class OAuthDataStore(object):
+    """A database abstraction used to lookup consumers and tokens."""
 
     def lookup_consumer(self, key):
-        # -> OAuthConsumer
+        """-> OAuthConsumer."""
         raise NotImplementedError
 
     def lookup_token(self, oauth_consumer, token_type, token_token):
-        # -> OAuthToken
+        """-> OAuthToken."""
         raise NotImplementedError
 
-    def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp):
-        # -> OAuthToken
+    def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
+        """-> OAuthToken."""
         raise NotImplementedError
 
-    def fetch_request_token(self, oauth_consumer):
-        # -> OAuthToken
+    def fetch_request_token(self, oauth_consumer, oauth_callback):
+        """-> OAuthToken."""
         raise NotImplementedError
 
-    def fetch_access_token(self, oauth_consumer, oauth_token):
-        # -> OAuthToken
+    def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
+        """-> OAuthToken."""
         raise NotImplementedError
 
     def authorize_request_token(self, oauth_token, user):
-        # -> OAuthToken
+        """-> OAuthToken."""
         raise NotImplementedError
 
-# OAuthSignatureMethod is a strategy class that implements a signature method
+
 class OAuthSignatureMethod(object):
+    """A strategy class that implements a signature method."""
     def get_name(self):
-        # -> str
+        """-> str."""
         raise NotImplementedError
 
     def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
-        # -> str key, str raw
+        """-> str key, str raw."""
         raise NotImplementedError
 
     def build_signature(self, oauth_request, oauth_consumer, oauth_token):
-        # -> str
+        """-> str."""
         raise NotImplementedError
 
     def check_signature(self, oauth_request, consumer, token, signature):
         built = self.build_signature(oauth_request, consumer, token)
         return built == signature
 
+
 class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
 
     def get_name(self):
@@ -502,19 +616,21 @@ class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
         return key, raw
 
     def build_signature(self, oauth_request, consumer, token):
-        # build the base signature string
-        key, raw = self.build_signature_base_string(oauth_request, consumer, token)
+        """Builds the base signature string."""
+        key, raw = self.build_signature_base_string(oauth_request, consumer,
+            token)
 
-        # hmac object
+        # HMAC object.
         try:
             import hashlib # 2.5
             hashed = hmac.new(key, raw, hashlib.sha1)
         except:
-            import sha # deprecated
+            import sha # Deprecated
             hashed = hmac.new(key, raw, sha)
 
-        # calculate the digest base 64
-        return base64.b64encode(hashed.digest())
+        # Calculate the digest base 64.
+        return binascii.b2a_base64(hashed.digest())[:-1]
+
 
 class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
 
@@ -522,11 +638,13 @@ class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
         return 'PLAINTEXT'
 
     def build_signature_base_string(self, oauth_request, consumer, token):
-        # concatenate the consumer key and secret
-        sig = escape(consumer.secret) + '&'
+        """Concatenates the consumer key and secret."""
+        sig = '%s&' % escape(consumer.secret)
         if token:
             sig = sig + escape(token.secret)
-        return sig
+        return sig, sig
 
     def build_signature(self, oauth_request, consumer, token):
-        return self.build_signature_base_string(oauth_request, consumer, token)
+        key, raw = self.build_signature_base_string(oauth_request, consumer,
+            token)
+        return key
\ No newline at end of file
diff --git a/lib/piston/resource.py b/lib/piston/resource.py
index 612ac9f..c63fd54 100644
--- a/lib/piston/resource.py
+++ b/lib/piston/resource.py
@@ -7,6 +7,7 @@ from django.views.decorators.vary import vary_on_headers
 from django.conf import settings
 from django.core.mail import send_mail, EmailMessage
 from django.db.models.query import QuerySet
+from django.http import Http404
 
 from emitters import Emitter
 from handler import typemapper
@@ -15,6 +16,8 @@ from authentication import NoAuthentication
 from utils import coerce_put_post, FormValidationError, HttpStatusCode
 from utils import rc, format_error, translate_mime, MimerDataException
 
+CHALLENGE = object()
+
 class Resource(object):
     """
     Resource. Create one for your URL mappings, just
@@ -23,20 +26,22 @@ class Resource(object):
     is an authentication handler. If not specified,
     `NoAuthentication` will be used by default.
     """
-    callmap = { 'GET': 'read', 'POST': 'create', 
+    callmap = { 'GET': 'read', 'POST': 'create',
                 'PUT': 'update', 'DELETE': 'delete' }
-    
+
     def __init__(self, handler, authentication=None):
         if not callable(handler):
             raise AttributeError, "Handler not callable."
-        
+
         self.handler = handler()
-        
+
         if not authentication:
-            self.authentication = NoAuthentication()
-        else:
+            self.authentication = (NoAuthentication(),)
+        elif isinstance(authentication, (list, tuple)):
             self.authentication = authentication
-            
+        else:
+            self.authentication = (authentication,)
+
         # Erroring
         self.email_errors = getattr(settings, 'PISTON_EMAIL_ERRORS', True)
         self.display_errors = getattr(settings, 'PISTON_DISPLAY_ERRORS', True)
@@ -53,12 +58,22 @@ class Resource(object):
         that as well.
         """
         em = kwargs.pop('emitter_format', None)
-        
+
         if not em:
             em = request.GET.get('format', 'json')
 
         return em
-    
+
+    def form_validation_response(self, e):
+        """
+        Method to return form validation error information. 
+        You will probably want to override this in your own
+        `Resource` subclass.
+        """
+        resp = rc.BAD_REQUEST
+        resp.write(' '+str(e.form.errors))
+        return resp
+
     @property
     def anonymous(self):
         """
@@ -69,16 +84,32 @@ class Resource(object):
         """
         if hasattr(self.handler, 'anonymous'):
             anon = self.handler.anonymous
-            
+
             if callable(anon):
                 return anon
 
             for klass in typemapper.keys():
                 if anon == klass.__name__:
                     return klass
-            
+
         return None
-    
+
+    def authenticate(self, request, rm):
+        actor, anonymous = False, True
+
+        for authenticator in self.authentication:
+            if not authenticator.is_authenticated(request):
+                if self.anonymous and \
+                    rm in self.anonymous.allowed_methods:
+
+                    actor, anonymous = self.anonymous(), True
+                else:
+                    actor, anonymous = authenticator.challenge, CHALLENGE
+            else:
+                return self.handler, self.handler.is_anonymous
+
+        return actor, anonymous
+
     @vary_on_headers('Authorization')
     def __call__(self, request, *args, **kwargs):
         """
@@ -92,30 +123,30 @@ class Resource(object):
         if rm == "PUT":
             coerce_put_post(request)
 
-        if not self.authentication.is_authenticated(request):
-            if self.anonymous and \
-                rm in self.anonymous.allowed_methods:
+        actor, anonymous = self.authenticate(request, rm)
 
-                handler = self.anonymous()
-                anonymous = True
-            else:
-                return self.authentication.challenge()
+        if anonymous is CHALLENGE:
+            return actor()
         else:
-            handler = self.handler
-            anonymous = handler.is_anonymous
-        
+            handler = actor
+
         # Translate nested datastructs into `request.data` here.
         if rm in ('POST', 'PUT'):
             try:
                 translate_mime(request)
             except MimerDataException:
                 return rc.BAD_REQUEST
-        
+            if not hasattr(request, 'data'):
+                if rm == 'POST':
+                    request.data = request.POST
+                else:
+                    request.data = request.PUT
+
         if not rm in handler.allowed_methods:
             return HttpResponseNotAllowed(handler.allowed_methods)
-        
+
         meth = getattr(handler, self.callmap.get(rm), None)
-        
+
         if not meth:
             raise Http404
 
@@ -123,60 +154,17 @@ class Resource(object):
         em_format = self.determine_emitter(request, *args, **kwargs)
 
         kwargs.pop('emitter_format', None)
-        
+
         # Clean up the request object a bit, since we might
         # very well have `oauth_`-headers in there, and we
         # don't want to pass these along to the handler.
         request = self.cleanup_request(request)
-        
+
         try:
             result = meth(request, *args, **kwargs)
-        except FormValidationError, e:
-            # TODO: Use rc.BAD_REQUEST here
-            return HttpResponse("Bad Request: %s" % e.form.errors, status=400)
-        except TypeError, e:
-            result = rc.BAD_REQUEST
-            hm = HandlerMethod(meth)
-            sig = hm.get_signature()
-
-            msg = 'Method signature does not match.\n\n'
-            
-            if sig:
-                msg += 'Signature should be: %s' % sig
-            else:
-                msg += 'Resource does not expect any parameters.'
-
-            if self.display_errors:                
-                msg += '\n\nException was: %s' % str(e)
-                
-            result.content = format_error(msg)
-        except HttpStatusCode, e:
-            #result = e ## why is this being passed on and not just dealt with now?
-            return e.response
         except Exception, e:
-            """
-            On errors (like code errors), we'd like to be able to
-            give crash reports to both admins and also the calling
-            user. There's two setting parameters for this:
-            
-            Parameters::
-             - `PISTON_EMAIL_ERRORS`: Will send a Django formatted
-               error email to people in `settings.ADMINS`.
-             - `PISTON_DISPLAY_ERRORS`: Will return a simple traceback
-               to the caller, so he can tell you what error they got.
-               
-            If `PISTON_DISPLAY_ERRORS` is not enabled, the caller will
-            receive a basic "500 Internal Server Error" message.
-            """
-            exc_type, exc_value, tb = sys.exc_info()
-            rep = ExceptionReporter(request, exc_type, exc_value, tb.tb_next)
-            if self.email_errors:
-                self.email_exception(rep)
-            if self.display_errors:
-                return HttpResponseServerError(
-                    format_error('\n'.join(rep.format_exception())))
-            else:
-                raise
+            result = self.error_handler(e, request, meth)
+
 
         emitter, ct = Emitter.get(em_format)
         fields = handler.fields
@@ -184,6 +172,18 @@ class Resource(object):
                 isinstance(result, list) or isinstance(result, QuerySet)):
             fields = handler.list_fields
 
+        status_code = 200
+
+        # If we're looking at a response object which contains non-string
+        # content, then assume we should use the emitter to format that 
+        # content
+        if isinstance(result, HttpResponse) and not result._is_string:
+            status_code = result.status_code
+            # Note: We can't use result.content here because that method attempts
+            # to convert the content into a string which we don't want. 
+            # when _is_string is False _container is the raw data
+            result = result._container
+            
         srl = emitter(result, typemapper, handler, fields, anonymous)
 
         try:
@@ -196,7 +196,10 @@ class Resource(object):
             if self.stream: stream = srl.stream_render(request)
             else: stream = srl.render(request)
 
-            resp = HttpResponse(stream, mimetype=ct)
+            if not isinstance(stream, HttpResponse):
+                resp = HttpResponse(stream, mimetype=ct, status=status_code)
+            else:
+                resp = stream
 
             resp.streaming = self.stream
 
@@ -215,17 +218,17 @@ class Resource(object):
 
             if True in [ k.startswith("oauth_") for k in block.keys() ]:
                 sanitized = block.copy()
-                
+
                 for k in sanitized.keys():
                     if k.startswith("oauth_"):
                         sanitized.pop(k)
-                        
+
                 setattr(request, method_type, sanitized)
 
         return request
-        
-    # -- 
-    
+
+    # --
+
     def email_exception(self, reporter):
         subject = "Piston crash report"
         html = reporter.get_traceback_html()
@@ -233,6 +236,63 @@ class Resource(object):
         message = EmailMessage(settings.EMAIL_SUBJECT_PREFIX+subject,
                                 html, settings.SERVER_EMAIL,
                                 [ admin[1] for admin in settings.ADMINS ])
-        
+
         message.content_subtype = 'html'
         message.send(fail_silently=True)
+
+
+    def error_handler(self, e, request, meth):
+        """
+        Override this method to add handling of errors customized for your 
+        needs
+        """
+        if isinstance(e, FormValidationError):
+            return self.form_validation_response(e)
+
+        elif isinstance(e, TypeError):
+            result = rc.BAD_REQUEST
+            hm = HandlerMethod(meth)
+            sig = hm.signature
+
+            msg = 'Method signature does not match.\n\n'
+
+            if sig:
+                msg += 'Signature should be: %s' % sig
+            else:
+                msg += 'Resource does not expect any parameters.'
+
+            if self.display_errors:
+                msg += '\n\nException was: %s' % str(e)
+
+            result.content = format_error(msg)
+            return result
+        elif isinstance(e, Http404):
+            return rc.NOT_FOUND
+
+        elif isinstance(e, HttpStatusCode):
+            return e.response
+ 
+        else: 
+            """
+            On errors (like code errors), we'd like to be able to
+            give crash reports to both admins and also the calling
+            user. There's two setting parameters for this:
+
+            Parameters::
+             - `PISTON_EMAIL_ERRORS`: Will send a Django formatted
+               error email to people in `settings.ADMINS`.
+             - `PISTON_DISPLAY_ERRORS`: Will return a simple traceback
+               to the caller, so he can tell you what error they got.
+
+            If `PISTON_DISPLAY_ERRORS` is not enabled, the caller will
+            receive a basic "500 Internal Server Error" message.
+            """
+            exc_type, exc_value, tb = sys.exc_info()
+            rep = ExceptionReporter(request, exc_type, exc_value, tb.tb_next)
+            if self.email_errors:
+                self.email_exception(rep)
+            if self.display_errors:
+                return HttpResponseServerError(
+                    format_error('\n'.join(rep.format_exception())))
+            else:
+                raise
diff --git a/lib/piston/signals.py b/lib/piston/signals.py
new file mode 100644
index 0000000..133be13
--- /dev/null
+++ b/lib/piston/signals.py
@@ -0,0 +1,14 @@
+# Django imports
+import django.dispatch 
+
+# Piston imports
+from utils import send_consumer_mail
+
+def consumer_post_save(sender, instance, created, **kwargs):
+    send_consumer_mail(instance)
+
+def consumer_post_delete(sender, instance, **kwargs):
+    instance.status = 'canceled'
+    send_consumer_mail(instance)
+
+
diff --git a/lib/piston/store.py b/lib/piston/store.py
index 741a470..787791a 100644
--- a/lib/piston/store.py
+++ b/lib/piston/store.py
@@ -1,6 +1,7 @@
 import oauth
 
 from models import Nonce, Token, Consumer
+from models import generate_random, VERIFIER_SIZE
 
 class DataStore(oauth.OAuthDataStore):
     """Layer between Python OAuth and Django database."""
@@ -39,17 +40,22 @@ class DataStore(oauth.OAuthDataStore):
         else:
             return nonce.key
 
-    def fetch_request_token(self, oauth_consumer):
+    def fetch_request_token(self, oauth_consumer, oauth_callback):
         if oauth_consumer.key == self.consumer.key:
             self.request_token = Token.objects.create_token(consumer=self.consumer,
                                                             token_type=Token.REQUEST,
                                                             timestamp=self.timestamp)
+            
+            if oauth_callback:
+                self.request_token.set_callback(oauth_callback)
+            
             return self.request_token
         return None
 
-    def fetch_access_token(self, oauth_consumer, oauth_token):
+    def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
         if oauth_consumer.key == self.consumer.key \
         and oauth_token.key == self.request_token.key \
+        and oauth_verifier == self.request_token.verifier \
         and self.request_token.is_approved:
             self.access_token = Token.objects.create_token(consumer=self.consumer,
                                                            token_type=Token.ACCESS,
@@ -63,6 +69,7 @@ class DataStore(oauth.OAuthDataStore):
             # authorize the request token in the store
             self.request_token.is_approved = True
             self.request_token.user = user
+            self.request_token.verifier = generate_random(VERIFIER_SIZE)
             self.request_token.save()
             return self.request_token
         return None
\ No newline at end of file
diff --git a/lib/piston/test.py b/lib/piston/test.py
new file mode 100644
index 0000000..57eda72
--- /dev/null
+++ b/lib/piston/test.py
@@ -0,0 +1,62 @@
+# Django imports
+import django.test.client as client
+import django.test as test
+from django.utils.http import urlencode
+
+# Piston imports
+from piston import oauth
+from piston.models import Consumer, Token
+
+# 3rd/Python party imports
+import httplib2, urllib, cgi
+
+URLENCODED_FORM_CONTENT = 'application/x-www-form-urlencoded'
+
+class OAuthClient(client.Client):
+    def __init__(self, consumer, token):
+        self.token = oauth.OAuthToken(token.key, token.secret)
+        self.consumer = oauth.OAuthConsumer(consumer.key, consumer.secret)
+        self.signature = oauth.OAuthSignatureMethod_HMAC_SHA1()
+
+        super(OAuthClient, self).__init__()
+
+    def request(self, **request):
+        # Figure out parameters from request['QUERY_STRING'] and FakePayload
+        params = {}
+        if request['REQUEST_METHOD'] in ('POST', 'PUT'):
+            if request['CONTENT_TYPE'] == URLENCODED_FORM_CONTENT:
+                payload = request['wsgi.input'].read()
+                request['wsgi.input'] = client.FakePayload(payload)
+                params = cgi.parse_qs(payload)
+
+        url = "http://testserver"; + request['PATH_INFO']
+
+        req = oauth.OAuthRequest.from_consumer_and_token(
+            self.consumer, token=self.token, 
+            http_method=request['REQUEST_METHOD'], http_url=url, 
+            parameters=params
+        )
+
+        req.sign_request(self.signature, self.consumer, self.token)
+        headers = req.to_header()
+        request['HTTP_AUTHORIZATION'] = headers['Authorization']
+
+        return super(OAuthClient, self).request(**request)
+
+    def post(self, path, data={}, content_type=None, follow=False, **extra):
+        if content_type is None:
+            content_type = URLENCODED_FORM_CONTENT
+
+        if isinstance(data, dict):
+            data = urlencode(data)
+        
+        return super(OAuthClient, self).post(path, data, content_type, follow, **extra)
+
+class TestCase(test.TestCase):
+    pass
+
+class OAuthTestCase(TestCase):
+    @property
+    def oauth(self):
+        return OAuthClient(self.consumer, self.token)
+
diff --git a/lib/piston/tests.py b/lib/piston/tests.py
new file mode 100644
index 0000000..ed097fe
--- /dev/null
+++ b/lib/piston/tests.py
@@ -0,0 +1,175 @@
+# Django imports
+from django.core import mail
+from django.contrib.auth.models import User
+from django.conf import settings
+from django.http import HttpRequest, HttpResponse
+from django.utils import simplejson
+
+# Piston imports
+from test import TestCase
+from models import Consumer
+from handler import BaseHandler
+from utils import rc
+from resource import Resource
+
+class ConsumerTest(TestCase):
+    fixtures = ['models.json']
+
+    def setUp(self):
+        self.consumer = Consumer()
+        self.consumer.name = "Piston Test Consumer"
+        self.consumer.description = "A test consumer for Piston."
+        self.consumer.user = User.objects.get(pk=3)
+        self.consumer.generate_random_codes()
+
+    def test_create_pending(self):
+        """ Ensure creating a pending Consumer sends proper emails """
+        # If it's pending we should have two messages in the outbox; one 
+        # to the consumer and one to the site admins.
+        if len(settings.ADMINS):
+            self.assertEquals(len(mail.outbox), 2)
+        else:
+            self.assertEquals(len(mail.outbox), 1)
+
+        expected = "Your API Consumer for example.com is awaiting approval."
+        self.assertEquals(mail.outbox[0].subject, expected)
+
+    def test_delete_consumer(self):
+        """ Ensure deleting a Consumer sends a cancel email """
+
+        # Clear out the outbox before we test for the cancel email.
+        mail.outbox = []
+
+        # Delete the consumer, which should fire off the cancel email.
+        self.consumer.delete() 
+        
+        self.assertEquals(len(mail.outbox), 1)
+        expected = "Your API Consumer for example.com has been canceled."
+        self.assertEquals(mail.outbox[0].subject, expected)
+
+
+class CustomResponseWithStatusCodeTest(TestCase):
+     """
+     Test returning content to be formatted and a custom response code from a 
+     handler method. In this case we're returning 201 (created) and a dictionary 
+     of data. This data will be formatted as json. 
+     """
+
+     def test_reponse_with_data_and_status_code(self):
+         response_data = dict(complex_response=dict(something='good', 
+             something_else='great'))
+
+         class MyHandler(BaseHandler):
+             """
+             Handler which returns a response w/ both data and a status code (201)
+             """
+             allowed_methods = ('POST', )
+
+             def create(self, request):
+                 resp = rc.CREATED
+                 resp.content = response_data
+                 return resp
+
+         resource = Resource(MyHandler)
+         request = HttpRequest()
+         request.method = 'POST'
+         response = resource(request, emitter_format='json')
+
+         self.assertEquals(201, response.status_code)
+         self.assertTrue(response._is_string, "Expected response content to be a string")
+
+         # compare the original data dict with the json response 
+         # converted to a dict
+         self.assertEquals(response_data, simplejson.loads(response.content))
+
+
+class ErrorHandlerTest(TestCase):
+    def test_customized_error_handler(self):
+        """
+        Throw a custom error from a handler method and catch (and format) it 
+        in an overridden error_handler method on the associated Resource object
+        """
+        class GoAwayError(Exception):
+            def __init__(self, name, reason):
+                self.name = name
+                self.reason = reason
+
+        class MyHandler(BaseHandler):
+            """
+            Handler which raises a custom exception 
+            """
+            allowed_methods = ('GET',)
+
+            def read(self, request):
+                raise GoAwayError('Jerome', 'No one likes you')
+
+        class MyResource(Resource):
+            def error_handler(self, error, request, meth):
+                # if the exception is our exeption then generate a 
+                # custom response with embedded content that will be 
+                # formatted as json 
+                if isinstance(error, GoAwayError):
+                    response = rc.FORBIDDEN
+                    response.content = dict(error=dict(
+                        name=error.name, 
+                        message="Get out of here and dont come back", 
+                        reason=error.reason
+                    ))    
+
+                    return response
+
+                return super(MyResource, self).error_handler(error, request, meth)
+
+        resource = MyResource(MyHandler)
+
+        request = HttpRequest()
+        request.method = 'GET'
+        response = resource(request, emitter_format='json')
+
+        self.assertEquals(401, response.status_code)
+
+        # verify the content we got back can be converted back to json 
+        # and examine the dictionary keys all exist as expected
+        response_data = simplejson.loads(response.content)
+        self.assertTrue('error' in response_data)
+        self.assertTrue('name' in response_data['error'])
+        self.assertTrue('message' in response_data['error'])
+        self.assertTrue('reason' in response_data['error'])
+
+    def test_type_error(self):
+        """
+        Verify that type errors thrown from a handler method result in a valid 
+        HttpResonse object being returned from the error_handler method
+        """
+        class MyHandler(BaseHandler):
+            def read(self, request):
+                raise TypeError()
+
+        request = HttpRequest()
+        request.method = 'GET'
+        response = Resource(MyHandler)(request)
+
+        self.assertTrue(isinstance(response, HttpResponse), "Expected a response, not: %s" 
+            % response)
+
+
+    def test_other_error(self):
+        """
+        Verify that other exceptions thrown from a handler method result in a valid
+        HttpResponse object being returned from the error_handler method
+        """
+        class MyHandler(BaseHandler):
+            def read(self, request):
+                raise Exception()
+
+        resource = Resource(MyHandler)
+        resource.display_errors = True
+        resource.email_errors = False
+
+        request = HttpRequest()
+        request.method = 'GET'
+        response = resource(request)
+
+        self.assertTrue(isinstance(response, HttpResponse), "Expected a response, not: %s" 
+            % response)
+
diff --git a/lib/piston/utils.py b/lib/piston/utils.py
index b197ded..d8461d8 100644
--- a/lib/piston/utils.py
+++ b/lib/piston/utils.py
@@ -1,12 +1,18 @@
+import time
 from django.http import HttpResponseNotAllowed, HttpResponseForbidden, HttpResponse, HttpResponseBadRequest
 from django.core.urlresolvers import reverse
 from django.core.cache import cache
 from django import get_version as django_version
+from django.core.mail import send_mail, mail_admins
+from django.conf import settings
+from django.utils.translation import ugettext as _
+from django.template import loader, TemplateDoesNotExist
+from django.contrib.sites.models import Site
 from decorator import decorator
 
 from datetime import datetime, timedelta
 
-__version__ = '0.2.2'
+__version__ = '0.2.3rc1'
 
 def get_version():
     return __version__
@@ -27,6 +33,7 @@ class rc_factory(object):
                  NOT_FOUND = ('Not Found', 404),
                  DUPLICATE_ENTRY = ('Conflict/Duplicate', 409),
                  NOT_HERE = ('Gone', 410),
+                 INTERNAL_ERROR = ('Internal Error', 500),
                  NOT_IMPLEMENTED = ('Not Implemented', 501),
                  THROTTLED = ('Throttled', 503))
 
@@ -41,7 +48,30 @@ class rc_factory(object):
         except TypeError:
             raise AttributeError(attr)
 
-        return HttpResponse(r, content_type='text/plain', status=c)
+        class HttpResponseWrapper(HttpResponse):
+            """
+            Wrap HttpResponse and make sure that the internal _is_string 
+            flag is updated when the _set_content method (via the content 
+            property) is called
+            """
+            def _set_content(self, content):
+                """
+                Set the _container and _is_string properties based on the 
+                type of the value parameter. This logic is in the construtor
+                for HttpResponse, but doesn't get repeated when setting 
+                HttpResponse.content although this bug report (feature request)
+                suggests that it should: http://code.djangoproject.com/ticket/9403 
+                """
+                if not isinstance(content, basestring) and hasattr(content, '__iter__'):
+                    self._container = content
+                    self._is_string = False
+                else:
+                    self._container = [content]
+                    self._is_string = True
+
+            content = property(HttpResponse._get_content, _set_content)            
+
+        return HttpResponseWrapper(r, content_type='text/plain', status=c)
     
 rc = rc_factory()
     
@@ -59,6 +89,7 @@ def validate(v_form, operation='POST'):
         form = v_form(getattr(request, operation))
     
         if form.is_valid():
+            setattr(request, 'form', form)
             return f(self, request, *a, **kwa)
         else:
             raise FormValidationError(form)
@@ -102,24 +133,20 @@ def throttle(max_requests, timeout=60*60, extra=''):
             """
             ident += ':%s' % extra
     
-            now = datetime.now()
-            ts_key = 'throttle:ts:%s' % ident
-            timestamp = cache.get(ts_key)
-            offset = now + timedelta(seconds=timeout)
-    
-            if timestamp and timestamp < offset:
+            now = time.time()
+            count, expiration = cache.get(ident, (1, None))
+
+            if expiration is None:
+                expiration = now + timeout
+
+            if count >= max_requests and expiration > now:
                 t = rc.THROTTLED
-                wait = timeout - (offset-timestamp).seconds
+                wait = int(expiration - now)
                 t.content = 'Throttled, wait %d seconds.' % wait
-                
+                t['Retry-After'] = wait
                 return t
-                
-            count = cache.get(ident, 1)
-            cache.set(ident, count+1)
-            
-            if count >= max_requests:
-                cache.set(ts_key, offset, timeout)
-                cache.set(ident, 1)
+
+            cache.set(ident, (count+1, expiration), (expiration - now))
     
         return f(self, request, *args, **kwargs)
     return wrap
@@ -135,6 +162,20 @@ def coerce_put_post(request):
     in mod_python. This should fix it.
     """
     if request.method == "PUT":
+        # Bug fix: if _load_post_and_files has already been called, for
+        # example by middleware accessing request.POST, the below code to
+        # pretend the request is a POST instead of a PUT will be too late
+        # to make a difference. Also calling _load_post_and_files will result 
+        # in the following exception:
+        #   AttributeError: You cannot set the upload handlers after the upload has been processed.
+        # The fix is to check for the presence of the _post field which is set 
+        # the first time _load_post_and_files is called (both by wsgi.py and 
+        # modpython.py). If it's set, the request has to be 'reset' to redo
+        # the query value parsing in POST mode.
+        if hasattr(request, '_post'):
+            del request._post
+            del request._files
+        
         try:
             request.method = "POST"
             request._load_post_and_files()
@@ -176,7 +217,7 @@ class Mimer(object):
             for mime in mimes:
                 if ctype.startswith(mime):
                     return loadee
-
+                    
     def content_type(self):
         """
         Returns the content type of the request in all cases where it is
@@ -186,11 +227,10 @@ class Mimer(object):
 
         ctype = self.request.META.get('CONTENT_TYPE', type_formencoded)
         
-        if ctype.startswith(type_formencoded):
+        if type_formencoded in ctype:
             return None
         
         return ctype
-        
 
     def translate(self):
         """
@@ -211,14 +251,18 @@ class Mimer(object):
         if not self.is_multipart() and ctype:
             loadee = self.loader_for_type(ctype)
             
-            try:
-                self.request.data = loadee(self.request.raw_post_data)
-                
-                # Reset both POST and PUT from request, as its
-                # misleading having their presence around.
-                self.request.POST = self.request.PUT = dict()
-            except (TypeError, ValueError):
-                raise MimerDataException
+            if loadee:
+                try:
+                    self.request.data = loadee(self.request.raw_post_data)
+                        
+                    # Reset both POST and PUT from request, as its
+                    # misleading having their presence around.
+                    self.request.POST = self.request.PUT = dict()
+                except (TypeError, ValueError):
+                    # This also catches if loadee is None.
+                    raise MimerDataException
+            else:
+                self.request.data = None
 
         return self.request
                 
@@ -260,3 +304,48 @@ def require_mime(*mimes):
 
 require_extended = require_mime('json', 'yaml', 'xml', 'pickle')
     
+def send_consumer_mail(consumer):
+    """
+    Send a consumer an email depending on what their status is.
+    """
+    try:
+        subject = settings.PISTON_OAUTH_EMAIL_SUBJECTS[consumer.status]
+    except AttributeError:
+        subject = "Your API Consumer for %s " % Site.objects.get_current().name
+        if consumer.status == "accepted":
+            subject += "was accepted!"
+        elif consumer.status == "canceled":
+            subject += "has been canceled."
+        elif consumer.status == "rejected":
+            subject += "has been rejected."
+        else: 
+            subject += "is awaiting approval."
+
+    template = "piston/mails/consumer_%s.txt" % consumer.status    
+    
+    try:
+        body = loader.render_to_string(template, 
+            { 'consumer' : consumer, 'user' : consumer.user })
+    except TemplateDoesNotExist:
+        """ 
+        They haven't set up the templates, which means they might not want
+        these emails sent.
+        """
+        return 
+
+    try:
+        sender = settings.PISTON_FROM_EMAIL
+    except AttributeError:
+        sender = settings.DEFAULT_FROM_EMAIL
+
+    if consumer.user:
+        send_mail(_(subject), body, sender, [consumer.user.email], fail_silently=True)
+
+    if consumer.status == 'pending' and len(settings.ADMINS):
+        mail_admins(_(subject), body, fail_silently=True)
+
+    if settings.DEBUG and consumer.user:
+        print "Mail being sent, to=%s" % consumer.user.email
+        print "Subject: %s" % _(subject)
+        print body
+
diff --git a/lib/piston/validate_jsonp.py b/lib/piston/validate_jsonp.py
new file mode 100644
index 0000000..10fba21
--- /dev/null
+++ b/lib/piston/validate_jsonp.py
@@ -0,0 +1,210 @@
+# -*- coding: utf-8 -*-
+
+# Placed into the Public Domain by tav <tav espians com>
+
+"""Validate Javascript Identifiers for use as JSON-P callback parameters."""
+
+import re
+from unicodedata import category
+
+# ------------------------------------------------------------------------------
+# javascript identifier unicode categories and "exceptional" chars
+# ------------------------------------------------------------------------------
+
+valid_jsid_categories_start = frozenset([
+    'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl'
+    ])
+
+valid_jsid_categories = frozenset([
+    'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl', 'Mn', 'Mc', 'Nd', 'Pc'
+    ])
+
+valid_jsid_chars = ('$', '_')
+
+# ------------------------------------------------------------------------------
+# regex to find array[index] patterns
+# ------------------------------------------------------------------------------
+
+array_index_regex = re.compile(r'\[[0-9]+\]$')
+
+has_valid_array_index = array_index_regex.search
+replace_array_index = array_index_regex.sub
+
+# ------------------------------------------------------------------------------
+# javascript reserved words -- including keywords and null/boolean literals
+# ------------------------------------------------------------------------------
+
+is_reserved_js_word = frozenset([
+
+    'abstract', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class',
+    'const', 'continue', 'debugger', 'default', 'delete', 'do', 'double',
+    'else', 'enum', 'export', 'extends', 'false', 'final', 'finally', 'float',
+    'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof',
+    'int', 'interface', 'long', 'native', 'new', 'null', 'package', 'private',
+    'protected', 'public', 'return', 'short', 'static', 'super', 'switch',
+    'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try',
+    'typeof', 'var', 'void', 'volatile', 'while', 'with',
+
+    # potentially reserved in a future version of the ES5 standard
+    # 'let', 'yield'
+    
+    ]).__contains__
+
+# ------------------------------------------------------------------------------
+# the core validation functions
+# ------------------------------------------------------------------------------
+
+def is_valid_javascript_identifier(identifier, escape=r'\u', ucd_cat=category):
+    """Return whether the given ``id`` is a valid Javascript identifier."""
+
+    if not identifier:
+        return False
+
+    if not isinstance(identifier, unicode):
+        try:
+            identifier = unicode(identifier, 'utf-8')
+        except UnicodeDecodeError:
+            return False
+
+    if escape in identifier:
+
+        new = []; add_char = new.append
+        split_id = identifier.split(escape)
+        add_char(split_id.pop(0))
+
+        for segment in split_id:
+            if len(segment) < 4:
+                return False
+            try:
+                add_char(unichr(int('0x' + segment[:4], 16)))
+            except Exception:
+                return False
+            add_char(segment[4:])
+            
+        identifier = u''.join(new)
+
+    if is_reserved_js_word(identifier):
+        return False
+
+    first_char = identifier[0]
+
+    if not ((first_char in valid_jsid_chars) or
+            (ucd_cat(first_char) in valid_jsid_categories_start)):
+        return False
+
+    for char in identifier[1:]:
+        if not ((char in valid_jsid_chars) or
+                (ucd_cat(char) in valid_jsid_categories)):
+            return False
+
+    return True
+
+
+def is_valid_jsonp_callback_value(value):
+    """Return whether the given ``value`` can be used as a JSON-P callback."""
+
+    for identifier in value.split(u'.'):
+        while '[' in identifier:
+            if not has_valid_array_index(identifier):
+                return False
+            identifier = replace_array_index(u'', identifier)
+        if not is_valid_javascript_identifier(identifier):
+            return False
+
+    return True
+
+# ------------------------------------------------------------------------------
+# test
+# ------------------------------------------------------------------------------
+
+def test():
+    """
+    The function ``is_valid_javascript_identifier`` validates a given identifier
+    according to the latest draft of the ECMAScript 5 Specification:
+
+      >>> is_valid_javascript_identifier('hello')
+      True
+
+      >>> is_valid_javascript_identifier('alert()')
+      False
+
+      >>> is_valid_javascript_identifier('a-b')
+      False
+
+      >>> is_valid_javascript_identifier('23foo')
+      False
+
+      >>> is_valid_javascript_identifier('foo23')
+      True
+
+      >>> is_valid_javascript_identifier('$210')
+      True
+
+      >>> is_valid_javascript_identifier(u'Stra\u00dfe')
+      True
+
+      >>> is_valid_javascript_identifier(r'\u0062') # u'b'
+      True
+
+      >>> is_valid_javascript_identifier(r'\u62')
+      False
+
+      >>> is_valid_javascript_identifier(r'\u0020')
+      False
+
+      >>> is_valid_javascript_identifier('_bar')
+      True
+
+      >>> is_valid_javascript_identifier('some_var')
+      True
+
+      >>> is_valid_javascript_identifier('$')
+      True
+
+    But ``is_valid_jsonp_callback_value`` is the function you want to use for
+    validating JSON-P callback parameter values:
+
+      >>> is_valid_jsonp_callback_value('somevar')
+      True
+
+      >>> is_valid_jsonp_callback_value('function')
+      False
+
+      >>> is_valid_jsonp_callback_value(' somevar')
+      False
+
+    It supports the possibility of '.' being present in the callback name, e.g.
+
+      >>> is_valid_jsonp_callback_value('$.ajaxHandler')
+      True
+
+      >>> is_valid_jsonp_callback_value('$.23')
+      False
+
+    As well as the pattern of providing an array index lookup, e.g.
+
+      >>> is_valid_jsonp_callback_value('array_of_functions[42]')
+      True
+
+      >>> is_valid_jsonp_callback_value('array_of_functions[42][1]')
+      True
+
+      >>> is_valid_jsonp_callback_value('$.ajaxHandler[42][1].foo')
+      True
+
+      >>> is_valid_jsonp_callback_value('array_of_functions[42]foo[1]')
+      False
+
+      >>> is_valid_jsonp_callback_value('array_of_functions[]')
+      False
+
+      >>> is_valid_jsonp_callback_value('array_of_functions["key"]')
+      False
+
+    Enjoy!
+
+    """
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
\ No newline at end of file



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]