diff --git a/entries/from_url.py b/entries/from_url.py index d842b83..b19475f 100644 --- a/entries/from_url.py +++ b/entries/from_url.py @@ -3,7 +3,8 @@ from urllib.parse import urlparse from django.contrib.sites.models import Site from django.http import HttpResponse from django.urls import resolve, Resolver404 -from micropub import error +from micropub.views import error +from lemoncurry.middleware import ResponseException from .models import Entry @@ -11,24 +12,24 @@ from .models import Entry def from_url(url: str) -> Entry: domain = Site.objects.get_current().domain if not url: - raise error.bad_req('url parameter required') + raise ResponseException(error.bad_req('url parameter required')) if '//' not in url: url = '//' + url parts = urlparse(url, scheme='https') if parts.scheme not in ('http', 'https') or parts.netloc != domain: - raise error.bad_req('url does not point to this site') + raise ResponseException(error.bad_req('url does not point to this site')) try: match = resolve(parts.path) except Resolver404: - raise error.bad_req('url does not point to a valid page on this site') + raise ResponseException(error.bad_req('url does not point to a valid page on this site')) if match.view_name != 'entries:entry': - raise error.bad_req('url does not point to an entry on this site') + raise ResponseException(error.bad_req('url does not point to an entry on this site')) try: entry = Entry.objects.get(pk=match.kwargs['id']) except Entry.DoesNotExist: - raise error.bad_req('url does not point to an existing entry') + raise ResponseException(error.bad_req('url does not point to an existing entry')) return entry diff --git a/entries/pagination.py b/entries/pagination.py index aa7c174..ab3763e 100644 --- a/entries/pagination.py +++ b/entries/pagination.py @@ -1,8 +1,6 @@ from django.core.paginator import Paginator from django.shortcuts import redirect -from lemoncurry.middleware import ResponseException - def paginate(queryset, reverse, page): class Page: @@ -20,7 +18,7 @@ def paginate(queryset, reverse, page): # If the first page was requested, redirect to the clean version of the URL # with no page suffix. if page == 1: - raise ResponseException(redirect(Page(1).url)) + return redirect(Page(1).url) paginator = Paginator(queryset, 10) entries = paginator.page(page or 1) diff --git a/entries/views/lists.py b/entries/views/lists.py index c177670..35afd08 100644 --- a/entries/views/lists.py +++ b/entries/views/lists.py @@ -9,6 +9,8 @@ from ..pagination import paginate def by_kind(request, kind, page=None): entries = Entry.objects.filter(kind=kind.id) entries = paginate(queryset=entries, reverse=kind.index_page, page=page) + if hasattr(entries, 'content'): + return entries return { 'entries': entries, @@ -29,6 +31,8 @@ def by_cat(request, slug, page=None): cat = get_object_or_404(Cat, slug=slug) entries = cat.entries.all() entries = paginate(queryset=entries, reverse=url, page=page) + if hasattr(entries, 'content'): + return entries return { 'entries': entries, diff --git a/home/views.py b/home/views.py index d5ddea0..bb414e4 100644 --- a/home/views.py +++ b/home/views.py @@ -24,6 +24,10 @@ def index(request, page=None): entries = user.entries.filter(kind__in=kinds.on_home) entries = pagination.paginate(queryset=entries, reverse=url, page=page) + # If we got a valid HTTP response, just return it without rendering. + if hasattr(entries, 'content'): + return entries + return { 'user': user, 'entries': entries, diff --git a/lemonauth/tokens.py b/lemonauth/tokens.py index 5e6640e..8749e93 100644 --- a/lemonauth/tokens.py +++ b/lemonauth/tokens.py @@ -1,26 +1,28 @@ -from micropub import error +from micropub.views import error from .models import IndieAuthCode, Token -def auth(request) -> Token: +def auth(request): if 'HTTP_AUTHORIZATION' in request.META: auth = request.META.get('HTTP_AUTHORIZATION').split(' ') if auth[0] != 'Bearer': - raise error.bad_req('auth type {0} not supported'.format(auth[0])) + return error.bad_req('auth type {0} not supported'.format(auth[0])) if len(auth) != 2: - raise error.bad_req('invalid Bearer auth format, must be Bearer ') + return error.bad_req( + 'invalid Bearer auth format, must be Bearer ' + ) token = auth[1] elif 'access_token' in request.POST: token = request.POST.get('access_token') elif 'access_token' in request.GET: token = request.GET.get('access_token') else: - raise error.unauthorized() + return error.unauthorized() try: token = Token.objects.get(pk=token) except Token.DoesNotExist: - raise error.forbidden() + return error.forbidden() return token diff --git a/lemonauth/views/token.py b/lemonauth/views/token.py index 251c016..72543f0 100644 --- a/lemonauth/views/token.py +++ b/lemonauth/views/token.py @@ -11,6 +11,8 @@ from lemoncurry import utils class TokenView(View): def get(self, req): token = tokens.auth(req) + if hasattr(token, 'content'): + return token res = { 'me': token.me, 'client_id': token.client_id, diff --git a/micropub/error.py b/micropub/error.py deleted file mode 100644 index a5e9373..0000000 --- a/micropub/error.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.http import JsonResponse -from lemoncurry.middleware import ResponseException -from typing import Optional - - -def forbidden() -> ResponseException: - return res('forbidden', 403) - - -def unauthorized() -> ResponseException: - return res('unauthorized', 401) - - -def bad_req(msg: str) -> ResponseException: - return res('invalid_request', msg=msg) - - -def bad_type(type: str) -> ResponseException: - msg = 'unsupported request type {0}'.format(type) - return res('invalid_request', 415, msg) - - -def bad_scope(scope: str) -> ResponseException: - return res('insufficient_scope', 401, scope=scope) - - -def res(error: str, - status: Optional[int]=400, - msg: Optional[str]=None, - scope: Optional[str]=None): - content = {'error': error} - if msg is not None: - content['error_description'] = msg - if scope: - content['scope'] = scope - return ResponseException(JsonResponse(content, status=status)) diff --git a/micropub/views/__init__.py b/micropub/views/__init__.py index 7ff7878..f487986 100644 --- a/micropub/views/__init__.py +++ b/micropub/views/__init__.py @@ -18,7 +18,10 @@ actions = { @csrf_exempt @require_http_methods(['GET', 'HEAD', 'POST']) def micropub(request): - request.token = tokens.auth(request) + token = tokens.auth(request) + if hasattr(token, 'content'): + return token + request.token = token if request.method in ('GET', 'HEAD'): return query(request) @@ -26,6 +29,6 @@ def micropub(request): if request.content_type == 'application/json': request.json = json.load(request) action = request.json.get('action', 'create') - if action not in actions: - raise error.bad_req('unknown action: {}'.format(action)) - return actions[action](request) + if action in actions: + return actions[action](request) + return error.bad_req('unknown action: {}'.format(action)) diff --git a/micropub/views/create.py b/micropub/views/create.py index 23bd21e..6c4cf21 100644 --- a/micropub/views/create.py +++ b/micropub/views/create.py @@ -7,7 +7,7 @@ from entries.models import Cat, Entry from entries.kinds import Article, Note, Reply, Like, Repost from lemoncurry import utils -from .. import error +from . import error def form_to_mf2(request): @@ -33,14 +33,14 @@ def create(request): 'application/x-www-form-urlencoded': form_to_mf2, } if 'create' not in request.token: - raise error.bad_scope('create') + return error.bad_scope('create') if request.content_type not in normalise: - raise error.unsupported_type(request.content_type) + return error.unsupported_type(request.content_type) body = normalise[request.content_type](request) if 'type' not in body: - raise error.bad_req('mf2 object type required') + return error.bad_req('mf2 object type required') if body['type'] != ['h-entry']: - raise error.bad_req('only h-entry supported') + return error.bad_req('only h-entry supported') entry = Entry(author=request.token.user) props = body.get('properties', {}) diff --git a/micropub/views/delete.py b/micropub/views/delete.py index 035da7e..4fc1f3e 100644 --- a/micropub/views/delete.py +++ b/micropub/views/delete.py @@ -4,7 +4,7 @@ from ronkyuu import webmention from entries.from_url import from_url from entries.jobs import ping_hub, send_mentions -from .. import error +from . import error def delete(request): normalise = { @@ -12,14 +12,14 @@ def delete(request): 'application/x-www-form-urlencoded': lambda r: r.POST.get('url'), } if 'delete' not in request.token: - raise error.bad_scope('delete') + return error.bad_scope('delete') if request.content_type not in normalise: - raise error.unsupported_type(request.content_type) + return error.unsupported_type(request.content_type) url = normalise[request.content_type](request) entry = from_url(url) if entry.author != request.token.user: - raise error.forbid('entry belongs to another user') + return error.forbid('entry belongs to another user') perma = entry.absolute_url pings = entry.affected_urls diff --git a/micropub/views/error.py b/micropub/views/error.py new file mode 100644 index 0000000..1c7b1a8 --- /dev/null +++ b/micropub/views/error.py @@ -0,0 +1,31 @@ +from django.http import JsonResponse + + +def forbidden(): + return res('forbidden', 403) + + +def unauthorized(): + return res('unauthorized', 401) + + +def bad_req(msg): + return res('invalid_request', msg=msg) + + +def bad_type(type): + msg = 'unsupported request type {0}'.format(type) + return res('invalid_request', 415, msg) + + +def bad_scope(scope): + return res('insufficient_scope', 401, scope=scope) + + +def res(error, status=400, msg=None, scope=None): + content = {'error': error} + if msg: + content['error_description'] = msg + if scope: + content['scope'] = scope + return JsonResponse(content, status=status) diff --git a/micropub/views/media.py b/micropub/views/media.py index db59972..5fe3128 100644 --- a/micropub/views/media.py +++ b/micropub/views/media.py @@ -8,7 +8,7 @@ import magic from lemonauth import tokens from lemoncurry.utils import absolute_url -from .. import error +from . import error ACCEPTED_MEDIA_TYPES = ( 'image/gif', @@ -21,13 +21,15 @@ ACCEPTED_MEDIA_TYPES = ( @require_POST def media(request): token = tokens.auth(request) + if hasattr(token, 'content'): + return token if 'file' not in request.FILES: - raise error.bad_req( + return error.bad_req( "a file named 'file' must be provided to the media endpoint" ) file = request.FILES['file'] if file.content_type not in ACCEPTED_MEDIA_TYPES: - raise error.bad_req( + return error.bad_req( 'unacceptable file type {0}'.format(file.content_type) ) @@ -39,7 +41,7 @@ def media(request): sha.update(chunk) if mime != file.content_type: - raise error.bad_req( + return error.bad_req( 'detected file type {0} did not match specified file type {1}' .format(mime, file.content_type) ) diff --git a/micropub/views/query.py b/micropub/views/query.py index ded7775..0bbda24 100644 --- a/micropub/views/query.py +++ b/micropub/views/query.py @@ -2,7 +2,7 @@ from django.http import JsonResponse from django.urls import reverse from lemoncurry import requests from lemoncurry.utils import absolute_url -from .. import error +from . import error def config(request): @@ -14,10 +14,10 @@ def config(request): def source(request): get = request.GET if 'url' not in get: - raise error.bad_req('must specify url parameter for source query') + return error.bad_req('must specify url parameter for source query') mf2 = requests.mf2(get['url']).to_dict(filter_by_type='h-entry') if not mf2: - raise error.bad_req('no h-entry at the requested url') + return error.bad_req('no h-entry at the requested url') entry = mf2[0] keys = get.getlist('properties', []) + get.getlist('properties[]', []) if not keys: @@ -40,9 +40,11 @@ queries = { def query(request): if 'q' not in request.GET: - raise error.bad_req('must specify q parameter') + return error.bad_req('must specify q parameter') q = request.GET['q'] if q not in queries: - raise error.bad_req('unsupported query {0}'.format(q)) + return error.bad_req('unsupported query {0}'.format(q)) res = queries[q](request) + if hasattr(res, 'content'): + return res return JsonResponse(res)