show a friendly page for dead share links, drop plugin-specific selector defaults

Opening an expired or invalid link used to dump raw problem-details
JSON at the guest. Now they get a small page matching the sign-in
look, with the same neutral wording whether the link expired or never
existed (so tokens cannot be probed by outsiders), and an automatic
redirect to the Jellyfin home page. Fixes #1.

GuestHiddenSelectors now defaults to empty instead of shipping CSS
selectors for a plugin nobody else runs. Existing installs keep their
saved value. Fixes #3.
Cette révision appartient à :
Franciskid
2026-07-08 01:39:28 +02:00
Parent 2fb912e734
révision 06a49d0bfe
3 fichiers modifiés avec 53 ajouts et 8 suppressions
+49 -2
Voir le fichier
@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.ShareLinks.Configuration;
@@ -13,6 +14,7 @@ using Jellyfin.Plugin.ShareLinks.Storage;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@@ -315,18 +317,63 @@ public sealed class ShareLinksController : ControllerBase
SetNoStoreHeaders();
if (string.IsNullOrWhiteSpace(token))
{
return NotFound();
return LinkUnavailablePage(Request);
}
var html = await _redemptionService.RedeemAsync(token, Request, cancellationToken).ConfigureAwait(false);
if (html is null)
{
return NotFound();
return LinkUnavailablePage(Request);
}
return Content(html, "text/html; charset=utf-8");
}
private static ContentResult LinkUnavailablePage(HttpRequest request)
{
var pathBase = request.PathBase.Value ?? string.Empty;
var redirectUrl = $"{pathBase}/web/";
var redirectUrlJson = JsonSerializer.Serialize(redirectUrl);
var redirectUrlHtml = System.Net.WebUtility.HtmlEncode(redirectUrl);
var html = $$"""
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="refresh" content="6;url={{redirectUrlHtml}}">
<title>Link unavailable</title>
<style>
body { font-family: system-ui, sans-serif; margin: 0; min-height: 100vh; display: grid; place-items: center; background: #111827; color: #e5e7eb; }
main { max-width: 36rem; padding: 2rem; }
.muted { color: #9ca3af; }
a { color: #60a5fa; }
</style>
</head>
<body>
<main>
<div>This share link is no longer valid.</div>
<div class="muted">Ce lien de partage n'est plus valide.</div>
<div class="muted">Taking you to the home page...</div>
<p><a href="{{redirectUrlHtml}}">Open Jellyfin</a></p>
</main>
<script>
setTimeout(function () { window.location.replace({{redirectUrlJson}}); }, 4000);
</script>
</body>
</html>
""";
return new ContentResult
{
StatusCode = StatusCodes.Status404NotFound,
ContentType = "text/html; charset=utf-8",
Content = html
};
}
private static ShareLinkAdminRecordDto ToDto(ShareLinkRecord record)
{
return new ShareLinkAdminRecordDto
+2 -4
Voir le fichier
@@ -45,9 +45,7 @@ public class PluginConfiguration : BasePluginConfiguration
/// <summary>
/// Gets or sets a comma-separated list of CSS selectors that are hidden from guest
/// sessions in the web client. Used to suppress other plugins' injected UI (search
/// bars, floating buttons) so a guest only sees the shared title. The default value
/// hides the elements of a plugin that injects its own floating button and panel
/// into the web client; edit or clear it to match whatever else you run.
/// bars, floating buttons) so a guest only sees the shared title. Empty by default.
/// </summary>
public string GuestHiddenSelectors { get; set; } = ".ais-fab,.ais-overlay,#ais-styles";
public string GuestHiddenSelectors { get; set; } = string.Empty;
}
+2 -2
Voir le fichier
@@ -125,7 +125,7 @@
<div class="sl-field inputContainer" style="grid-column: 1 / -1;">
<input is="emby-input" type="text" id="GuestHiddenSelectors" label="Guest hidden selectors (CSS, comma-separated)" />
<div class="fieldDescription">Elements hidden from guest sessions, e.g. other plugins' buttons. The default hides a plugin I had installed that injects its own floating UI into the web client.</div>
<div class="fieldDescription">Elements hidden from guest sessions, e.g. other plugins' buttons. Empty by default.</div>
</div>
<div class="sl-field inputContainer">
@@ -212,7 +212,7 @@
page.querySelector('#MaxExpiryHours').value = cfg.MaxExpiryHours || 720;
page.querySelector('#PublicBaseUrlOverride').value = cfg.PublicBaseUrlOverride || '';
page.querySelector('#GuestUsernamePrefix').value = cfg.GuestUsernamePrefix || 'share-';
page.querySelector('#GuestHiddenSelectors').value = cfg.GuestHiddenSelectors != null ? cfg.GuestHiddenSelectors : '.ais-fab,.ais-overlay,#ais-styles';
page.querySelector('#GuestHiddenSelectors').value = cfg.GuestHiddenSelectors != null ? cfg.GuestHiddenSelectors : '';
page.querySelector('#AllowTranscoding').checked = cfg.AllowTranscoding !== false;
page.querySelector('#AllowRemuxing').checked = cfg.AllowRemuxing !== false;
page.querySelector('#CleanupIntervalMinutes').value = cfg.CleanupIntervalMinutes || 60;