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.Linq;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Plugin.ShareLinks.Configuration;
|
using Jellyfin.Plugin.ShareLinks.Configuration;
|
||||||
@@ -13,6 +14,7 @@ using Jellyfin.Plugin.ShareLinks.Storage;
|
|||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -315,18 +317,63 @@ public sealed class ShareLinksController : ControllerBase
|
|||||||
SetNoStoreHeaders();
|
SetNoStoreHeaders();
|
||||||
if (string.IsNullOrWhiteSpace(token))
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
{
|
{
|
||||||
return NotFound();
|
return LinkUnavailablePage(Request);
|
||||||
}
|
}
|
||||||
|
|
||||||
var html = await _redemptionService.RedeemAsync(token, Request, cancellationToken).ConfigureAwait(false);
|
var html = await _redemptionService.RedeemAsync(token, Request, cancellationToken).ConfigureAwait(false);
|
||||||
if (html is null)
|
if (html is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return LinkUnavailablePage(Request);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Content(html, "text/html; charset=utf-8");
|
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)
|
private static ShareLinkAdminRecordDto ToDto(ShareLinkRecord record)
|
||||||
{
|
{
|
||||||
return new ShareLinkAdminRecordDto
|
return new ShareLinkAdminRecordDto
|
||||||
|
|||||||
@@ -45,9 +45,7 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a comma-separated list of CSS selectors that are hidden from guest
|
/// 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
|
/// 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
|
/// bars, floating buttons) so a guest only sees the shared title. Empty by default.
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
/// </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;">
|
<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)" />
|
<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>
|
||||||
|
|
||||||
<div class="sl-field inputContainer">
|
<div class="sl-field inputContainer">
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
page.querySelector('#MaxExpiryHours').value = cfg.MaxExpiryHours || 720;
|
page.querySelector('#MaxExpiryHours').value = cfg.MaxExpiryHours || 720;
|
||||||
page.querySelector('#PublicBaseUrlOverride').value = cfg.PublicBaseUrlOverride || '';
|
page.querySelector('#PublicBaseUrlOverride').value = cfg.PublicBaseUrlOverride || '';
|
||||||
page.querySelector('#GuestUsernamePrefix').value = cfg.GuestUsernamePrefix || 'share-';
|
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('#AllowTranscoding').checked = cfg.AllowTranscoding !== false;
|
||||||
page.querySelector('#AllowRemuxing').checked = cfg.AllowRemuxing !== false;
|
page.querySelector('#AllowRemuxing').checked = cfg.AllowRemuxing !== false;
|
||||||
page.querySelector('#CleanupIntervalMinutes').value = cfg.CleanupIntervalMinutes || 60;
|
page.querySelector('#CleanupIntervalMinutes').value = cfg.CleanupIntervalMinutes || 60;
|
||||||
|
|||||||
Référencer dans un nouveau ticket
Bloquer un utilisateur