Merge branch 'master' of git.incorporeal.org:bss/incorporeal-cms

This commit is contained in:
2021-04-17 14:57:20 -05:00
21 changed files with 271 additions and 81 deletions

View File

@@ -3,7 +3,7 @@ import logging
import os
from logging.config import dictConfig
from flask import Flask, request, send_from_directory
from flask import Flask, request
from ._version import get_versions
@@ -40,11 +40,6 @@ def create_app(instance_path=None, test_config=None):
logger.info("RESPONSE: %s %s: %s", request.method, request.path, response.status)
return response
@app.route(f'/{app.config["MEDIA_DIR"]}/<path:filename>')
def media_files(filename):
return send_from_directory(os.path.join(app.instance_path, app.config['MEDIA_DIR']),
filename)
from . import error_pages, pages
app.register_blueprint(pages.bp)
app.register_error_handler(400, error_pages.bad_request)

View File

@@ -54,3 +54,5 @@ class Config(object):
DEFAULT_PAGE_STYLE = 'light'
TITLE_SUFFIX = 'incorporeal.org'
CONTACT_EMAIL = 'bss@incorporeal.org'
# specify FAVICON in your instance config.py to override the suou icon

View File

@@ -34,6 +34,7 @@ def render(template_name_or_list, **context):
PAGE_STYLES = {
'dark': 'css/dark.css',
'light': 'css/light.css',
'plain': 'css/plain.css',
}
selected_style = request.args.get('style', None)

View File

