Requirements register, spec rewrite, and TRACES tags
jRay had no requirement IDs, so nothing in this repo could be traced to and the CI gate had no denominator to read. The other two components had already moved to registers; this brings the plugin level with them. Adds docs/requirements.md with 46 permanent JR-nnn IDs, each carrying a parent requirement, priority, status and verification tier, plus a per-requirement verification plan. JR is flat rather than split by theme: the plugin is one deployable with one audience, and JRay-public-server already ships UR/DR, so a second repo using those prefixes would make UR-007 ambiguous across registers. Rewrites SPEC.md as requirements prose with Current:/Gap: on every one. It had drifted into a format-plus-API reference that documented schema_version 1 while owning a format whose v2 shape was specified only in the other two repos, said nothing about SR-002's scene-scoped semantics, and carried the manifest exchange as a "planned" aside while its configuration classes were already implemented. Plugin-side exchange obligations move here from the server's spec, where they were an ownership inversion. Adds JR-038..041 for PR-005, which had no software row in any repo -- it was held structurally by SR-004 and GR-005 both being prohibitions, and a goal preserved only by prohibitions is the kind that erodes unnoticed. jRay is the component that actually opens a socket. Tags 18 units with the requirements they satisfy. Tags name what the code satisfies, so FileTransformationRegistration is not tagged JR-021: that requirement is a prohibition and was still violated elsewhere when this was written. Vendors jray-project as a submodule for the system spec and shared gate. TRACES: JR-001, JR-004, JR-005, JR-007, JR-008, JR-009, JR-010, JR-011 TRACES: JR-012, JR-013, JR-014, JR-015, JR-016, JR-017, JR-018, JR-019 TRACES: JR-020, JR-024, JR-025, JR-036, JR-038 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
namespace Jellyfin.Plugin.JRay.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// How far a configured manifest server is trusted. See the JRay public server
|
||||
/// specification, §9 "Trusting third-party servers".
|
||||
/// </summary>
|
||||
public enum ServerTrustLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Accept manifests, but never contribute to this server and never send
|
||||
/// library inventory beyond the single item being queried. The default for
|
||||
/// user-added servers.
|
||||
/// </summary>
|
||||
FetchOnly = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Eligible to contribute to, subject to <see cref="ManifestServer.AllowContribute"/>.
|
||||
/// </summary>
|
||||
Full = 1,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The minimum cut-match tier a fetched manifest must reach before it is stored.
|
||||
/// See the public server specification, §3 "Cut matching".
|
||||
/// </summary>
|
||||
public enum MatchTier
|
||||
{
|
||||
/// <summary>Audio 0.60–0.85, or runtimes within ±30s. Surfaced as a caveat in the UI.</summary>
|
||||
Loose = 0,
|
||||
|
||||
/// <summary>Runtimes within ±2s.</summary>
|
||||
Runtime = 1,
|
||||
|
||||
/// <summary>Audio signature score ≥ 0.85; may carry a non-zero offset.</summary>
|
||||
Audio = 2,
|
||||
|
||||
/// <summary>Identical <c>video_hash</c> — the same file.</summary>
|
||||
Exact = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One entry in the ordered list of manifest servers the plugin queries
|
||||
/// (public server specification, §9 "Multiple servers").
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The list is ordered because order *is* the user's trust ranking, made
|
||||
/// explicit: for a fetch, servers are tried in order and the first acceptable
|
||||
/// result wins. Querying every server for every item would multiply egress and
|
||||
/// leak the library to more parties.
|
||||
///
|
||||
/// <c>FetchOnly</c> is the default for user-added servers. Adding a third-party
|
||||
/// server means trusting its operator not to serve deliberately wrong actor
|
||||
/// data — the client-side controls bound the damage to bad overlay content,
|
||||
/// they cannot make wrong data right.
|
||||
/// </remarks>
|
||||
// TRACES: JR-025 | PR-005, PR-006
|
||||
public class ManifestServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManifestServer"/> class.
|
||||
/// </summary>
|
||||
public ManifestServer()
|
||||
{
|
||||
Url = string.Empty;
|
||||
Name = string.Empty;
|
||||
Token = string.Empty;
|
||||
Enabled = false;
|
||||
AllowContribute = false;
|
||||
TrustLevel = ServerTrustLevel.FetchOnly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the base URL of the server, e.g. "https://jray.tourolle.paris".
|
||||
/// HTTPS is required for non-loopback servers: a plaintext server would let
|
||||
/// any network intermediary rewrite actor overlays.
|
||||
/// </summary>
|
||||
public string Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display label shown in the configuration page.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the API token used to contribute manifests. Optional —
|
||||
/// required only to contribute, never to fetch. This is an anonymous bearer
|
||||
/// capability rather than an account (public server specification, §5a).
|
||||
/// </summary>
|
||||
public string Token { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this server is queried at all.
|
||||
/// Lets an admin disable an entry without deleting it and losing its token.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether locally generated manifests may be
|
||||
/// contributed to this server. Independent of fetching, and off by default:
|
||||
/// contribution is never fanned out, because broadcasting uploads to every
|
||||
/// configured server would multiply privacy exposure without the user
|
||||
/// intending it.
|
||||
/// </summary>
|
||||
public bool AllowContribute { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how far this server is trusted.
|
||||
/// </summary>
|
||||
public ServerTrustLevel TrustLevel { get; set; }
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
|
||||
namespace Jellyfin.Plugin.JRay.Configuration;
|
||||
@@ -5,8 +6,28 @@ namespace Jellyfin.Plugin.JRay.Configuration;
|
||||
/// <summary>
|
||||
/// Plugin configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every manifest-exchange switch here defaults to <b>off</b>, including the
|
||||
/// pre-configured community server, so no traffic leaves an installation until
|
||||
/// an admin acts. Fetching and contributing each reveal to a server operator
|
||||
/// that some instance holds a given title; that is inherent to the exchange, so
|
||||
/// the defaults bound the exposure rather than pretending to remove it.
|
||||
/// </remarks>
|
||||
// TRACES: JR-036, JR-038 | PR-005
|
||||
public class PluginConfiguration : BasePluginConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// The community manifest exchange. Shipped pre-configured but
|
||||
/// <b>disabled</b>, so no traffic leaves an installation until an admin opts
|
||||
/// in (public server specification, §9).
|
||||
/// </summary>
|
||||
public const string CommunityServerUrl = "https://jray.tourolle.paris";
|
||||
|
||||
/// <summary>
|
||||
/// Display name for <see cref="CommunityServerUrl"/>.
|
||||
/// </summary>
|
||||
public const string CommunityServerName = "JRay Community";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
|
||||
/// </summary>
|
||||
@@ -15,6 +36,24 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
TruthFileSuffix = ".jray.json";
|
||||
CacheDurationMinutes = 60;
|
||||
EnableOverlay = true;
|
||||
|
||||
// Manifest sharing is a network egress feature, so every part of it is
|
||||
// off by default (public server specification, §9 "Configuration").
|
||||
EnableManifestSharing = false;
|
||||
ContributeManifests = false;
|
||||
ComputeAudioSignatures = false;
|
||||
MinimumMatchTier = MatchTier.Runtime;
|
||||
|
||||
// Pre-configured but disabled: the admin opts in by enabling it, rather
|
||||
// than by having to discover and type a URL.
|
||||
Servers.Add(new ManifestServer
|
||||
{
|
||||
Url = CommunityServerUrl,
|
||||
Name = CommunityServerName,
|
||||
Enabled = false,
|
||||
AllowContribute = false,
|
||||
TrustLevel = ServerTrustLevel.FetchOnly,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -37,4 +76,62 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
/// any previously injected script is removed.
|
||||
/// </summary>
|
||||
public bool EnableOverlay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether JRay may fetch actor-timeline
|
||||
/// manifests from the configured servers. Off by default — this is a network
|
||||
/// egress feature and must be opt-in.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Fetching reveals to a server operator that some instance holds a given
|
||||
/// title. That is inherent to the exchange, and each configured server
|
||||
/// multiplies the exposure, which the configuration page states plainly.
|
||||
/// </remarks>
|
||||
public bool EnableManifestSharing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether locally generated manifests may be
|
||||
/// contributed back. A separate opt-in from downloading, and off by default.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Contribution additionally requires <see cref="ManifestServer.AllowContribute"/>
|
||||
/// on the specific server and a token for it. Uploads are never fanned out to
|
||||
/// every configured server.
|
||||
/// </remarks>
|
||||
public bool ContributeManifests { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum cut-match tier a fetched manifest must reach
|
||||
/// before it is stored.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Defaults to <see cref="MatchTier.Runtime"/>. <see cref="MatchTier.Loose"/>
|
||||
/// admits manifests whose runtime differs by up to 30s, which may be a
|
||||
/// different trim of the same cut — usable, but it should be surfaced as a
|
||||
/// caveat rather than applied silently.
|
||||
/// </remarks>
|
||||
public MatchTier MinimumMatchTier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the plugin computes audio
|
||||
/// signatures for library items, enabling content-based cut matching and
|
||||
/// identification of files whose providence is unknown.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Off by default. Uses the FFmpeg binary Jellyfin already ships (via
|
||||
/// <c>IMediaEncoder.EncoderPath</c>), so there is no extra dependency, but it
|
||||
/// costs roughly a second or two of I/O per item and is therefore opt-in.
|
||||
/// </remarks>
|
||||
public bool ComputeAudioSignatures { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ordered list of manifest servers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Order is the user's trust ranking: for a fetch, servers are tried in order
|
||||
/// and the first result clearing <see cref="MinimumMatchTier"/> wins. For a
|
||||
/// series, first-match applies per <i>episode</i>, so a later server is
|
||||
/// queried only for the episodes earlier ones lacked.
|
||||
/// </remarks>
|
||||
public Collection<ManifestServer> Servers { get; } = new();
|
||||
}
|
||||
|
||||
@@ -11,12 +11,19 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace Jellyfin.Plugin.JRay.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Exposes scene-actor-extraction "truth" data: which actors are on screen
|
||||
/// at a given timestamp in a movie.
|
||||
/// Exposes scene-actor-extraction "truth" data: which actors are present in
|
||||
/// the scene at a given timestamp.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Presence is <b>scene-scoped</b>, not instantaneous: a window is a claim about
|
||||
/// scene membership, not a recognition event, so an actor who is off-camera
|
||||
/// during a reverse shot is still present. Windows are served exactly as stored
|
||||
/// — never merged, split or trimmed.
|
||||
/// </remarks>
|
||||
[ApiController]
|
||||
[Route("Plugins/JRay/Items/{itemId}")]
|
||||
[Authorize]
|
||||
// TRACES: JR-004, JR-005, JR-012, JR-013, JR-014 | SR-002
|
||||
public class ActorsController : ControllerBase
|
||||
{
|
||||
private readonly ITruthDataService _truthDataService;
|
||||
|
||||
@@ -19,9 +19,15 @@ namespace Jellyfin.Plugin.JRay.Controllers;
|
||||
/// by genre), and supplies the genre/series option lists the config page's
|
||||
/// rule editor needs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Percent done is <c>covered / (total - ignored)</c>: ignored items are
|
||||
/// intentionally out of scope, so excluding a genre must not drag the figure
|
||||
/// down as though it were outstanding work.
|
||||
/// </remarks>
|
||||
[ApiController]
|
||||
[Route("Plugins/JRay/Coverage")]
|
||||
[Authorize(Roles = "Administrator")]
|
||||
// TRACES: JR-018, JR-019 | PR-003
|
||||
public class CoverageController : ControllerBase
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
@@ -13,9 +13,15 @@ namespace Jellyfin.Plugin.JRay.Controllers;
|
||||
/// series, or a single item; setting a rule for a target that already has one
|
||||
/// replaces it, so a target can never be both prioritised and ignored.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rules steer work discovery only. They never reach the read endpoints or the
|
||||
/// overlay, because a rule says "don't spend compute here", not "pretend this
|
||||
/// item does not exist".
|
||||
/// </remarks>
|
||||
[ApiController]
|
||||
[Route("Plugins/JRay/Policy")]
|
||||
[Authorize(Roles = "Administrator")]
|
||||
// TRACES: JR-016, JR-014 | PR-003
|
||||
public class PolicyController : ControllerBase
|
||||
{
|
||||
private readonly IMediaPolicyStore _policyStore;
|
||||
|
||||
@@ -19,9 +19,19 @@ namespace Jellyfin.Plugin.JRay.Controllers;
|
||||
/// Lets a remote extraction worker discover which library items still need
|
||||
/// to be processed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The sample is random so that repeated polling spreads work across the
|
||||
/// backlog without the server tracking who holds what, and so two workers
|
||||
/// polling concurrently mostly do not collide.
|
||||
///
|
||||
/// Prioritise/ignore rules are applied <b>here and only here</b> (JR-017):
|
||||
/// they express "don't spend compute on this", not "pretend this does not
|
||||
/// exist", so they never reach the read endpoints or the overlay.
|
||||
/// </remarks>
|
||||
[ApiController]
|
||||
[Route("Plugins/JRay/Tasks")]
|
||||
[Authorize(Roles = "Administrator")]
|
||||
// TRACES: JR-015, JR-017 | PR-003
|
||||
public class TasksController : ControllerBase
|
||||
{
|
||||
private const int DefaultLimit = 10;
|
||||
|
||||
@@ -12,11 +12,18 @@ namespace Jellyfin.Plugin.JRay.Controllers;
|
||||
/// <summary>
|
||||
/// Accepts scene-actor-extraction "truth" data pushed directly by a remote
|
||||
/// extraction worker, for servers that cannot run the extraction pipeline
|
||||
/// locally. See SPEC.md.
|
||||
/// locally. See SPEC.md §2.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The <c>schema_version</c> check here refuses an unrecognised version rather
|
||||
/// than guessing at its shape. It is currently the only source that checks —
|
||||
/// sidecar reads do not — which JR-003 requires be fixed by moving the check
|
||||
/// into the shared read path.
|
||||
/// </remarks>
|
||||
[ApiController]
|
||||
[Route("Plugins/JRay/Items/{itemId}/Truth")]
|
||||
[Authorize(Roles = "Administrator")]
|
||||
// TRACES: JR-003, JR-009, JR-014 | SR-003
|
||||
public class TruthController : ControllerBase
|
||||
{
|
||||
private const int SupportedSchemaVersion = 1;
|
||||
|
||||
@@ -11,9 +11,14 @@ namespace Jellyfin.Plugin.JRay.Controllers;
|
||||
/// Serves static client-side assets for JRay, e.g. the pause-overlay script
|
||||
/// injected into the web client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Anonymous by necessity, not by oversight: the script tag is injected into
|
||||
/// <c>index.html</c>, which is served before a user has logged in.
|
||||
/// </remarks>
|
||||
[ApiController]
|
||||
[Route("Plugins/JRay")]
|
||||
[AllowAnonymous]
|
||||
// TRACES: JR-014, JR-020 | PR-001
|
||||
public class WebController : ControllerBase
|
||||
{
|
||||
private const string OverlayScriptResource = "Jellyfin.Plugin.JRay.Web.jray-overlay.js";
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Jellyfin.Plugin.JRay.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The object handed to JRay's transformation callback by the File
|
||||
/// Transformation plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// That plugin builds a Newtonsoft <c>JObject</c> with a single <c>contents</c>
|
||||
/// key and calls <c>JObject.ToObject(parameterType)</c> against this type.
|
||||
/// Newtonsoft binds member names case-insensitively, so <see cref="Contents"/>
|
||||
/// binds to <c>contents</c> without an attribute — and a System.Text.Json
|
||||
/// attribute would have no effect here.
|
||||
/// </remarks>
|
||||
public class TransformationPayload
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the current contents of the file being served, including
|
||||
/// any transformations applied by other plugins ahead of JRay in the
|
||||
/// chain.
|
||||
/// </summary>
|
||||
public string? Contents { get; set; }
|
||||
}
|
||||
@@ -5,8 +5,19 @@ namespace Jellyfin.Plugin.JRay.Models;
|
||||
|
||||
/// <summary>
|
||||
/// One actor entry in a <see cref="TruthFile"/>, with the time windows during
|
||||
/// which they are visible on screen.
|
||||
/// which they are present in the scene.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// "Present in the scene", not "visible on screen": a window is a claim about
|
||||
/// scene membership, so an actor who turns away or is off-camera during a
|
||||
/// reverse shot is still present. Two windows mean a genuine departure and
|
||||
/// return, not a break in detection.
|
||||
///
|
||||
/// Identity is public identifiers — never a name alone, which is ambiguous and
|
||||
/// unstable. <c>jellyfin_id</c> is preferred locally and stripped on
|
||||
/// contribution, being meaningless outside the instance that produced it.
|
||||
/// </remarks>
|
||||
// TRACES: JR-004, JR-007 | SR-001, SR-002
|
||||
public class TruthActor
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -5,8 +5,19 @@ namespace Jellyfin.Plugin.JRay.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Root object of a scene-actor-extraction "truth" file
|
||||
/// (schema_version 1, minimal verbosity). See SPEC.md.
|
||||
/// (schema_version 1, minimal verbosity). See SPEC.md §1.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// JRay <b>owns</b> this format; the extraction pipeline is its producer and the
|
||||
/// public server carries a derived envelope. Because three repos ship
|
||||
/// independently, breaking changes are batched into one coordinated
|
||||
/// <c>schema_version</c> bump rather than made piecemeal.
|
||||
///
|
||||
/// This type is still the v1 shape. JR-002 replaces it: <c>anneal_sec</c> out,
|
||||
/// an <c>extraction</c> provenance block and a <c>cut</c> block in, and
|
||||
/// <c>scenes</c> becoming objects that carry belief and identification route.
|
||||
/// </remarks>
|
||||
// TRACES: JR-001, JR-002 | SR-003
|
||||
public class TruthFile
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -14,6 +14,11 @@ namespace Jellyfin.Plugin.JRay.Services;
|
||||
/// Stores truth files pushed directly to JRay (e.g. by a remote extraction
|
||||
/// worker) under the plugin's configuration directory, keyed by item id.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Deliberately outside the media library filesystem, so a worker that cannot
|
||||
/// write beside the media file is not a second-class producer.
|
||||
/// </remarks>
|
||||
// TRACES: JR-009, JR-010 | PR-004
|
||||
public sealed class ManagedTruthStore : IManagedTruthStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace Jellyfin.Plugin.JRay.Services;
|
||||
/// reads. All access is synchronised so the work-discovery API and the
|
||||
/// config page can touch it concurrently.
|
||||
/// </summary>
|
||||
// TRACES: JR-016 | PR-003
|
||||
public sealed class MediaPolicyStore : IMediaPolicyStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace Jellyfin.Plugin.JRay.Services;
|
||||
/// hold only one action, the only conflicts possible are across scopes, and
|
||||
/// specificity resolves those.
|
||||
/// </summary>
|
||||
// TRACES: JR-016 | PR-003
|
||||
public static class PolicyResolver
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -16,6 +16,13 @@ namespace Jellyfin.Plugin.JRay.Services;
|
||||
/// item's source file, and caches the parsed result for a configurable
|
||||
/// duration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Managed truth — pushed by a worker, or fetched from a manifest server —
|
||||
/// takes precedence over a sidecar file. Storing fetched manifests through the
|
||||
/// managed store is what keeps this a two-way rule rather than a three-way one,
|
||||
/// so the read path never learns that the exchange exists.
|
||||
/// </remarks>
|
||||
// TRACES: JR-008, JR-010, JR-011 | PR-001
|
||||
public sealed class TruthDataService : ITruthDataService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
* JRay pause overlay: lists the cast of the scene the viewer paused in.
|
||||
*
|
||||
* Presence is scene-scoped. An actor who has turned away, is occluded, or is
|
||||
* off-camera during a reverse shot is still in the scene, so this must not be
|
||||
* presented as "who is visible right now" — that is a different, and weaker,
|
||||
* claim than the data makes.
|
||||
*
|
||||
* Every server-supplied string is written with textContent, never innerHTML.
|
||||
* With the manifest exchange these strings may originate from a third-party
|
||||
* server, and this is the one control that holds even if every other check is
|
||||
* bypassed.
|
||||
*
|
||||
* TRACES: JR-005, JR-020, JR-024 | SR-002, SR-004
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user