diff --git a/entries/from_url.py b/entries/from_url.py index b19475f..d842b83 100644 --- a/entries/from_url.py +++ b/entries/from_url.py @@ -3,8 +3,7 @@ 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.views import error -from lemoncurry.middleware import ResponseException +from micropub import error from .models import Entry @@ -12,24 +11,24 @@ from .models import Entry def from_url(url: str) -> Entry: domain = Site.objects.get_current().domain if not url: - raise ResponseException(error.bad_req('url parameter required')) + raise 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 ResponseException(error.bad_req('url does not point to this site')) + raise error.bad_req('url does not point to this site') try: match = resolve(parts.path) except Resolver404: - raise ResponseException(error.bad_req('url does not point to a valid page on this site')) + raise error.bad_req('url does not point to a valid page on this site') if match.view_name != 'entries:entry': - raise ResponseException(error.bad_req('url does not point to an entry on this site')) + raise 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 ResponseException(error.bad_req('url does not point to an existing entry')) + raise error.bad_req('url does not point to an existing entry') return entry diff --git a/entries/pagination.py b/entries/pagination.py index ab3763e..aa7c174 100644 --- a/entries/pagination.py +++ b/entries/pagination.py @@ -1,6 +1,8 @@ from django.core.paginator import Paginator from django.shortcuts import redirect +from lemoncurry.middleware import ResponseException + def paginate(queryset, reverse, page): class Page: @@ -18,7 +20,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: - return redirect(Page(1).url) + raise ResponseException(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 35afd08..c177670 100644 --- a/entries/views/lists.py +++ b/entries/views/lists.py @@ -9,8 +9,6 @@ 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, @@ -31,8 +29,6 @@ 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 bb414e4..d5ddea0 100644 --- a/home/views.py +++ b/home/views.py @@ -24,10 +24,6 @@ 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 8749e93..5e6640e 100644 --- a/lemonauth/tokens.py +++ b/lemonauth/tokens.py @@ -1,28 +1,26 @@ -from micropub.views import error +from micropub import error from .models import IndieAuthCode, Token -def auth(request): +def auth(request) -> Token: if 'HTTP_AUTHORIZATION' in request.META: auth = request.META.get('HTTP_AUTHORIZATION').split(' ') if auth[0] != 'Bearer': - return error.bad_req('auth type {0} not supported'.format(auth[0])) + raise error.bad_req('auth type {0} not supported'.format(auth[0])) if len(auth) != 2: - return error.bad_req( - 'invalid Bearer auth format, must be Bearer ' - ) + raise 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: - return error.unauthorized() + raise error.unauthorized() try: token = Token.objects.get(pk=token) except Token.DoesNotExist: - return error.forbidden() + raise error.forbidden() return token diff --git a/lemonauth/views/token.py b/lemonauth/views/token.py index 72543f0..251c016 100644 --- a/lemonauth/views/token.py +++ b/lemonauth/views/token.py @@ -11,8 +11,6 @@ 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 new file mode 100644 index 0000000..a5e9373 --- /dev/null +++ b/micropub/error.py @@ -0,0 +1,36 @@ +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 f487986..7ff7878 100644 --- a/micropub/views/__init__.py +++ b/micropub/views/__init__.py @@ -18,10 +18,7 @@ actions = { @csrf_exempt @require_http_methods(['GET', 'HEAD', 'POST']) def micropub(request): - token = tokens.auth(request) - if hasattr(token, 'content'): - return token - request.token = token + request.token = tokens.auth(request) if request.method in ('GET', 'HEAD'): return query(request) @@ -29,6 +26,6 @@ def micropub(request): if request.content_type == 'application/json': request.json = json.load(request) action = request.json.get('action', 'create') - if action in actions: - return actions[action](request) - return error.bad_req('unknown action: {}'.format(action)) + if action not in actions: + raise error.bad_req('unknown action: {}'.format(action)) + return actions[action](request) diff --git a/micropub/views/create.py b/micropub/views/create.py index 6c4cf21..23bd21e 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: - return error.bad_scope('create') + raise error.bad_scope('create') if request.content_type not in normalise: - return error.unsupported_type(request.content_type) + raise error.unsupported_type(request.content_type) body = normalise[request.content_type](request) if 'type' not in body: - return error.bad_req('mf2 object type required') + raise error.bad_req('mf2 object type required') if body['type'] != ['h-entry']: - return error.bad_req('only h-entry supported') + raise 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 4fc1f3e..035da7e 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: - return error.bad_scope('delete') + raise error.bad_scope('delete') if request.content_type not in normalise: - return error.unsupported_type(request.content_type) + raise error.unsupported_type(request.content_type) url = normalise[request.content_type](request) entry = from_url(url) if entry.author != request.token.user: - return error.forbid('entry belongs to another user') + raise 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 deleted file mode 100644 index 1c7b1a8..0000000 --- a/micropub/views/error.py +++ /dev/null @@ -1,31 +0,0 @@ -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 5fe3128..db59972 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,15 +21,13 @@ ACCEPTED_MEDIA_TYPES = ( @require_POST def media(request): token = tokens.auth(request) - if hasattr(token, 'content'): - return token if 'file' not in request.FILES: - return error.bad_req( + raise 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: - return error.bad_req( + raise error.bad_req( 'unacceptable file type {0}'.format(file.content_type) ) @@ -41,7 +39,7 @@ def media(request): sha.update(chunk) if mime != file.content_type: - return error.bad_req( + raise 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 0bbda24..ded7775 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: - return error.bad_req('must specify url parameter for source query') + raise 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: - return error.bad_req('no h-entry at the requested url') + raise error.bad_req('no h-entry at the requested url') entry = mf2[0] keys = get.getlist('properties', []) + get.getlist('properties[]', []) if not keys: @@ -40,11 +40,9 @@ queries = { def query(request): if 'q' not in request.GET: - return error.bad_req('must specify q parameter') + raise error.bad_req('must specify q parameter') q = request.GET['q'] if q not in queries: - return error.bad_req('unsupported query {0}'.format(q)) + raise error.bad_req('unsupported query {0}'.format(q)) res = queries[q](request) - if hasattr(res, 'content'): - return res return JsonResponse(res)