@@ -6,7 +6,7 @@ import re
from flask import Blueprint, Markup, abort
from flask import current_app as app
from flask import redirect, request
from flask import redirect, request, send_from_directory
from tzlocal import get_localzone
from incorporealcms.lib import get_meta_str, init_md, render
@@ -21,8 +21,9 @@ bp = Blueprint('pages', __name__, url_prefix='/')
def display_page(path):
"""Get the file contents of the requested path and render the file."""
try:
resolved_path = request_path_to_instance_resource_path(path)
logger.debug("received request for path '%s', resolved to '%s'", path, resolved_path)
resolved_path, render_type = request_path_to_instance_resource_path(path)
logger.debug("received request for path '%s', resolved to '%s', type '%s'",
path, resolved_path, render_type)
except PermissionError:
abort(400)
except IsADirectoryError:
@@ -30,28 +31,39 @@ def display_page(path):
except FileNotFoundError:
abort(404)
try:
with app.open_instance_resource(resolved_path, 'r') as entry_file:
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(entry_file.name), get_localzone())
entry = entry_file.read()
except OSError:
logger.exception("resolved path '%s' could not be opened!", resolved_path)
abort(500)
if render_type == 'file':
return send_from_directory(app.instance_path, resolved_path)
elif render_type == 'symlink':
logger.debug("attempting to redirect path '%s' to reverse of resource '%s'", path, resolved_path)
redirect_path = f'/{instance_resource_path_to_request_path(resolved_path)}'
logger.debug("redirect path: '%s'", redirect_path)
return redirect(redirect_path, code=301)
elif render_type == 'markdown':
try:
with app.open_instance_resource(resolved_path, 'r') as entry_file:
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(entry_file.name), get_localzone())
entry = entry_file.read()
except OSError:
logger.exception("resolved path '%s' could not be opened!", resolved_path)
abort(500)
else:
md = init_md()
content = Markup(md.convert(entry))
logger.debug("file metadata: %s", md.Meta)
parent_navs = generate_parent_navs(resolved_path)
page_name = (get_meta_str(md, 'title') if md.Meta.get('title') else
f'/{instance_resource_path_to_request_path(resolved_path)}')
page_title = f'{page_name} - {app.config["TITLE_SUFFIX"]}' if page_name else app.config['TITLE_SUFFIX']
logger.debug("title (potentially derived): %s", page_title)
return render('base.html', title=page_title, description=get_meta_str(md, 'description'),
image=get_meta_str(md, 'image'), base_url=request.base_url, content=content,
navs=parent_navs, mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z'))
else:
md = init_md()
content = Markup(md.convert(entry))
logger.debug("file metadata: %s", md.Meta)
parent_navs = generate_parent_navs(resolved_path)
page_name = (get_meta_str(md, 'title') if md.Meta.get('title') else
f'/{instance_resource_path_to_request_path(resolved_path)}')
page_title = f'{page_name} - {app.config["TITLE_SUFFIX"]}' if page_name else app.config['TITLE_SUFFIX']
logger.debug("title (potentially derived): %s", page_title)
return render('base.html', title=page_title, description=get_meta_str(md, 'description'),
image=get_meta_str(md, 'image'), base_url=request.base_url, content=content, navs=parent_navs,
mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z'))
logger.exception("unsupported render_type '%s'!?", render_type)
abort(500)
def request_path_to_instance_resource_path(path):
@@ -63,34 +75,52 @@ def request_path_to_instance_resource_path(path):
"""
# check if the path is allowed
base_dir = os.path.realpath(f'{app.instance_path}/pages/')
resolved_path = os.path.realpath(os.path.join(base_dir, path))
logger.debug("base_dir: %s, constructed resolved_path: %s", base_dir, resolved_path)
verbatim_path = os.path.abspath(os.path.join(base_dir, path))
resolved_path = os.path.realpath(verbatim_path)
logger.debug("base_dir '%s', constructed resolved_path '%s' for path '%s'", base_dir, resolved_path, path)
# bail if the requested real path isn't inside the base directory
if base_dir != os.path.commonpath((base_dir, resolved_path)):
logger.warning("client tried to request a path '%s' outside of the base_dir!", path)
raise PermissionError
# if this is a file-like requset but actually a directory, redirect the user
if os.path.isdir(resolved_path) and not path.endswith('/'):
logger.info("client requested a path '%s' that is actually a directory", path)
raise IsADirectoryError
# see if we have a real file or if we should infer markdown rendering
if os.path.exists(resolved_path):
# if this is a file-like request but actually a directory, redirect the user
if os.path.isdir(resolved_path) and not path.endswith('/'):
logger.info("client requested a path '%s' that is actually a directory", path)
raise IsADirectoryError
# derive the proper markdown file depending on if this is a dir or file
if os.path.isdir(resolved_path):
absolute_resource = os.path.join(resolved_path, 'index.md')
else:
absolute_resource = f'{resolved_path}.md'
# if the requested path contains a symlink, redirect the user
if verbatim_path != resolved_path:
logger.info("client requested a path '%s' that is actually a symlink to file '%s'", path, resolved_path)
return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'symlink'
logger.info("final path = '%s' for request '%s'", absolute_resource, path)
# derive the proper markdown or actual file depending on if this is a dir or file
if os.path.isdir(resolved_path):
resolved_path = os.path.join(resolved_path, 'index.md')
return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'markdown'
logger.info("final DIRECT path = '%s' for request '%s'", resolved_path, path)
return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'file'
# if we're here, this isn't direct file access, so try markdown inference
verbatim_path = os.path.abspath(os.path.join(base_dir, f'{path}.md'))
resolved_path = os.path.realpath(verbatim_path)
# does the final file actually exist?
if not os.path.exists(absolute_resource):
logger.warning("requested final path '%s' does not exist!", absolute_resource)
if not os.path.exists(resolved_path):
logger.warning("requested final path '%s' does not exist!", resolved_path)
raise FileNotFoundError
# check for symlinks
if verbatim_path != resolved_path:
logger.info("client requested a path '%s' that is actually a symlink to file '%s'", path, resolved_path)
return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'symlink'
logger.info("final path = '%s' for request '%s'", resolved_path, path)
# we checked that the file exists via absolute path, but now we need to give the path relative to instance dir
return absolute_resource.replace(f'{app.instance_path}{os.path.sep}', '')
return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'markdown'
def instance_resource_path_to_request_path(path):
@@ -127,8 +157,19 @@ def generate_parent_navs(path):
md = init_md()
# read the resource
with app.open_instance_resource(path, 'r') as entry_file:
entry = entry_file.read()
_ = Markup(md.convert(entry))
page_name = " ".join(md.Meta.get('title')) if md.Meta.get('title') else request_path
return generate_parent_navs(parent_resource_path) + [(page_name, request_path)]
try:
with app.open_instance_resource(path, 'r') as entry_file:
entry = entry_file.read()
_ = Markup(md.convert(entry))
page_name = (" ".join(md.Meta.get('title')) if md.Meta.get('title')
else request_path_to_breadcrumb_display(request_path))
return generate_parent_navs(parent_resource_path) + [(page_name, request_path)]
except FileNotFoundError:
return generate_parent_navs(parent_resource_path) + [(request_path, request_path)]
def request_path_to_breadcrumb_display(path):
"""Given a request path, e.g. "/foo/bar/baz/", turn it into breadcrumby text "baz"."""
undired = path.rstrip('/')
leaf = undired[undired.rfind('/'):]
return leaf.strip('/')

View File

@@ -1,3 +1,5 @@
@import '/static/css/base.css';
html {
color: #CCC;
}

View File

@@ -1,3 +1,5 @@
@import '/static/css/base.css';
html {
color: #222;
}

View File

@@ -0,0 +1,8 @@
.img-25 {
max-width: 25% !important;
}
.img-50 {
max-width: 50% !important;
}

View File

@@ -7,9 +7,8 @@
<meta property="og:url" content="{{ base_url }}">
<meta name="twitter:card" content="summary_large_image">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename=user_style) }}">
<link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}">
<link rel="icon" href="{% if config.FAVICON %}{{ config.FAVICON }}{% else %}{{ url_for('static', filename='img/favicon.png') }}{% endif %}">
<div class="site-wrap">
{% block header %}
@@ -23,6 +22,7 @@
<div class="styles">
<a href="?style=dark">[dark]</a>
<a href="?style=light">[light]</a>
<a href="?style=plain">[plain]</a>
</div>
</div>
{% endblock %}