From fa8419976d3d5758c62c4e9e049533244d7cbc47 Mon Sep 17 00:00:00 2001 From: Danielle McLean Date: Mon, 2 Jul 2018 15:08:13 +1000 Subject: [PATCH] Enable support for deleting entries through Micropub :D --- entries/models.py | 18 ++++++++++++++ micropub/views/__init__.py | 18 ++++++++++++-- micropub/views/create.py | 13 ++--------- micropub/views/delete.py | 48 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 micropub/views/delete.py diff --git a/entries/models.py b/entries/models.py index 05224b3..e7419c9 100644 --- a/entries/models.py +++ b/entries/models.py @@ -146,6 +146,24 @@ class Entry(ModelMeta, TimeStampedModel): base = 'https://' + DjangoSite.objects.get_current().domain return urljoin(base, self.url) + @property + def affected_urls(self): + base = 'https://' + DjangoSite.objects.get_current().domain + kind = kinds.from_id[self.kind] + urls = { + self.url, + reverse('entries:index', kwargs={'kind': kind}), + reverse('entries:atom_by_kind', kwargs={'kind': kind}), + reverse('entries:rss_by_kind', kwargs={'kind': kind}), + } | {cat.url for cat in self.cats.all()} + if kind.on_home: + urls |= { + reverse('home:index'), + reverse('entries:atom'), + reverse('entries:rss') + } + return {urljoin(base, u) for u in urls} + @property def url(self): kind = kinds.from_id[self.kind] diff --git a/micropub/views/__init__.py b/micropub/views/__init__.py index 381a98f..f487986 100644 --- a/micropub/views/__init__.py +++ b/micropub/views/__init__.py @@ -1,11 +1,19 @@ +import json + from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods from lemonauth import tokens from .create import create +from .delete import delete from .query import query +actions = { + 'create': create, + 'delete': delete, +} + @csrf_exempt @require_http_methods(['GET', 'HEAD', 'POST']) @@ -14,7 +22,13 @@ def micropub(request): if hasattr(token, 'content'): return token request.token = token - if request.method == 'POST': - return create(request) if request.method in ('GET', 'HEAD'): return query(request) + + action = request.POST.get('action', 'create') + 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)) diff --git a/micropub/views/create.py b/micropub/views/create.py index 7257172..6c4cf21 100644 --- a/micropub/views/create.py +++ b/micropub/views/create.py @@ -1,4 +1,3 @@ -import json from django.urls import reverse from django.http import HttpResponse from urllib.parse import urljoin @@ -30,7 +29,7 @@ def form_to_mf2(request): def create(request): normalise = { - 'application/json': json.load, + 'application/json': lambda r: r.json, 'application/x-www-form-urlencoded': form_to_mf2, } if 'create' not in request.token: @@ -77,15 +76,7 @@ def create(request): base = utils.origin(request) perma = urljoin(base, entry.url) short = urljoin(base, entry.short_url) - others = [urljoin(base, url) for url in ( - reverse('home:index'), - reverse('entries:atom'), - reverse('entries:rss'), - reverse('entries:index', kwargs={'kind': kind}), - reverse('entries:atom_by_kind', kwargs={'kind': kind}), - reverse('entries:rss_by_kind', kwargs={'kind': kind}), - )] + [urljoin(base, cat.url) for cat in cats] - ping_hub.delay(perma, *others) + ping_hub.delay(*entry.affected_urls) send_mentions.delay(perma) res = HttpResponse(status=201) diff --git a/micropub/views/delete.py b/micropub/views/delete.py new file mode 100644 index 0000000..4ee8a89 --- /dev/null +++ b/micropub/views/delete.py @@ -0,0 +1,48 @@ +from django.http import HttpResponse +from django.urls import resolve, Resolver404 +from urllib.parse import urlparse + +from entries.jobs import ping_hub +from entries.models import Entry + +from . import error + +def delete(request): + normalise = { + 'application/json': lambda r: r.json.get('url'), + 'application/x-www-form-urlencoded': lambda r: r.POST.get('url'), + } + if 'delete' not in request.token: + return error.bad_scope('delete') + if request.content_type not in normalise: + return error.unsupported_type(request.content_type) + url = normalise[request.content_type](request) + if not url: + return error.bad_req('url parameter required') + + if '//' not in url: + url = '//' + url + url = urlparse(url, scheme='https') + + if url.scheme not in ('http', 'https') or url.netloc != request.site.domain: + return error.bad_req('url does not point to this site') + try: + match = resolve(url.path) + except Resolver404: + return error.bad_req('url does not point to a valid page on this site') + + if match.view_name != 'entries:entry': + return 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: + return error.bad_req('url does not point to an existing entry') + + if entry.author != request.token.user: + return error.forbid('entry belongs to another user') + + urls = entry.affected_urls + entry.delete() + ping_hub.delay(urls) + return HttpResponse(status=204)