changeset 969:5b735229b6fb

admin: Micropub improvements. - Add support for Micropub media endpoint. - Add support for uploading photos via the endpoint. - Fix URL returned after creating post. - Hack for Micro.blog access token problems. - Hack for bug in Flask-IndieAuth.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 08 Oct 2017 09:32:33 -0700
parents 20f49786937c
children 660250c95246
files piecrust/admin/views/micropub.py
diffstat 1 files changed, 144 insertions(+), 19 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/admin/views/micropub.py	Sun Oct 08 09:31:15 2017 -0700
+++ b/piecrust/admin/views/micropub.py	Sun Oct 08 09:32:33 2017 -0700
@@ -1,13 +1,16 @@
 import re
 import os
 import os.path
+import json
+import uuid
 import logging
 import datetime
 import yaml
 from werkzeug.utils import secure_filename
-from flask import g, request, abort, Response
+from flask import g, url_for, request, abort, jsonify, Response
 from flask_indieauth import requires_indieauth
 from ..blueprint import foodtruck_bp
+from piecrust import CACHE_DIR
 from piecrust.configuration import merge_dicts
 from piecrust.page import Page
 
@@ -17,21 +20,112 @@
 re_unsafe_asset_char = re.compile('[^a-zA-Z0-9_]')
 
 
+def _patch_flask_indieauth():
+    import flask_indieauth
+
+    def _patched_get_access_token_from_json_request(request):
+        try:
+            jsondata = json.loads(request.get_data(as_text=True))
+            return jsondata['access_token']
+        except ValueError:
+            return None
+
+    _orig_check_auth = flask_indieauth.check_auth
+
+    def _patched_check_auth(access_token):
+        user_agent = request.headers.get('User-Agent') or ''
+        if user_agent.startswith('Micro.blog/'):
+            return None
+        return _orig_check_auth(access_token)
+
+    flask_indieauth.get_access_token_from_json_request = \
+        _patched_get_access_token_from_json_request
+    flask_indieauth.check_auth = _patched_check_auth
+    logger.info("Patched Flask-IndieAuth.")
+
+
+_patch_flask_indieauth()
+
+
+_enable_debug_auth = False
+
+
+def _debug_auth():
+    if _enable_debug_auth:
+        logger.warning("Headers: %s" % request.headers)
+        logger.warning("Args: %s" % request.args)
+        logger.warning("Form: %s" % request.form)
+        logger.warning("Data: %s" % request.get_data(True))
+
+
 @foodtruck_bp.route('/micropub', methods=['POST'])
 @requires_indieauth
-def micropub():
+def post_micropub():
+    _debug_auth()
+
     post_type = request.form.get('h')
 
     if post_type == 'entry':
-        uri = _create_hentry()
+        source_name, content_item = _create_hentry()
         _run_publisher()
-        return _get_location_response(uri)
+        return _get_location_response(source_name, content_item)
 
     logger.debug("Unknown or unsupported update type.")
     logger.debug(request.form)
     abort(400)
 
 
+@foodtruck_bp.route('/micropub/media', methods=['POST'])
+@requires_indieauth
+def post_micropub_media():
+    _debug_auth()
+    photo = request.files.get('file')
+    if not photo:
+        logger.error("Micropub media request without a file part.")
+        abort(400)
+        return
+
+    fn = secure_filename(photo.filename)
+    fn = re_unsafe_asset_char.sub('_', fn)
+    fn = '%s_%s' % (str(uuid.uuid1()), fn)
+
+    photo_cache_dir = os.path.join(
+        g.site.root_dir,
+        CACHE_DIR, g.site.piecrust_factory.cache_key,
+        'uploads')
+    try:
+        os.makedirs(photo_cache_dir, mode=0o775, exist_ok=True)
+    except OSError:
+        pass
+
+    photo_path = os.path.join(photo_cache_dir, fn)
+    logger.info("Uploading file to: %s" % photo_path)
+    photo.save(photo_path)
+
+    r = Response()
+    r.status_code = 201
+    r.headers.add('Location', fn)
+    return r
+
+
+@foodtruck_bp.route('/micropub', methods=['GET'])
+def get_micropub():
+    data = {}
+    if request.args.get('q') == 'config':
+        endpoint_url = (request.host_url.rstrip('/') +
+                        url_for('.post_micropub_media'))
+        data.update({
+           "media-endpoint": endpoint_url
+        })
+
+        pcapp = g.site.piecrust_app
+        syn_data = pcapp.config.get('micropub/syndicate_to')
+        if syn_data:
+            data['syndicate-to'] = syn_data
+
+    return jsonify(**data)
+
+
 def _run_publisher():
     pcapp = g.site.piecrust_app
     target = pcapp.config.get('micropub/publish_target')
