mirror of
https://github.com/kennethreitz-archive/django-piston-xauth.git
synced 2026-06-17 14:01:00 +00:00
305 lines
10 KiB
Python
305 lines
10 KiB
Python
import sys, inspect
|
|
|
|
from django.http import (HttpResponse, Http404, HttpResponseNotAllowed,
|
|
HttpResponseForbidden, HttpResponseServerError)
|
|
from django.views.debug import ExceptionReporter
|
|
from django.views.decorators.vary import vary_on_headers
|
|
from django.conf import settings
|
|
from django.core.mail import EmailMultiAlternatives
|
|
from django.db.models.query import QuerySet
|
|
from django.http import Http404
|
|
|
|
from emitters import Emitter
|
|
from handler import typemapper
|
|
from doc import HandlerMethod
|
|
from authentication import NoAuthentication
|
|
from utils import coerce_put_post, FormValidationError, HttpStatusCode
|
|
from utils import rc, format_error, translate_mime, MimerDataException
|
|
from utils import head_guard
|
|
|
|
CHALLENGE = object()
|
|
|
|
class Resource(object):
|
|
"""
|
|
Resource. Create one for your URL mappings, just
|
|
like you would with Django. Takes one argument,
|
|
the handler. The second argument is optional, and
|
|
is an authentication handler. If not specified,
|
|
`NoAuthentication` will be used by default.
|
|
"""
|
|
callmap = { 'GET': 'read', 'POST': 'create',
|
|
'PUT': 'update', 'DELETE': 'delete',
|
|
'HEAD': 'meta' }
|
|
|
|
def __init__(self, handler, authentication=None):
|
|
if not callable(handler):
|
|
raise AttributeError, "Handler not callable."
|
|
|
|
self.handler = handler()
|
|
self.csrf_exempt = getattr(self.handler, 'csrf_exempt', True)
|
|
|
|
if not authentication:
|
|
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)
|
|
self.stream = getattr(settings, 'PISTON_STREAM_OUTPUT', False)
|
|
|
|
def determine_emitter(self, request, *args, **kwargs):
|
|
"""
|
|
Function for determening which emitter to use
|
|
for output. It lives here so you can easily subclass
|
|
`Resource` in order to change how emission is detected.
|
|
|
|
You could also check for the `Accept` HTTP header here,
|
|
since that pretty much makes sense. Refer to `Mimer` for
|
|
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):
|
|
"""
|
|
Gets the anonymous handler. Also tries to grab a class
|
|
if the `anonymous` value is a string, so that we can define
|
|
anonymous handlers that aren't defined yet (like, when
|
|
you're subclassing your basehandler into an anonymous one.)
|
|
"""
|
|
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
|
|
|
|
@head_guard
|
|
@vary_on_headers('Authorization')
|
|
def __call__(self, request, *args, **kwargs):
|
|
"""
|
|
NB: Sends a `Vary` header so we don't cache requests
|
|
that are different (OAuth stuff in `Authorization` header.)
|
|
"""
|
|
rm = request.method.upper()
|
|
|
|
# Django's internal mechanism doesn't pick up
|
|
# PUT request, so we trick it a little here.
|
|
if rm == "PUT":
|
|
coerce_put_post(request)
|
|
|
|
actor, anonymous = self.authenticate(request, rm)
|
|
|
|
if anonymous is CHALLENGE:
|
|
return actor()
|
|
else:
|
|
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
|
|
|
|
# Support emitter both through (?P<emitter_format>) and ?format=emitter.
|
|
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 Exception, e:
|
|
result = self.error_handler(e, request, meth, em_format)
|
|
|
|
try:
|
|
emitter, ct = Emitter.get(em_format)
|
|
fields = handler.fields
|
|
|
|
if hasattr(handler, 'list_fields') and isinstance(result, (list, tuple, QuerySet)):
|
|
fields = handler.list_fields
|
|
except ValueError:
|
|
result = rc.BAD_REQUEST
|
|
result.content = "Invalid output format specified '%s'." % em_format
|
|
return result
|
|
|
|
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:
|
|
"""
|
|
Decide whether or not we want a generator here,
|
|
or we just want to buffer up the entire result
|
|
before sending it to the client. Won't matter for
|
|
smaller datasets, but larger will have an impact.
|
|
"""
|
|
if self.stream: stream = srl.stream_render(request)
|
|
else: stream = srl.render(request)
|
|
|
|
if not isinstance(stream, HttpResponse):
|
|
resp = HttpResponse(stream, mimetype=ct, status=status_code)
|
|
else:
|
|
resp = stream
|
|
|
|
resp.streaming = self.stream
|
|
|
|
return resp
|
|
except HttpStatusCode, e:
|
|
return e.response
|
|
|
|
@staticmethod
|
|
def cleanup_request(request):
|
|
"""
|
|
Removes `oauth_` keys from various dicts on the
|
|
request object, and returns the sanitized version.
|
|
"""
|
|
for method_type in ('GET', 'PUT', 'POST', 'DELETE'):
|
|
block = getattr(request, method_type, { })
|
|
|
|
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()
|
|
|
|
email_text = "This message is only visible in HTML view."
|
|
|
|
email = EmailMultiAlternatives(subject, email_text, settings.SERVER_EMAIL, [ admin[1] for admin in settings.ADMINS ])
|
|
email.attach_alternative(html, "text/html")
|
|
email.send(fail_silently=True)
|
|
|
|
|
|
def error_handler(self, e, request, meth, em_format):
|
|
"""
|
|
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
|