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 à :
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Référencer dans un nouveau ticket
Bloquer un utilisateur