No more inline CSS

This commit is contained in:
Thomas Sileo 2022-08-29 21:42:54 +02:00
parent 2fb85e138e
commit ebdba62a06
11 changed files with 213 additions and 66 deletions

View file

@ -71,6 +71,7 @@ from app.templates import is_current_user_admin
from app.uploads import UPLOAD_DIR
from app.utils import pagination
from app.utils.emoji import EMOJIS_BY_NAME
from app.utils.highlight import HIGHLIGHT_CSS_HASH
from app.utils.url import check_url
from app.webfinger import get_remote_follow_template
@ -133,9 +134,10 @@ class CustomMiddleware:
headers["x-xss-protection"] = "1; mode=block"
headers["x-frame-options"] = "SAMEORIGIN"
# TODO(ts): disallow inline CSS?
headers[
"content-security-policy"
] = "default-src 'self'; style-src 'self' 'unsafe-inline';"
headers["content-security-policy"] = (
f"default-src 'self'; "
f"style-src 'self' 'sha256-{HIGHLIGHT_CSS_HASH}';"
)
if not DEBUG:
headers["strict-transport-security"] = "max-age=63072000;"

View file

@ -17,6 +17,36 @@ $code-highlight-background: #f0f0f0;
color: $primary-color;
}
#admin {
.admin-menu {
margin-bottom: 30px;
padding: 0 20px;
}
}
.empty-state {
padding: 20px;
}
.public-top-menu {
margin: 30px 0 0 0;
}
.width-95 {
width: 95%;
}
.bold {
font-weight: bold;
}
.admin-new {
textarea {
font-size: 1.2em;
width: 95%;
}
}
.show-more-wrapper {
.p-summary {
display: inline-block;
@ -65,13 +95,6 @@ blockquote {
color: $muted-color;
}
.poll-bar {
width:100%;height:20px;
line {
stroke: $secondary-color;
}
}
.light-background {
background: $light-background;
}
@ -240,6 +263,9 @@ footer {
padding: 0 20px;
li {
display: block;
span {
padding-right:10px;
}
}
}
@ -274,6 +300,57 @@ footer {
margin: 20px 0;
}
.show-hide-sensitive-btn {
display:inline-block;
}
.no-margin-top {
margin-top: 0;
}
.float-right {
float: right;
}
ul.poll-items {
list-style-type: none;
padding: 0;
li {
display: block;
p {
margin: 20px 0 10px 0;
.poll-vote {
padding-left: 20px;
}
}
.poll-bar {
width:100%;height:20px;
line {
stroke: $secondary-color;
stroke-width: 20px;
}
}
}
}
.attachment-wrapper {
.attachment-item {
margin-top: 20px;
}
img.attachment {
margin: 0;
}
a.attachment {
display: inline-block;
margin-bottom: 15px;
}
audio.attachment {
width: 480px;
}
}
nav {
form {
margin: 15px 0;
@ -334,7 +411,7 @@ nav.flexbox {
}
}
.activity-attachment {
margin: 30px 0;
margin: 30px 0 20px 0;
img, audio, video {
width: 100%;
max-width: 740px;
@ -345,6 +422,20 @@ nav.flexbox {
max-width: 740px;
}
}
.activity-og-meta {
display: flex;
column-gap: 20px;
margin: 20px 0;
img {
max-width: 200px;
max-height: 100px;
}
small {
display: block;
}
}
.ap-object-expanded {
border: 2px dashed $secondary-color;
}
@ -367,3 +458,54 @@ nav.flexbox {
.emoji, .custom-emoji {
max-width: 25px;
}
.indieauth-box {
display: flex;
column-gap: 20px;
.indieauth-logo {
flex: initial;
width: 100px;
img {
max-width: 100px;
}
}
.indieauth-details {
flex: 1;
div {
padding-left: 20px;
a {
font-size: 1.2em;
font-weight: 600;
}
}
}
}
.public-interactions {
display: flex;
column-gap: 20px;
flex-wrap: wrap;
margin-top: 20px;
.interactions-block {
flex: 0 1 30%;
max-width: 50%;
.facepile-wrapper {
display: flex;
column-gap: 20px;
row-gap: 20px;
flex-wrap: wrap;
margin-top: 20px;
a {
height: 50px;
img {
max-width: 50px;
}
}
.and-x-more {
display: inline-block;
align-self: center;
}
}
}
}

View file

@ -25,7 +25,7 @@
</nav>
<form class="form" action="{{ request.url_for("admin_actions_new") }}" enctype="multipart/form-data" method="POST">
<form class="form" action="{{ request.url_for("admin_actions_new") }}" enctype="multipart/form-data" method="POST" class="admin-new">
{{ utils.embed_csrf_token() }}
{{ utils.embed_redirect_url() }}
<p>
@ -38,7 +38,7 @@
{% if request.query_params.type == "Article" %}
<p>
<input type="text" style="width:95%" name="name" placeholder="Title">
<input type="text" class="width-95" name="name" placeholder="Title">
</p>
{% endif %}
@ -49,7 +49,7 @@
<span class="ji"><img src="{{ emoji.icon.url }}" alt="{{ emoji.name }}" title="{{ emoji.name }}" class="custom-emoji"></span>
{% endfor %}
<textarea name="content" rows="10" cols="50" autofocus="autofocus" designMode="on" placeholder="Hey!" style="font-size:1.2em;width:95%;">{{ content }}</textarea>
<textarea name="content" rows="10" cols="50" autofocus="autofocus" designMode="on" placeholder="Hey!">{{ content }}</textarea>
{% if request.query_params.type == "Question" %}
<p>
@ -69,20 +69,20 @@
</p>
{% for i in ["1", "2", "3", "4"] %}
<p>
<input type="text" name="poll_answer_{{ i }}" style="width:95%;" placeholder="Option {{ i }}, leave empty to disable">
<input type="text" name="poll_answer_{{ i }}" class="width-95" placeholder="Option {{ i }}, leave empty to disable">
</p>
{% endfor %}
{% endif %}
<p>
<input type="text" name="content_warning" placeholder="content warning (will mark the post as sensitive)"{% if content_warning %} value="{{ content_warning }}"{% endif %} style="width:95%;">
<input type="text" name="content_warning" placeholder="content warning (will mark the post as sensitive)"{% if content_warning %} value="{{ content_warning }}"{% endif %} class="width-95">
</p>
<p>
<input type="checkbox" name="is_sensitive" id="is_sensitive"> <label for="is_sensitive">Mark attachment(s) as sensitive</label>
</p>
<input type="hidden" name="in_reply_to" value="{{ request.query_params.in_reply_to }}">
<p>
<input id="files" name="files" type="file" multiple style="width:95%;">
<input id="files" name="files" type="file" class="width-95" multiple>
</p>
<div id="alts"></div>
<p>

View file

@ -12,7 +12,7 @@
<data class="p-name" value="{{ local_actor.display_name}}'s articles"></data>
{% for outbox_object in objects %}
<li>
<span class="muted" style="padding-right:10px;">{{ outbox_object.ap_published_at.strftime("%b %d, %Y") }}</span> <a href="{{ outbox_object.url }}">{{ outbox_object.name }}</a>
<span class="muted">{{ outbox_object.ap_published_at.strftime("%b %d, %Y") }}</span> <a href="{{ outbox_object.url }}">{{ outbox_object.name }}</a>
</li>
{% endfor %}
</ul>

View file

@ -29,7 +29,7 @@
<a href="{{ url_for }}" {% if request.url.path == url_for %}class="active"{% endif %}>{{ text }}</a>
{% endmacro %}
<div style="margin:30px 0 0 0;">
<div class="public-top-menu">
<nav class="flexbox">
<ul>
<li>{{ header_link("index", "Notes") }}</li>

View file

@ -46,7 +46,7 @@
</div>
{% else %}
<div style="padding: 20px;">
<div class="empty-state">
<p>Nothing to see here yet!</p>
</div>
{% endif %}

View file

@ -2,15 +2,15 @@
{% extends "layout.html" %}
{% block content %}
<div class="box">
<div style="display:flex;column-gap: 20px;">
<div class"indieauth-box">
{% if client.logo %}
<div style="flex:initial;width:100px;">
<img src="{{client.logo | media_proxy_url }}" style="max-width:100px;" alt="{{ client.name }} logo">
<div class="indieauth-logo">
<img src="{{client.logo | media_proxy_url }}" alt="{{ client.name }} logo">
</div>
{% endif %}
<div style="flex:1;">
<div style="padding-left: 20px;">
<a class="lcolor" style="font-size:1.2em;font-weight:600;" href="{{ client.url }}">{{ client.name }}</a>
<div class="indieauth-details">
<div>
<a class="lcolor" href="{{ client.url }}">{{ client.name }}</a>
<p>wants you to login as <strong class="lcolor">{{ me }}</strong> with the following redirect URI: <code>{{ redirect_uri }}</code>.</p>

View file

@ -9,9 +9,7 @@
<link rel="alternate" href="{{ url_for("rss_feed") }}" type="application/rss+xml" title="{{ local_actor.display_name}}'s microblog">
<link rel="alternate" href="{{ url_for("atom_feed") }}" type="application/atom+xml" title="{{ local_actor.display_name}}'s microblog">
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<style>
{{ highlight_css }}
</style>
<style>{{ highlight_css }}</style>
{% block head %}{% endblock %}
</head>
<body>
@ -23,7 +21,7 @@
{% set url_for = request.app.router.url_path_for(url) %}
<a href="{{ url_for }}" {% if request.url.path == url_for %}class="active"{% endif %}>{{ text }}</a>
{% endmacro %}
<div style="margin-bottom:30px;padding: 0 20px;">
<div class="admin-menu">
<nav class="flexbox">
<ul>
<li>{{ admin_link("index", "Public") }}</li>

View file

@ -58,7 +58,7 @@
{% if facepile_item %}
<a href="{{ facepile_item.actor_url }}">{{ facepile_item.actor_name }}</a>
{% endif %}
<a style="font-weight:bold;" href="{{ notif.webmention.source }}">{{ notif.webmention.source }}</a>
<a class="bold" href="{{ notif.webmention.source }}">{{ notif.webmention.source }}</a>
</div>
{{ utils.display_object(notif.outbox_object) }}
{% elif notif.notification_type.value == "updated_webmention" %}
@ -68,7 +68,7 @@
{% if facepile_item %}
<a href="{{ facepile_item.actor_url }}">{{ facepile_item.actor_name }}</a>
{% endif %}
<a style="font-weight:bold;" href="{{ notif.webmention.source }}">{{ notif.webmention.source }}</a>
<a class="bold" href="{{ notif.webmention.source }}">{{ notif.webmention.source }}</a>
</div>
{{ utils.display_object(notif.outbox_object) }}
{% elif notif.notification_type.value == "deleted_webmention" %}
@ -78,7 +78,7 @@
{% if facepile_item %}
<a href="{{ facepile_item.actor_url }}">{{ facepile_item.actor_name }}</a>
{% endif %}
<a style="font-weight:bold;" href="{{ notif.webmention.source }}">{{ notif.webmention.source }}</a>
<a class="bold" href="{{ notif.webmention.source }}">{{ notif.webmention.source }}</a>
</div>
{{ utils.display_object(notif.outbox_object) }}
{% else %}

View file

@ -206,7 +206,7 @@
<div class="icon-box">
<img src="{{ actor.resized_icon_url }}" alt="{{ actor.display_name }}'s avatar" class="actor-icon u-photo">
</div>
<a href="{{ actor.url }}" class="u-url" style="">
<a href="{{ actor.url }}" class="u-url">
<div><strong>{{ actor.display_name | clean_html(actor) | safe }}</strong></div>
<div class="actor-handle p-name">{{ actor.handle }}</div>
</a>
@ -292,16 +292,16 @@
{% macro display_og_meta(object) %}
{% if object.og_meta %}
{% for og_meta in object.og_meta %}
<div class="activity-og-meta" style="display:flex;column-gap: 20px;margin:20px 0;">
<div class="activity-og-meta">
{% if og_meta.image %}
<div>
<img src="{{ og_meta.image | media_proxy_url }}" style="max-width:200px;max-height:100px;">
<img src="{{ og_meta.image | media_proxy_url }}">
</div>
{% endif %}
<div>
<a href="{{ og_meta.url | privacy_replace_url }}">{{ og_meta.title }}</a>
{% if og_meta.site_name %}
<small style="display:block;">{{ og_meta.site_name }}</small>
<small>{{ og_meta.site_name }}</small>
{% endif %}
</div>
</div>
@ -314,27 +314,27 @@
{% for attachment in object.attachments %}
{% if object.sensitive and (attachment.type == "Image" or (attachment | has_media_type("image")) or attachment.type == "Video" or (attachment | has_media_type("video"))) %}
<div>
<label for="{{attachment.proxied_url}}" class="label-btn" style="display:inline-block;">show/hide sensitive content</label>
<div class="attachment-wrapper">
<label for="{{attachment.proxied_url}}" class="label-btn show-hide-sensitive-btn">show/hide sensitive content</label>
<div>
<div class="sensitive-attachment">
<input class="sensitive-attachment-state" type="checkbox" id="{{attachment.proxied_url}}" aria-hidden="true">
<div class="sensitive-attachment-box">
<div></div>
{% else %}
<div style="margin-top:20px;">
<div class="attachment-item">
{% endif %}
{% if attachment.type == "Image" or (attachment | has_media_type("image")) %}
{% if attachment.url not in object.inlined_images %}
<img src="{{ attachment.resized_url or attachment.proxied_url }}"{% if attachment.name %} title="{{ attachment.name }}" alt="{{ attachment.name }}"{% endif %} class="attachment" style="margin:0;">
<img src="{{ attachment.resized_url or attachment.proxied_url }}"{% if attachment.name %} title="{{ attachment.name }}" alt="{{ attachment.name }}"{% endif %} class="attachment">
{% endif %}
{% elif attachment.type == "Video" or (attachment | has_media_type("video")) %}
<video controls preload="metadata" src="{{ attachment.url | media_proxy_url }}"{% if attachment.name %} title="{{ attachment.name }}"{% endif %}></video>
{% elif attachment.type == "Audio" or (attachment | has_media_type("audio")) %}
<audio controls preload="metadata" src="{{ attachment.url | media_proxy_url }}"{% if attachment.name%} title="{{ attachment.name }}"{% endif %} style="width:480px;" class="attachment"></audio>
<audio controls preload="metadata" src="{{ attachment.url | media_proxy_url }}"{% if attachment.name%} title="{{ attachment.name }}"{% endif %} class="attachment"></audio>
{% elif attachment.type == "Link" %}
<a href="{{ attachment.url }}" class="attachment" style="display:inline-block;margin-bottom: 15px;">{{ attachment.url }}</a>
<a href="{{ attachment.url }}" class="attachment">{{ attachment.url }}</a>
{% else %}
<a href="{{ attachment.url | media_proxy_url }}"{% if attachment.name %} title="{{ attachment.name }}"{% endif %} class="attachment">{{ attachment.url }}</a>
{% endif %}
@ -371,7 +371,7 @@
{% endif %}
{% if object.ap_type == "Article" %}
<h2 class="p-name" style="margin-top:0;">{{ object.name }}</h2>
<h2 class="p-name no-margin-top">{{ object.name }}</h2>
{% endif %}
{% if is_article_mode %}
@ -401,11 +401,11 @@
{% endif %}
{% if object.poll_items %}
<ul style="list-style-type: none;padding:0;">
<ul class="poll-items">
{% for item in object.poll_items %}
<li style="display:block;">
<li>
{% set pct = item | poll_item_pct(object.poll_voters_count) %}
<p style="margin:20px 0 10px 0;">
<p>
{% if can_vote %}
<input type="{% if object.is_one_of_poll %}radio{% else %}checkbox{% endif %}" name="name" value="{{ item.name }}" id="{{object.permalink_id}}-{{item.name}}">
<label for="{{object.permalink_id}}-{{item.name}}">
@ -414,17 +414,17 @@
{{ item.name | clean_html(object) | safe }}
{% if object.voted_for_answers and item.name in object.voted_for_answers %}
<span class="muted" style="padding-left:20px;">you voted for this answer</span>
<span class="muted poll-vote">you voted for this answer</span>
{% endif %}
{% if can_vote %}
</label>
{% endif %}
<span style="float:right;">{{ pct }}% <span class="muted">({{ item.replies.totalItems }} votes)</span></span>
<span class="float-right">{{ pct }}% <span class="muted">({{ item.replies.totalItems }} votes)</span></span>
</p>
<svg class="poll-bar">
<line x1="0" y1="10px" x2="{{ pct or 1 }}%" y2="10px" style="stroke-width: 20px;"></line>
<line x1="0" y1="10px" x2="{{ pct or 1 }}%" y2="10px"></line>
</svg>
</li>
{% endfor %}
@ -447,7 +447,7 @@
</div>
{% endif %}
<div class="activity-attachment" style="margin-bottom:20px;">
<div class="activity-attachment">
{{ display_attachments(object) }}
</div>
@ -575,17 +575,17 @@
{% if likes or shares or webmentions %}
<div style="display: flex;column-gap: 20px;flex-wrap: wrap;margin-top:20px;">
<div class="public-interactions">
{% if likes %}
<div style="flex: 0 1 30%;max-width: 50%;">Likes
<div style="display: flex;column-gap: 20px;row-gap:20px;flex-wrap: wrap;margin-top:20px;">
<div class="interactions-block">Likes
<div class="facepile-wrapper">
{% for like in likes %}
<a href="{% if is_admin %}{{ url_for("admin_profile") }}?actor_id={{ like.actor.ap_id }}{% else %}{{ like.actor.url }}{% endif %}" title="{{ like.actor.handle }}" style="height:50px;" rel="noreferrer">
<img src="{{ like.actor.resized_icon_url }}" alt="{{ like.actor.handle}}" style="max-width:50px;">
<a href="{% if is_admin %}{{ url_for("admin_profile") }}?actor_id={{ like.actor.ap_id }}{% else %}{{ like.actor.url }}{% endif %}" title="{{ like.actor.handle }}" rel="noreferrer">
<img src="{{ like.actor.resized_icon_url }}" alt="{{ like.actor.handle}}">
</a>
{% endfor %}
{% if object.likes_count > likes | length %}
<div style="display:inline-block;align-self:center;">
<div class="and-x-more">
and {{ object.likes_count - likes | length }} more.
</div>
{% endif %}
@ -594,15 +594,15 @@
{% endif %}
{% if shares %}
<div style="flex: 0 1 30%;max-width: 50%;">Shares
<div style="display: flex;column-gap: 20px;row-gap:20px;flex-wrap: wrap;margin-top:20px;">
<div class="interactions-block">Shares
<div class="facepile-wrapper">
{% for share in shares %}
<a href="{% if is_admin %}{{ url_for("admin_profile") }}?actor_id={{ share.actor.ap_id }}{% else %}{{ share.actor.url }}{% endif %}" title="{{ share.actor.handle }}" style="height:50px;" rel="noreferrer">
<img src="{{ share.actor.resized_icon_url }}" alt="{{ share.actor.handle}}" style="max-width:50px;">
<a href="{% if is_admin %}{{ url_for("admin_profile") }}?actor_id={{ share.actor.ap_id }}{% else %}{{ share.actor.url }}{% endif %}" title="{{ share.actor.handle }}" rel="noreferrer">
<img src="{{ share.actor.resized_icon_url }}" alt="{{ share.actor.handle}}">
</a>
{% endfor %}
{% if object.announces_count > shares | length %}
<div style="display:inline-block;align-self:center;">
<div class="and-x-more">
and {{ object.announces_count - shares | length }} more.
</div>
{% endif %}
@ -611,13 +611,13 @@
{% endif %}
{% if webmentions %}
<div style="flex: 0 1 30%;max-width: 50%;">Webmentions
<div style="display: flex;column-gap: 20px;row-gap:20px;flex-wrap: wrap;margin-top:20px;">
<div class="interactions-block">Webmentions
<div class="facepile-wrapper">
{% for webmention in webmentions %}
{% set wm = webmention.as_facepile_item %}
{% if wm %}
<a href="{{ wm.url }}" title="{{ wm.actor_name }}" style="height:50px;" rel="noreferrer">
<img src="{{ wm.actor_icon_url | media_proxy_url }}" alt="{{ wm.actor_name }}" style="max-width:50px;">
<a href="{{ wm.url }}" title="{{ wm.actor_name }}" rel="noreferrer">
<img src="{{ wm.actor_icon_url | media_proxy_url }}" alt="{{ wm.actor_name }}">
</a>
{% endif %}
{% endfor %}

View file

@ -1,3 +1,5 @@
import base64
import hashlib
from functools import lru_cache
from bs4 import BeautifulSoup # type: ignore
@ -11,6 +13,9 @@ from app.config import CODE_HIGHLIGHTING_THEME
_FORMATTER = HtmlFormatter(style=CODE_HIGHLIGHTING_THEME)
HIGHLIGHT_CSS = _FORMATTER.get_style_defs()
HIGHLIGHT_CSS_HASH = base64.b64encode(
hashlib.sha256(HIGHLIGHT_CSS.encode()).digest()
).decode()
@lru_cache(256)