Mercurial > piecrust2
comparison piecrust/admin/views/micropub.py @ 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 | d65838abbd90 |
children | 660250c95246 |
comparison
equal
deleted
inserted
replaced
968:20f49786937c | 969:5b735229b6fb |
---|---|
1 import re | 1 import re |
2 import os | 2 import os |
3 import os.path | 3 import os.path |
4 import json | |
5 import uuid | |
4 import logging | 6 import logging |
5 import datetime | 7 import datetime |
6 import yaml | 8 import yaml |
7 from werkzeug.utils import secure_filename | 9 from werkzeug.utils import secure_filename |
8 from flask import g, request, abort, Response | 10 from flask import g, url_for, request, abort, jsonify, Response |
9 from flask_indieauth import requires_indieauth | 11 from flask_indieauth import requires_indieauth |
10 from ..blueprint import foodtruck_bp | 12 from ..blueprint import foodtruck_bp |
13 from piecrust import CACHE_DIR | |
11 from piecrust.configuration import merge_dicts | 14 from piecrust.configuration import merge_dicts |
12 from piecrust.page import Page | 15 from piecrust.page import Page |
13 | 16 |
14 | 17 |
15 logger = logging.getLogger(__name__) | 18 logger = logging.getLogger(__name__) |
16 | 19 |
17 re_unsafe_asset_char = re.compile('[^a-zA-Z0-9_]') | 20 re_unsafe_asset_char = re.compile('[^a-zA-Z0-9_]') |
18 | 21 |
19 | 22 |
23 def _patch_flask_indieauth(): | |
24 import flask_indieauth | |
25 | |
26 def _patched_get_access_token_from_json_request(request): | |
27 try: | |
28 jsondata = json.loads(request.get_data(as_text=True)) | |
29 return jsondata['access_token'] | |
30 except ValueError: | |
31 return None | |
32 | |
33 _orig_check_auth = flask_indieauth.check_auth | |
34 | |
35 def _patched_check_auth(access_token): | |
36 user_agent = request.headers.get('User-Agent') or '' | |
37 if user_agent.startswith('Micro.blog/'): | |
38 return None | |
39 return _orig_check_auth(access_token) | |
40 | |
41 flask_indieauth.get_access_token_from_json_request = \ | |
42 _patched_get_access_token_from_json_request | |
43 flask_indieauth.check_auth = _patched_check_auth | |
44 logger.info("Patched Flask-IndieAuth.") | |
45 | |
46 | |
47 _patch_flask_indieauth() | |
48 | |
49 | |
50 _enable_debug_auth = False | |
51 | |
52 | |
53 def _debug_auth(): | |
54 if _enable_debug_auth: | |
55 logger.warning("Headers: %s" % request.headers) | |
56 logger.warning("Args: %s" % request.args) | |
57 logger.warning("Form: %s" % request.form) | |
58 logger.warning("Data: %s" % request.get_data(True)) | |
59 | |
60 | |
20 @foodtruck_bp.route('/micropub', methods=['POST']) | 61 @foodtruck_bp.route('/micropub', methods=['POST']) |
21 @requires_indieauth | 62 @requires_indieauth |
22 def micropub(): | 63 def post_micropub(): |
64 _debug_auth() | |
65 | |
23 post_type = request.form.get('h') | 66 post_type = request.form.get('h') |
24 | 67 |
25 if post_type == 'entry': | 68 if post_type == 'entry': |
26 uri = _create_hentry() | 69 source_name, content_item = _create_hentry() |
27 _run_publisher() | 70 _run_publisher() |
28 return _get_location_response(uri) | 71 return _get_location_response(source_name, content_item) |
29 | 72 |
30 logger.debug("Unknown or unsupported update type.") | 73 logger.debug("Unknown or unsupported update type.") |
31 logger.debug(request.form) | 74 logger.debug(request.form) |
32 abort(400) | 75 abort(400) |
76 | |
77 | |
78 @foodtruck_bp.route('/micropub/media', methods=['POST']) | |
79 @requires_indieauth | |
80 def post_micropub_media(): | |
81 _debug_auth() | |
82 photo = request.files.get('file') | |
83 if not photo: | |
84 logger.error("Micropub media request without a file part.") | |
85 abort(400) | |
86 return | |
87 | |
88 fn = secure_filename(photo.filename) | |
89 fn = re_unsafe_asset_char.sub('_', fn) | |
90 fn = '%s_%s' % (str(uuid.uuid1()), fn) | |
91 | |
92 photo_cache_dir = os.path.join( | |
93 g.site.root_dir, | |
94 CACHE_DIR, g.site.piecrust_factory.cache_key, | |
95 'uploads') | |
96 try: | |
97 os.makedirs(photo_cache_dir, mode=0o775, exist_ok=True) | |
98 except OSError: | |
99 pass | |
100 | |
101 photo_path = os.path.join(photo_cache_dir, fn) | |
102 logger.info("Uploading file to: %s" % photo_path) | |
103 photo.save(photo_path) | |
104 | |
105 r = Response() | |
106 r.status_code = 201 | |
107 r.headers.add('Location', fn) | |
108 return r | |
109 | |
110 | |
111 @foodtruck_bp.route('/micropub', methods=['GET']) | |
112 def get_micropub(): | |
113 data = {} | |
114 if request.args.get('q') == 'config': | |
115 endpoint_url = (request.host_url.rstrip('/') + | |
116 url_for('.post_micropub_media')) | |
117 data.update({ | |
118 "media-endpoint": endpoint_url | |
119 }) | |
120 | |
121 pcapp = g.site.piecrust_app | |
122 syn_data = pcapp.config.get('micropub/syndicate_to') | |
123 if syn_data: | |
124 data['syndicate-to'] = syn_data | |
125 | |
126 return jsonify(**data) | |
33 | 127 |
34 | 128 |
35 def _run_publisher(): | 129 def _run_publisher(): |
36 pcapp = g.site.piecrust_app | 130 pcapp = g.site.piecrust_app |
37 target = pcapp.config.get('micropub/publish_target') | 131 target = pcapp.config.get('micropub/publish_target') |
38 if target: | 132 if target: |
39 logger.debug("Running pushing target '%s'." % target) | 133 logger.debug("Running pushing target '%s'." % target) |
40 g.site.publish(target) | 134 g.site.publish(target) |
41 | 135 |
42 | 136 |
43 def _get_location_response(uri): | 137 def _get_location_response(source_name, content_item): |
138 from piecrust.app import PieCrust | |
139 pcapp = PieCrust(g.site.root_dir) | |
140 source = pcapp.getSource(source_name) | |
141 | |
142 page = Page(source, content_item) | |
143 uri = page.getUri() | |
144 | |
44 logger.debug("Redirecting to: %s" % uri) | 145 logger.debug("Redirecting to: %s" % uri) |
45 r = Response() | 146 r = Response() |
46 r.status_code = 201 | 147 r.status_code = 201 |
47 r.headers.add('Location', uri) | 148 r.headers.add('Location', uri) |
48 return r | 149 return r |
49 | 150 |
50 | 151 |
51 def _create_hentry(): | 152 def _create_hentry(): |
52 f = request.form | 153 f = request.form |
53 pcapp = g.site.piecrust_app | |
54 | 154 |
55 summary = f.get('summary') | 155 summary = f.get('summary') |
56 categories = f.getlist('category[]') | 156 categories = f.getlist('category[]') |
57 location = f.get('location') | 157 location = f.get('location') |
58 reply_to = f.get('in-reply-to') | 158 reply_to = f.get('in-reply-to') |
118 if content_item is None: | 218 if content_item is None: |
119 logger.error("Can't create item for: %s" % metadata) | 219 logger.error("Can't create item for: %s" % metadata) |
120 abort(500) | 220 abort(500) |
121 | 221 |
122 # TODO: add proper APIs for creating related assets. | 222 # TODO: add proper APIs for creating related assets. |
123 photo_names = None | 223 photo_names = [] |
124 if photos: | 224 if photo_urls or photos: |
125 photo_dir, _ = os.path.splitext(content_item.spec) | 225 photo_dir, _ = os.path.splitext(content_item.spec) |
126 photo_dir += '-assets' | 226 photo_dir += '-assets' |
127 if not os.path.exists(photo_dir): | 227 try: |
128 os.makedirs(photo_dir) | 228 os.makedirs(photo_dir, mode=0o775, exist_ok=True) |
129 | 229 except OSError: |
130 photo_names = [] | 230 pass |
231 | |
232 # Photo URLs come from files uploaded via the media endpoint... | |
233 # They're waiting for us in the upload cache folder, so let's | |
234 # move them to the post's assets folder. | |
235 if photo_urls: | |
236 photo_cache_dir = os.path.join( | |
237 g.site.root_dir, | |
238 CACHE_DIR, g.site.piecrust_factory.cache_key, | |
239 'uploads') | |
240 | |
241 for p_url in photo_urls: | |
242 _, __, p_url = p_url.rpartition('/') | |
243 p_path = os.path.join(photo_cache_dir, p_url) | |
244 p_uuid, p_fn = p_url.split('_', 1) | |
245 p_asset = os.path.join(photo_dir, p_fn) | |
246 logger.info("Moving upload '%s' to '%s'." % (p_path, p_asset)) | |
247 os.rename(p_path, p_asset) | |
248 | |
249 p_fn_no_ext, _ = os.path.splitext(p_fn) | |
250 photo_names.append(p_fn_no_ext) | |
251 | |
252 # There could also be some files uploaded along with the post | |
253 # so upload them right now. | |
254 if photos: | |
131 for photo in photos: | 255 for photo in photos: |
132 if not photo or not photo.filename: | 256 if not photo or not photo.filename: |
133 logger.warning("Got empty photo in request files... skipping.") | 257 logger.warning("Got empty photo in request files... skipping.") |
134 continue | 258 continue |
135 | 259 |
176 fp.write(summary) | 300 fp.write(summary) |
177 fp.write('\n') | 301 fp.write('\n') |
178 fp.write('<!--break-->\n\n') | 302 fp.write('<!--break-->\n\n') |
179 fp.write(content) | 303 fp.write(content) |
180 | 304 |
181 if photo_urls: | |
182 fp.write('\n\n') | |
183 for pu in photo_urls: | |
184 fp.write('<img src="{{assets.%s}}" alt=""/>\n\n' % pu) | |
185 | |
186 if photo_names: | 305 if photo_names: |
187 fp.write('\n\n') | 306 fp.write('\n\n') |
188 for pn in photo_names: | 307 for pn in photo_names: |
189 fp.write('<img src="{{assets.%s}}" alt="%s"/>\n\n' % | 308 fp.write('<img src="{{assets.%s}}" alt="%s"/>\n\n' % |
190 (pn, pn)) | 309 (pn, pn)) |
191 | 310 |
192 page = Page(source, content_item) | 311 if os.supports_fd: |
193 uri = page.getUri() | 312 import stat |
194 return uri | 313 try: |
195 | 314 os.chmod(fp.fileno(), |
315 stat.S_IRUSR|stat.S_IWUSR|stat.S_IRGRP|stat.S_IWGRP) | |
316 except OSError: | |
317 pass | |
318 | |
319 return source_name, content_item | |
320 |