@@ -40,7 +134,14 @@
         g.site.publish(target)
 
 
-def _get_location_response(uri):
+def _get_location_response(source_name, content_item):
+    from piecrust.app import PieCrust
+    pcapp = PieCrust(g.site.root_dir)
+    source = pcapp.getSource(source_name)
+
+    page = Page(source, content_item)
+    uri = page.getUri()
+
     logger.debug("Redirecting to: %s" % uri)
     r = Response()
     r.status_code = 201
@@ -50,7 +151,6 @@
 
 def _create_hentry():
     f = request.form
-    pcapp = g.site.piecrust_app
 
     summary = f.get('summary')
     categories = f.getlist('category[]')
@@ -120,14 +220,38 @@
         abort(500)
 
     # TODO: add proper APIs for creating related assets.
-    photo_names = None
-    if photos:
+    photo_names = []
+    if photo_urls or photos:
         photo_dir, _ = os.path.splitext(content_item.spec)
         photo_dir += '-assets'
-        if not os.path.exists(photo_dir):
-            os.makedirs(photo_dir)
+        try:
+            os.makedirs(photo_dir, mode=0o775, exist_ok=True)
+        except OSError:
+            pass
+
+    # Photo URLs come from files uploaded via the media endpoint...
+    # They're waiting for us in the upload cache folder, so let's
+    # move them to the post's assets folder.
+    if photo_urls:
+        photo_cache_dir = os.path.join(
+            g.site.root_dir,
+            CACHE_DIR, g.site.piecrust_factory.cache_key,
+            'uploads')
 
-        photo_names = []
+        for p_url in photo_urls:
+            _, __, p_url = p_url.rpartition('/')
+            p_path = os.path.join(photo_cache_dir, p_url)
+            p_uuid, p_fn = p_url.split('_', 1)
+            p_asset = os.path.join(photo_dir, p_fn)
+            logger.info("Moving upload '%s' to '%s'." % (p_path, p_asset))
+            os.rename(p_path, p_asset)
+
+            p_fn_no_ext, _ = os.path.splitext(p_fn)
+            photo_names.append(p_fn_no_ext)
+
+    # There could also be some files uploaded along with the post
+    # so upload them right now.
+    if photos:
         for photo in photos:
             if not photo or not photo.filename:
                 logger.warning("Got empty photo in request files... skipping.")
@@ -178,18 +302,19 @@
             fp.write('<!--break-->\n\n')
         fp.write(content)
 
-        if photo_urls:
-            fp.write('\n\n')
-            for pu in photo_urls:
-                fp.write('<img src="{{assets.%s}}" alt=""/>\n\n' % pu)
-
         if photo_names:
             fp.write('\n\n')
             for pn in photo_names:
                 fp.write('<img src="{{assets.%s}}" alt="%s"/>\n\n' %
                          (pn, pn))
 
-    page = Page(source, content_item)
-    uri = page.getUri()
-    return uri
+        if os.supports_fd:
+            import stat
+            try:
+                os.chmod(fp.fileno(),
+                         stat.S_IRUSR|stat.S_IWUSR|stat.S_IRGRP|stat.S_IWGRP)
+            except OSError:
+                pass
 
+    return source_name, content_item
+