Skip to content
Snippets Groups Projects
Verified Commit 7897c8ac authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Fix #879: Admins can now add custom CSS from their pod settings

parent 78ab1537
No related branches found
No related tags found
No related merge requests found
import html import html
import requests import requests
import xml.sax.saxutils
from django import http from django import http
from django.conf import settings from django.conf import settings
...@@ -51,7 +52,13 @@ def serve_spa(request): ...@@ -51,7 +52,13 @@ def serve_spa(request):
# let's inject our meta tags in the HTML # let's inject our meta tags in the HTML
head += "\n" + "\n".join(render_tags(final_tags)) + "\n</head>" head += "\n" + "\n".join(render_tags(final_tags)) + "\n</head>"
css = get_custom_css() or ""
if css:
# We add the style add the end of the body to ensure it has the highest
# priority (since it will come after other stylesheets)
body, tail = tail.split("</body>", 1)
css = "<style>{}</style>".format(css)
tail = body + "\n" + css + "\n</body>" + tail
return http.HttpResponse(head + tail) return http.HttpResponse(head + tail)
...@@ -128,6 +135,14 @@ def get_request_head_tags(request): ...@@ -128,6 +135,14 @@ def get_request_head_tags(request):
return match.func(request, *match.args, **match.kwargs) return match.func(request, *match.args, **match.kwargs)
def get_custom_css():
css = preferences.get("ui__custom_css").strip()
if not css:
return
return xml.sax.saxutils.escape(css)
class SPAFallbackMiddleware: class SPAFallbackMiddleware:
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
......
...@@ -4,6 +4,7 @@ from dynamic_preferences.registries import global_preferences_registry ...@@ -4,6 +4,7 @@ from dynamic_preferences.registries import global_preferences_registry
raven = types.Section("raven") raven = types.Section("raven")
instance = types.Section("instance") instance = types.Section("instance")
ui = types.Section("ui")
@global_preferences_registry.register @global_preferences_registry.register
...@@ -98,3 +99,19 @@ class InstanceNodeinfoStatsEnabled(types.BooleanPreference): ...@@ -98,3 +99,19 @@ class InstanceNodeinfoStatsEnabled(types.BooleanPreference):
"Disable this if you don't want to share usage and library statistics " "Disable this if you don't want to share usage and library statistics "
"in the nodeinfo endpoint but don't want to disable it completely." "in the nodeinfo endpoint but don't want to disable it completely."
) )
@global_preferences_registry.register
class CustomCSS(types.StringPreference):
show_in_api = True
section = ui
name = "custom_css"
verbose_name = "Custom CSS code"
default = ""
help_text = (
"Custom CSS code, to be included in a <style> tag on all pages. "
"Loading third-party resources such as fonts or images can affect the performance "
"of the app and the privacy of your users."
)
widget = widgets.Textarea
field_kwargs = {"required": False}
...@@ -141,3 +141,47 @@ def test_get_route_head_tags(mocker, settings): ...@@ -141,3 +141,47 @@ def test_get_route_head_tags(mocker, settings):
assert tags == match.func.return_value assert tags == match.func.return_value
match.func.assert_called_once_with(request, *[], **{"pk": 42}) match.func.assert_called_once_with(request, *[], **{"pk": 42})
resolve.assert_called_once_with(request.path, urlconf=settings.SPA_URLCONF) resolve.assert_called_once_with(request.path, urlconf=settings.SPA_URLCONF)
def test_serve_spa_includes_custom_css(mocker, no_api_auth):
request = mocker.Mock(path="/")
mocker.patch.object(
middleware,
"get_spa_html",
return_value="<html><head></head><body></body></html>",
)
mocker.patch.object(middleware, "get_default_head_tags", return_value=[])
mocker.patch.object(middleware, "get_request_head_tags", return_value=[])
get_custom_css = mocker.patch.object(
middleware, "get_custom_css", return_value="body { background: black; }"
)
response = middleware.serve_spa(request)
assert response.status_code == 200
expected = [
"<html><head>\n\n</head><body>",
"<style>body { background: black; }</style>",
"</body></html>",
]
get_custom_css.assert_called_once_with()
assert response.content == "\n".join(expected).encode()
@pytest.mark.parametrize(
"custom_css, expected",
[
("body { background: black; }", "body { background: black; }"),
(
"body { injection: </style> & Hello",
"body { injection: &lt;/style&gt; &amp; Hello",
),
(
'body { background: url("image/url"); }',
'body { background: url("image/url"); }',
),
],
)
def test_get_custom_css(preferences, custom_css, expected):
preferences["ui__custom_css"] = custom_css
assert middleware.get_custom_css() == expected
Admins can now add custom CSS from their pod settings (#879)
...@@ -84,6 +84,7 @@ export default { ...@@ -84,6 +84,7 @@ export default {
let federationLabel = this.$pgettext('Content/Admin/Menu', 'Federation') let federationLabel = this.$pgettext('Content/Admin/Menu', 'Federation')
let subsonicLabel = this.$pgettext('Content/Admin/Menu', 'Subsonic') let subsonicLabel = this.$pgettext('Content/Admin/Menu', 'Subsonic')
let statisticsLabel = this.$pgettext('Content/Admin/Menu', 'Statistics') let statisticsLabel = this.$pgettext('Content/Admin/Menu', 'Statistics')
let uiLabel = this.$pgettext('Content/Admin/Menu', 'User Interface')
let errorLabel = this.$pgettext('Content/Admin/Menu', 'Error reporting') let errorLabel = this.$pgettext('Content/Admin/Menu', 'Error reporting')
return [ return [
{ {
...@@ -134,6 +135,11 @@ export default { ...@@ -134,6 +135,11 @@ export default {
id: "subsonic", id: "subsonic",
settings: ["subsonic__enabled"] settings: ["subsonic__enabled"]
}, },
{
label: uiLabel,
id: "ui",
settings: ["ui__custom_css"]
},
{ {
label: statisticsLabel, label: statisticsLabel,
id: "statistics", id: "statistics",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment