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

Merge branch 'master' into develop

parents d5d85ea9 0989df47
No related branches found
No related tags found
No related merge requests found
import html
import requests
import xml.sax.saxutils
from django import http
from django.conf import settings
......@@ -51,7 +52,13 @@ def serve_spa(request):
# let's inject our meta tags in the HTML
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)
......@@ -128,6 +135,14 @@ def get_request_head_tags(request):
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:
def __init__(self, get_response):
self.get_response = get_response
......
......@@ -4,6 +4,7 @@ from dynamic_preferences.registries import global_preferences_registry
raven = types.Section("raven")
instance = types.Section("instance")
ui = types.Section("ui")
@global_preferences_registry.register
......@@ -98,3 +99,19 @@ class InstanceNodeinfoStatsEnabled(types.BooleanPreference):
"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."
)
@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}
......@@ -256,7 +256,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
if max_bitrate:
max_bitrate = max_bitrate * 1000
format = data.get("format", "raw") or None
format = data.get("format") or None
if max_bitrate and not format:
# specific bitrate requested, but no format specified
# so we use a default one, cf #867. This helps with clients
......
......@@ -141,3 +141,47 @@ def test_get_route_head_tags(mocker, settings):
assert tags == match.func.return_value
match.func.assert_called_once_with(request, *[], **{"pk": 42})
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
......@@ -29,8 +29,8 @@ from rest_framework.test import APIClient, APIRequestFactory
from funkwhale_api.activity import record
from funkwhale_api.federation import actors
from funkwhale_api.music import licenses
from funkwhale_api.moderation import mrf
from funkwhale_api.music import licenses
pytest_plugins = "aiohttp.pytest_plugin"
......
......@@ -288,15 +288,16 @@ def test_stream_transcode(
mocker,
settings,
):
upload = factories["music.Upload"](playable=True)
params = {"id": upload.track.pk, "maxBitRate": max_bitrate}
if format:
params["format"] = format
settings.SUBSONIC_DEFAULT_TRANSCODING_FORMAT = default_transcoding_format
url = reverse("api:subsonic-stream")
mocked_serve = mocker.patch.object(
music_views, "handle_serve", return_value=Response()
)
upload = factories["music.Upload"](playable=True)
response = logged_in_api_client.get(
url, {"id": upload.track.pk, "maxBitRate": max_bitrate, "format": format}
)
response = logged_in_api_client.get(url, params)
mocked_serve.assert_called_once_with(
upload=upload,
......
Fixed remaining transcoding issue with Subsonic API (#867)
Admins can now add custom CSS from their pod settings (#879)
......@@ -364,7 +364,11 @@ export default {
return
}
let image = this.$refs.cover
this.ambiantColors = ColorThief.prototype.getPalette(image, 4).slice(0, 4)
try {
this.ambiantColors = ColorThief.prototype.getPalette(image, 4).slice(0, 4)
} catch (e) {
console.log('Cannot generate player background from cover image, likely a cross-origin tainted canvas issue')
}
},
handleError({ sound, error }) {
this.$store.commit("player/isLoadingAudio", false)
......
......@@ -20,7 +20,7 @@
<tbody>
<tr v-for="track in tracks">
<td class="play-cell">
<play-button :class="['basic', {orange: isPlaying && track.id === currentTrack.id}, 'icon']" :discrete="true" :track="track"></play-button>
<play-button :class="['basic', {orange: currentTrack && isPlaying && track.id === currentTrack.id}, 'icon']" :discrete="true" :track="track"></play-button>
</td>
<td class="content-cell" colspan="5">
<track-favorite-icon :track="track"></track-favorite-icon>
......
<template>
<tr>
<td>
<play-button :class="['basic', {orange: isPlaying && track.id === currentTrack.id}, 'icon']" :discrete="true" :is-playable="playable" :track="track"></play-button>
<play-button :class="['basic', {orange: currentTrack && isPlaying && track.id === currentTrack.id}, 'icon']" :discrete="true" :is-playable="playable" :track="track"></play-button>
</td>
<td>
<img class="ui mini image" v-if="track.album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover.small_square_crop)">
......
......@@ -85,6 +85,7 @@ export default {
let moderationLabel = this.$pgettext('Content/Admin/Menu', 'Moderation')
let subsonicLabel = this.$pgettext('Content/Admin/Menu', 'Subsonic')
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')
return [
{
......@@ -143,6 +144,11 @@ export default {
id: "subsonic",
settings: ["subsonic__enabled"]
},
{
label: uiLabel,
id: "ui",
settings: ["ui__custom_css"]
},
{
label: statisticsLabel,
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