Compare commits
No commits in common. "d68dda85ad855e9aba6fff7dc7c0b750a56def6e" and "7d17a92793c0c9a191f785bafce6986232957f49" have entirely different histories.
d68dda85ad
...
7d17a92793
13 changed files with 86 additions and 73 deletions
|
@ -3,7 +3,8 @@ from urllib.parse import urlparse
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.urls import resolve, Resolver404
|
from django.urls import resolve, Resolver404
|
||||||
from micropub import error
|
from micropub.views import error
|
||||||
|
from lemoncurry.middleware import ResponseException
|
||||||
|
|
||||||
from .models import Entry
|
from .models import Entry
|
||||||
|
|
||||||
|
@ -11,24 +12,24 @@ from .models import Entry
|
||||||
def from_url(url: str) -> Entry:
|
def from_url(url: str) -> Entry:
|
||||||
domain = Site.objects.get_current().domain
|
domain = Site.objects.get_current().domain
|
||||||
if not url:
|
if not url:
|
||||||
raise error.bad_req('url parameter required')
|
raise ResponseException(error.bad_req('url parameter required'))
|
||||||
if '//' not in url:
|
if '//' not in url:
|
||||||
url = '//' + url
|
url = '//' + url
|
||||||
parts = urlparse(url, scheme='https')
|
parts = urlparse(url, scheme='https')
|
||||||
if parts.scheme not in ('http', 'https') or parts.netloc != domain:
|
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:
|
try:
|
||||||
match = resolve(parts.path)
|
match = resolve(parts.path)
|
||||||
except Resolver404:
|
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':
|
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:
|
try:
|
||||||
entry = Entry.objects.get(pk=match.kwargs['id'])
|
entry = Entry.objects.get(pk=match.kwargs['id'])
|
||||||
except Entry.DoesNotExist:
|
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
|
return entry
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
from lemoncurry.middleware import ResponseException
|
|
||||||
|
|
||||||
|
|
||||||
def paginate(queryset, reverse, page):
|
def paginate(queryset, reverse, page):
|
||||||
class 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
|
# If the first page was requested, redirect to the clean version of the URL
|
||||||
# with no page suffix.
|
# with no page suffix.
|
||||||
if page == 1:
|
if page == 1:
|
||||||
raise ResponseException(redirect(Page(1).url))
|
return redirect(Page(1).url)
|
||||||
|
|
||||||
paginator = Paginator(queryset, 10)
|
paginator = Paginator(queryset, 10)
|
||||||
entries = paginator.page(page or 1)
|
entries = paginator.page(page or 1)
|
||||||
|
|
|
@ -9,6 +9,8 @@ from ..pagination import paginate
|
||||||
def by_kind(request, kind, page=None):
|
def by_kind(request, kind, page=None):
|
||||||
entries = Entry.objects.filter(kind=kind.id)
|
entries = Entry.objects.filter(kind=kind.id)
|
||||||
entries = paginate(queryset=entries, reverse=kind.index_page, page=page)
|
entries = paginate(queryset=entries, reverse=kind.index_page, page=page)
|
||||||
|
if hasattr(entries, 'content'):
|
||||||
|
return entries
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'entries': entries,
|
'entries': entries,
|
||||||
|
@ -29,6 +31,8 @@ def by_cat(request, slug, page=None):
|
||||||
cat = get_object_or_404(Cat, slug=slug)
|
cat = get_object_or_404(Cat, slug=slug)
|
||||||
entries = cat.entries.all()
|
entries = cat.entries.all()
|
||||||
entries = paginate(queryset=entries, reverse=url, page=page)
|
entries = paginate(queryset=entries, reverse=url, page=page)
|
||||||
|
if hasattr(entries, 'content'):
|
||||||
|
return entries
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'entries': entries,
|
'entries': entries,
|
||||||
|
|
|
@ -24,6 +24,10 @@ def index(request, page=None):
|
||||||
entries = user.entries.filter(kind__in=kinds.on_home)
|
entries = user.entries.filter(kind__in=kinds.on_home)
|
||||||
entries = pagination.paginate(queryset=entries, reverse=url, page=page)
|
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 {
|
return {
|
||||||
'user': user,
|
'user': user,
|
||||||
'entries': entries,
|
'entries': entries,
|
||||||
|
|
|
@ -1,26 +1,28 @@
|
||||||
from micropub import error
|
from micropub.views import error
|
||||||
from .models import IndieAuthCode, Token
|
from .models import IndieAuthCode, Token
|
||||||
|
|
||||||
|
|
||||||
def auth(request) -> Token:
|
def auth(request):
|
||||||
if 'HTTP_AUTHORIZATION' in request.META:
|
if 'HTTP_AUTHORIZATION' in request.META:
|
||||||
auth = request.META.get('HTTP_AUTHORIZATION').split(' ')
|
auth = request.META.get('HTTP_AUTHORIZATION').split(' ')
|
||||||
if auth[0] != 'Bearer':
|
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:
|
if len(auth) != 2:
|
||||||
raise error.bad_req('invalid Bearer auth format, must be Bearer <token>')
|
return error.bad_req(
|
||||||
|
'invalid Bearer auth format, must be Bearer <token>'
|
||||||
|
)
|
||||||
token = auth[1]
|
token = auth[1]
|
||||||
elif 'access_token' in request.POST:
|
elif 'access_token' in request.POST:
|
||||||
token = request.POST.get('access_token')
|
token = request.POST.get('access_token')
|
||||||
elif 'access_token' in request.GET:
|
elif 'access_token' in request.GET:
|
||||||
token = request.GET.get('access_token')
|
token = request.GET.get('access_token')
|
||||||
else:
|
else:
|
||||||
raise error.unauthorized()
|
return error.unauthorized()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
token = Token.objects.get(pk=token)
|
token = Token.objects.get(pk=token)
|
||||||
except Token.DoesNotExist:
|
except Token.DoesNotExist:
|
||||||
raise error.forbidden()
|
return error.forbidden()
|
||||||
|
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ from lemoncurry import utils
|
||||||
class TokenView(View):
|
class TokenView(View):
|
||||||
def get(self, req):
|
def get(self, req):
|
||||||
token = tokens.auth(req)
|
token = tokens.auth(req)
|
||||||
|
if hasattr(token, 'content'):
|
||||||
|
return token
|
||||||
res = {
|
res = {
|
||||||
'me': token.me,
|
'me': token.me,
|
||||||
'client_id': token.client_id,
|
'client_id': token.client_id,
|
||||||
|
|
|
@ -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))
|
|
|
@ -18,7 +18,10 @@ actions = {
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@require_http_methods(['GET', 'HEAD', 'POST'])
|
@require_http_methods(['GET', 'HEAD', 'POST'])
|
||||||
def micropub(request):
|
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'):
|
if request.method in ('GET', 'HEAD'):
|
||||||
return query(request)
|
return query(request)
|
||||||
|
|
||||||
|
@ -26,6 +29,6 @@ def micropub(request):
|
||||||
if request.content_type == 'application/json':
|
if request.content_type == 'application/json':
|
||||||
request.json = json.load(request)
|
request.json = json.load(request)
|
||||||
action = request.json.get('action', 'create')
|
action = request.json.get('action', 'create')
|
||||||
if action not in actions:
|
if action in actions:
|
||||||
raise error.bad_req('unknown action: {}'.format(action))
|
|
||||||
return actions[action](request)
|
return actions[action](request)
|
||||||
|
return error.bad_req('unknown action: {}'.format(action))
|
||||||
|
|
|
@ -7,7 +7,7 @@ from entries.models import Cat, Entry
|
||||||
from entries.kinds import Article, Note, Reply, Like, Repost
|
from entries.kinds import Article, Note, Reply, Like, Repost
|
||||||
from lemoncurry import utils
|
from lemoncurry import utils
|
||||||
|
|
||||||
from .. import error
|
from . import error
|
||||||
|
|
||||||
|
|
||||||
def form_to_mf2(request):
|
def form_to_mf2(request):
|
||||||
|
@ -33,14 +33,14 @@ def create(request):
|
||||||
'application/x-www-form-urlencoded': form_to_mf2,
|
'application/x-www-form-urlencoded': form_to_mf2,
|
||||||
}
|
}
|
||||||
if 'create' not in request.token:
|
if 'create' not in request.token:
|
||||||
raise error.bad_scope('create')
|
return error.bad_scope('create')
|
||||||
if request.content_type not in normalise:
|
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)
|
body = normalise[request.content_type](request)
|
||||||
if 'type' not in body:
|
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']:
|
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)
|
entry = Entry(author=request.token.user)
|
||||||
props = body.get('properties', {})
|
props = body.get('properties', {})
|
||||||
|
|
|
@ -4,7 +4,7 @@ from ronkyuu import webmention
|
||||||
from entries.from_url import from_url
|
from entries.from_url import from_url
|
||||||
from entries.jobs import ping_hub, send_mentions
|
from entries.jobs import ping_hub, send_mentions
|
||||||
|
|
||||||
from .. import error
|
from . import error
|
||||||
|
|
||||||
def delete(request):
|
def delete(request):
|
||||||
normalise = {
|
normalise = {
|
||||||
|
@ -12,14 +12,14 @@ def delete(request):
|
||||||
'application/x-www-form-urlencoded': lambda r: r.POST.get('url'),
|
'application/x-www-form-urlencoded': lambda r: r.POST.get('url'),
|
||||||
}
|
}
|
||||||
if 'delete' not in request.token:
|
if 'delete' not in request.token:
|
||||||
raise error.bad_scope('delete')
|
return error.bad_scope('delete')
|
||||||
if request.content_type not in normalise:
|
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)
|
url = normalise[request.content_type](request)
|
||||||
entry = from_url(url)
|
entry = from_url(url)
|
||||||
|
|
||||||
if entry.author != request.token.user:
|
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
|
perma = entry.absolute_url
|
||||||
pings = entry.affected_urls
|
pings = entry.affected_urls
|
||||||
|
|
31
micropub/views/error.py
Normal file
31
micropub/views/error.py
Normal file
|
@ -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)
|
|
@ -8,7 +8,7 @@ import magic
|
||||||
|
|
||||||
from lemonauth import tokens
|
from lemonauth import tokens
|
||||||
from lemoncurry.utils import absolute_url
|
from lemoncurry.utils import absolute_url
|
||||||
from .. import error
|
from . import error
|
||||||
|
|
||||||
ACCEPTED_MEDIA_TYPES = (
|
ACCEPTED_MEDIA_TYPES = (
|
||||||
'image/gif',
|
'image/gif',
|
||||||
|
@ -21,13 +21,15 @@ ACCEPTED_MEDIA_TYPES = (
|
||||||
@require_POST
|
@require_POST
|
||||||
def media(request):
|
def media(request):
|
||||||
token = tokens.auth(request)
|
token = tokens.auth(request)
|
||||||
|
if hasattr(token, 'content'):
|
||||||
|
return token
|
||||||
if 'file' not in request.FILES:
|
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"
|
"a file named 'file' must be provided to the media endpoint"
|
||||||
)
|
)
|
||||||
file = request.FILES['file']
|
file = request.FILES['file']
|
||||||
if file.content_type not in ACCEPTED_MEDIA_TYPES:
|
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)
|
'unacceptable file type {0}'.format(file.content_type)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,7 +41,7 @@ def media(request):
|
||||||
sha.update(chunk)
|
sha.update(chunk)
|
||||||
|
|
||||||
if mime != file.content_type:
|
if mime != file.content_type:
|
||||||
raise error.bad_req(
|
return error.bad_req(
|
||||||
'detected file type {0} did not match specified file type {1}'
|
'detected file type {0} did not match specified file type {1}'
|
||||||
.format(mime, file.content_type)
|
.format(mime, file.content_type)
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django.http import JsonResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from lemoncurry import requests
|
from lemoncurry import requests
|
||||||
from lemoncurry.utils import absolute_url
|
from lemoncurry.utils import absolute_url
|
||||||
from .. import error
|
from . import error
|
||||||
|
|
||||||
|
|
||||||
def config(request):
|
def config(request):
|
||||||
|
@ -14,10 +14,10 @@ def config(request):
|
||||||
def source(request):
|
def source(request):
|
||||||
get = request.GET
|
get = request.GET
|
||||||
if 'url' not in 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')
|
mf2 = requests.mf2(get['url']).to_dict(filter_by_type='h-entry')
|
||||||
if not mf2:
|
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]
|
entry = mf2[0]
|
||||||
keys = get.getlist('properties', []) + get.getlist('properties[]', [])
|
keys = get.getlist('properties', []) + get.getlist('properties[]', [])
|
||||||
if not keys:
|
if not keys:
|
||||||
|
@ -40,9 +40,11 @@ queries = {
|
||||||
|
|
||||||
def query(request):
|
def query(request):
|
||||||
if 'q' not in request.GET:
|
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']
|
q = request.GET['q']
|
||||||
if q not in queries:
|
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)
|
res = queries[q](request)
|
||||||
|
if hasattr(res, 'content'):
|
||||||
|
return res
|
||||||
return JsonResponse(res)
|
return JsonResponse(res)
|
||||||
|
|
Loading…
Reference in a new issue