Mercurial > piecrust2
changeset 970:660250c95246
admin: Improve support for Micropub.
- Handle Micropub JSON syntax.
- Use UTF8 encoding for created posts.
- Remove Micro.blog hack for authorization.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Mon, 09 Oct 2017 21:07:00 -0700 |
parents | 5b735229b6fb |
children | 5485a11591ec |
files | piecrust/admin/settings.py piecrust/admin/views/micropub.py |
diffstat | 2 files changed, 100 insertions(+), 63 deletions(-) [+] |
line wrap: on
line diff
--- a/piecrust/admin/settings.py Sun Oct 08 09:32:33 2017 -0700 +++ b/piecrust/admin/settings.py Mon Oct 09 21:07:00 2017 -0700 @@ -1,1 +1,2 @@ FOODTRUCK_ROOT_URL = '' +TOKEN_ENDPOINT = "https://tokens.indieauth.com/token"
--- a/piecrust/admin/views/micropub.py Sun Oct 08 09:32:33 2017 -0700 +++ b/piecrust/admin/views/micropub.py Mon Oct 09 21:07:00 2017 -0700 @@ -30,55 +30,62 @@ 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 +_enable_debug_req = False -def _debug_auth(): - if _enable_debug_auth: +def _debug_req(): + if _enable_debug_req: 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)) + try: + logger.warning("JSON: %s" % request.json) + except: + pass @foodtruck_bp.route('/micropub', methods=['POST']) @requires_indieauth def post_micropub(): - _debug_auth() + _debug_req() - post_type = request.form.get('h') + if 'h' in request.form: + data = _get_mf2_from_form(request.form) + else: + try: + data = json.loads(request.get_data(as_text=True)) + except: + data = None - if post_type == 'entry': - source_name, content_item = _create_hentry() - _run_publisher() - return _get_location_response(source_name, content_item) + if data: + entry_type = _mf2get(data, 'type') + if entry_type == 'h-entry': + source_name, content_item = _create_hentry(data['properties']) + _run_publisher() + return _get_location_response(source_name, content_item) - logger.debug("Unknown or unsupported update type.") - logger.debug(request.form) + else: + logger.error("Post type '%s' is not supported." % post_type) + else: + logger.error("Missing form or JSON data.") + abort(400) @foodtruck_bp.route('/micropub/media', methods=['POST']) @requires_indieauth def post_micropub_media(): - _debug_auth() + _debug_req() + photo = request.files.get('file') if not photo: logger.error("Micropub media request without a file part.") @@ -149,62 +156,75 @@ return r -def _create_hentry(): - f = request.form +re_array_prop = re.compile(r'\[(?P<name>\w*)\]$') + + +def _get_mf2_from_form(f): + post_type = 'h-' + f.get('h', '') - summary = f.get('summary') - categories = f.getlist('category[]') - location = f.get('location') - reply_to = f.get('in-reply-to') - status = f.get('post-status') - # pubdate = f.get('published', 'now') + properties = {} + for key, vals in f.lists(): + m = re_array_prop.search(key) + if not m: + properties[key] = vals + continue + + key_name_only = key[:m.start()] + inner_name = m.group('name') + if not inner_name: + properties[key_name_only] = vals + continue + + properties[key_name_only] = [{inner_name: vals[0]}] + + return { + 'type': [post_type], + 'properties': properties} - # Figure out the title of the post. - name = f.get('name') - if not name: - name = f.get('name[]') + +def _mf2get(data, key): + val = data.get(key) + if val is not None: + return val[0] + return None + - # Figure out the contents of the post. +def _create_hentry(data): + name = _mf2get(data, 'name') + summary = _mf2get(data, 'summary') + location = _mf2get(data, 'location') + reply_to = _mf2get(data, 'in-reply-to') + status = _mf2get(data, 'post-status') + # pubdate = _mf2get(data, 'published') or 'now' + + categories = data.get('category') + + # Get the content. post_format = None - content = f.get('content') - if not content: - content = f.get('content[]') - if not content: - content = f.get('content[html]') + content = _mf2get(data, 'content') + if isinstance(content, dict): + content = content.get('html') post_format = 'none' - if not content: logger.error("No content specified!") - logger.error(dict(request.form)) + logger.error(data) abort(400) + # Clean-up stuff. # TODO: setting to conserve Windows-type line endings? content = content.replace('\r\n', '\n') if summary: summary = summary.replace('\r\n', '\n') - # Figure out the slug of the post. + # Get the slug. + slug = _mf2get(data, 'slug') or _mf2get(data, 'mp-slug') now = datetime.datetime.now() - slug = f.get('slug') - if not slug: - slug = f.get('mp-slug') if not slug: slug = '%02d%02d%02d' % (now.hour, now.minute, now.second) - # Get the media to attach to the post. - photo_urls = None - if 'photo' in f: - photo_urls = [f['photo']] - elif 'photo[]' in f: - photo_urls = f.getlist('photo[]') - - photos = None - if 'photo' in request.files: - photos = [request.files['photo']] - elif 'photo[]' in request.files: - photos = request.files.getlist('photo[]') - # Create the post in the correct content source. + # Note that this won't actually write anything to disk yet, we're + # just creating it in memory. pcapp = g.site.piecrust_app source_name = pcapp.config.get('micropub/source', 'posts') source = pcapp.getSource(source_name) @@ -219,19 +239,31 @@ logger.error("Can't create item for: %s" % metadata) abort(500) + # Get the media to attach to the post. + photos = None + if 'photo' in request.files: + photos = [request.files['photo']] + elif 'photo[]' in request.files: + photos = request.files.getlist('photo[]') + photo_urls = data.get('photo') + + # Create the assets folder if we have anything to put there. # TODO: add proper APIs for creating related assets. - photo_names = [] if photo_urls or photos: photo_dir, _ = os.path.splitext(content_item.spec) photo_dir += '-assets' try: os.makedirs(photo_dir, mode=0o775, exist_ok=True) except OSError: + # An `OSError` can still be raised in older versions of Python + # if the permissions don't match an existing folder. + # Let's ignore it. 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. + photo_names = [] if photo_urls: photo_cache_dir = os.path.join( g.site.root_dir, @@ -244,7 +276,11 @@ 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) + try: + os.rename(p_path, p_asset) + except OSError: + logger.error("Can't move '%s' to '%s'." % (p_path, p_asset)) + raise p_fn_no_ext, _ = os.path.splitext(p_fn) photo_names.append(p_fn_no_ext) @@ -276,8 +312,8 @@ post_config['location'] = location if reply_to: post_config['reply_to'] = reply_to - if status: - post_config['status'] = status + if status and status != 'published': + post_config['draft'] = True if post_format: post_config['format'] = post_format post_config['time'] = '%02d:%02d:%02d' % (now.hour, now.minute, now.second) @@ -289,7 +325,7 @@ merge_dicts(post_config, micro_config) logger.debug("Writing to item: %s" % content_item.spec) - with source.openItem(content_item, mode='w') as fp: + with source.openItem(content_item, mode='w', encoding='utf8') as fp: fp.write('---\n') yaml.dump(post_config, fp, default_flow_style=False,