discovery-media-player 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/context/standalone.js +41 -1
- package/package.json +1 -1
- package/server/handler.js +67 -6
- package/server/shares.js +55 -4
package/context/standalone.js
CHANGED
|
@@ -204,7 +204,35 @@ function createStandaloneContext(env = process.env) {
|
|
|
204
204
|
|
|
205
205
|
// Sans expéditeur configuré, le re-partage et le code du mur d'accès sont indisponibles — et
|
|
206
206
|
// le disent. Ils ne prétendent pas avoir envoyé.
|
|
207
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Envoi d'email : capacité de l'HÔTE. Sans route configurée, le player ne prétend pas avoir
|
|
209
|
+
* envoyé — il rend `null`, et l'interface dit « envoi indisponible ».
|
|
210
|
+
*
|
|
211
|
+
* ⚠️ UN SECRET À LUI, ET C'EST UN ARBITRAGE ASSUMÉ. Le troisième, donc l'inflation est réelle.
|
|
212
|
+
* Mais `PLAYER_HOST_FETCH_SECRET` part vers l'hôte à CHAQUE fichier ouvert : il vit dans ses
|
|
213
|
+
* journaux d'accès à raison de plusieurs entrées par lecture. Lui ajouter le pouvoir de faire
|
|
214
|
+
* partir du courrier au nom de l'hôte élargirait énormément ce qu'une fuite de journaux
|
|
215
|
+
* permettrait — et une réputation d'expéditeur perdue met des semaines à revenir. Répondre à
|
|
216
|
+
* une question et agir vers le dehors ne sont pas le même pouvoir, même dans la même
|
|
217
|
+
* direction.
|
|
218
|
+
*
|
|
219
|
+
* La charge utile porte les champs STRUCTURÉS en plus du HTML : un hôte qui compose lui-même
|
|
220
|
+
* (gabarit à sa marque, aucun texte venu de l'appelant) a tout ce qu'il lui faut sans avoir à
|
|
221
|
+
* découper le nôtre.
|
|
222
|
+
*/
|
|
223
|
+
mail: {
|
|
224
|
+
async send(message) {
|
|
225
|
+
const url = String(env.PLAYER_HOST_MAIL_URL || "").trim();
|
|
226
|
+
const secret = String(env.PLAYER_HOST_MAIL_SECRET || "");
|
|
227
|
+
if (!url) return null;
|
|
228
|
+
if (!secret) {
|
|
229
|
+
try { journal.capture(new Error("PLAYER_HOST_MAIL_URL est configurée sans PLAYER_HOST_MAIL_SECRET : aucun envoi ne partira"), {}); } catch { /* ignore */ }
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const reponse = await appelHote(url, secret, message, journal);
|
|
233
|
+
return reponse && reponse.sent === true ? { sent: true } : null;
|
|
234
|
+
},
|
|
235
|
+
},
|
|
208
236
|
|
|
209
237
|
identity: {
|
|
210
238
|
/**
|
|
@@ -335,6 +363,17 @@ function createStandaloneContext(env = process.env) {
|
|
|
335
363
|
get sourceUrl() { return String(env.PLAYER_SOURCE_URL || "https://github.com/Juli1artha/discovery-media-player").trim(); },
|
|
336
364
|
get legalUrl() { return String(env.PLAYER_LEGAL_URL || "").trim(); },
|
|
337
365
|
get privacyUrl() { return String(env.PLAYER_PRIVACY_URL || "").trim(); },
|
|
366
|
+
/**
|
|
367
|
+
* ⚠️ Mention d'un lien que PERSONNE n'a envoyé — plaquette publique ouverte depuis une carte.
|
|
368
|
+
* Parler d'« expéditeur » y serait faux, et c'est la seule phrase du produit dont l'objet
|
|
369
|
+
* est d'être exacte. Le player choisit d'après le lien lui-même : ni destinataire, ni
|
|
370
|
+
* créateur.
|
|
371
|
+
*/
|
|
372
|
+
get trackingNoticeAnonymous() {
|
|
373
|
+
const perso = String(env.PLAYER_TRACKING_NOTICE_ANON || "").trim();
|
|
374
|
+
return perso || "La consultation de ce document est mesurée (pages vues, temps de lecture).";
|
|
375
|
+
},
|
|
376
|
+
|
|
338
377
|
get trackingNotice() {
|
|
339
378
|
const perso = String(env.PLAYER_TRACKING_NOTICE || "").trim();
|
|
340
379
|
return perso || "La consultation de ce document est mesurée (pages vues, temps de lecture) et transmise à son expéditeur.";
|
|
@@ -356,6 +395,7 @@ function createStandaloneContext(env = process.env) {
|
|
|
356
395
|
// Même raison que ci-dessus : « la capacité existe » et « elle est configurée »
|
|
357
396
|
// sont deux questions, et seule la seconde explique un refus.
|
|
358
397
|
hostShare: !!String(env.PLAYER_HOST_SHARE_SECRET || ""),
|
|
398
|
+
hostMail: !!String(env.PLAYER_HOST_MAIL_URL || "").trim(),
|
|
359
399
|
},
|
|
360
400
|
};
|
|
361
401
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Self-hosted document viewer: per-recipient tracked links, reading analytics, live presentation. The core knows nothing about the application hosting it — everything it borrows arrives through an injected context.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pdf-viewer",
|
package/server/handler.js
CHANGED
|
@@ -1145,7 +1145,17 @@ async function relayerFichier(res, r, disposition) {
|
|
|
1145
1145
|
res.end(buf);
|
|
1146
1146
|
}
|
|
1147
1147
|
|
|
1148
|
-
|
|
1148
|
+
// ⚠️ UNE MENTION DONT L'OBJET EST D'ÊTRE EXACTE NE DOIT PAS INVENTER UN EXPÉDITEUR.
|
|
1149
|
+
//
|
|
1150
|
+
// Le texte par défaut disait « transmise à son expéditeur ». Vrai pour un lien nominatif, faux
|
|
1151
|
+
// pour un lien que PERSONNE n'a envoyé — la plaquette publique d'un programme, ouverte depuis une
|
|
1152
|
+
// carte par un visiteur qui n'a reçu aucun message. Le défaut est antérieur au mode serveur à
|
|
1153
|
+
// serveur ; c'est lui qui l'a rendu visible, puisqu'il crée précisément des liens sans
|
|
1154
|
+
// destinataire NI créateur.
|
|
1155
|
+
//
|
|
1156
|
+
// La distinction ne demande aucune donnée nouvelle : c'est déjà la clé d'idempotence de ces
|
|
1157
|
+
// liens-là. Signalé par le second hôte en regardant son propre écran — ce qu'aucun test ne fait.
|
|
1158
|
+
function legalFooter({ tracked, sansExpediteur }) {
|
|
1149
1159
|
const L = PLAYER.legal;
|
|
1150
1160
|
const liens = [
|
|
1151
1161
|
L.legalUrl ? `<a href="${esc(L.legalUrl)}" target=_blank rel=noreferrer>Mentions légales</a>` : "",
|
|
@@ -1153,7 +1163,10 @@ function legalFooter({ tracked }) {
|
|
|
1153
1163
|
// Obligation AGPL : l'accès au source se propose à qui UTILISE le logiciel, pas seulement à qui le distribue.
|
|
1154
1164
|
L.sourceUrl ? `<a href="${esc(L.sourceUrl)}" target=_blank rel=noreferrer>Code source</a>` : "",
|
|
1155
1165
|
].filter(Boolean).join("<span class=lgl-sep>·</span>");
|
|
1156
|
-
|
|
1166
|
+
// Un contexte qui ne fournit pas le second texte retombe sur le premier : rien ne casse chez un
|
|
1167
|
+
// hôte qui n'a pas encore de liens sans expéditeur.
|
|
1168
|
+
const texte = sansExpediteur ? (L.trackingNoticeAnonymous || L.trackingNotice) : L.trackingNotice;
|
|
1169
|
+
const mesure = tracked ? `<span class=lgl-note>${esc(texte)}</span>` : "";
|
|
1157
1170
|
if (!liens && !mesure) return "";
|
|
1158
1171
|
return `<div class=lgl>${mesure}${liens ? `<span class=lgl-links>${liens}</span>` : ""}</div>`;
|
|
1159
1172
|
}
|
|
@@ -1363,6 +1376,26 @@ ${gcid ? `<script nonce="${nonce}" src="https://accounts.google.com/gsi/client"
|
|
|
1363
1376
|
</body></html>`;
|
|
1364
1377
|
}
|
|
1365
1378
|
|
|
1379
|
+
/**
|
|
1380
|
+
* ⚠️ LE VISITEUR EST VENU SOUS UNE MARQUE ; C'EST CELLE-LÀ QU'IL DOIT LIRE.
|
|
1381
|
+
*
|
|
1382
|
+
* Le titre ajoutait le nom de l'INSTANCE — celui de la société qui exploite l'outil. Vrai tant
|
|
1383
|
+
* qu'une instance ne sert qu'un public ; faux dès qu'elle en sert deux, et le visiteur d'une
|
|
1384
|
+
* marque cliente voit alors le nom d'une société qui ne le concerne pas.
|
|
1385
|
+
*
|
|
1386
|
+
* Aucune configuration nouvelle : la marque du lien est DÉJÀ résolue (`brandForShare`, la même
|
|
1387
|
+
* qui habille le loader) et posée sur `share.brand_name` avant qu'on arrive ici. Le titre ne la
|
|
1388
|
+
* consultait simplement pas.
|
|
1389
|
+
*
|
|
1390
|
+
* Le « propulsé par » reste celui de l'instance, lui, et c'est délibéré : dire qui opère l'outil
|
|
1391
|
+
* est une information honnête, pas une trahison de marque.
|
|
1392
|
+
*/
|
|
1393
|
+
function titreOnglet(share) {
|
|
1394
|
+
const base = share.doc_title || share.file_name || "Document";
|
|
1395
|
+
const marque = String((share && share.brand_name) || "").trim();
|
|
1396
|
+
return marque ? `${base} — ${marque}` : PLAYER.branding.title(base);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1366
1399
|
function viewerHtml(share, nonce, logoUrl, pitch) {
|
|
1367
1400
|
const title = esc(share.doc_title || share.file_name || "Document");
|
|
1368
1401
|
// Aperçu interne : slug vide (= pas de tracking, pas de bouton de re-partage) + stream depuis le bucket public.
|
|
@@ -1394,7 +1427,7 @@ function viewerHtml(share, nonce, logoUrl, pitch) {
|
|
|
1394
1427
|
<meta name=robots content="noindex,nofollow">
|
|
1395
1428
|
<link rel=icon href="data:,">
|
|
1396
1429
|
<link rel=preconnect href="https://cdnjs.cloudflare.com" crossorigin>
|
|
1397
|
-
<title>${esc(
|
|
1430
|
+
<title>${esc(titreOnglet(share))}</title>
|
|
1398
1431
|
<style>
|
|
1399
1432
|
:root{--bg:#33312e;--bar:#26241f}
|
|
1400
1433
|
*{box-sizing:border-box}
|
|
@@ -1582,7 +1615,7 @@ ${LEGAL_CSS}
|
|
|
1582
1615
|
${botOn ? botMarkup(share, pitch) : ""}
|
|
1583
1616
|
</div>
|
|
1584
1617
|
${PLAYER.branding.poweredBy ? `<div class=brand>Propulsé par ${esc(PLAYER.branding.poweredBy)}</div>` : ""}
|
|
1585
|
-
${legalFooter({ tracked: !preview && !!share.slug })}
|
|
1618
|
+
${legalFooter({ tracked: !preview && !!share.slug, sansExpediteur: !share.recipient_email && !share.created_by })}
|
|
1586
1619
|
${brandLogo || !brandIntroRuntime ? "" : `<script nonce="${nonce}">(${brandIntroRuntime.toString()})();</script>`}
|
|
1587
1620
|
<script nonce="${nonce}">${PLAYER_BROWSER_JS}</script>
|
|
1588
1621
|
<script nonce="${nonce}" src="${PDFJS}/pdf.min.js"></script>
|
|
@@ -2586,15 +2619,42 @@ async function handler(req, res) {
|
|
|
2586
2619
|
try { out = await createReshare(body.slug || slug, { email: mail, name: body.name }); } catch { /* parent introuvable */ }
|
|
2587
2620
|
if (!out) return j(404, { ok: false });
|
|
2588
2621
|
let sent = false;
|
|
2622
|
+
let refusEnvoi = null;
|
|
2589
2623
|
if (body.send) {
|
|
2590
2624
|
try {
|
|
2591
2625
|
const parent = await getShareBySlug(body.slug || slug);
|
|
2626
|
+
// ⚠️ ON N'ENVOIE DE COURRIER QUE POUR UN LIEN QUI A UN DESTINATAIRE.
|
|
2627
|
+
//
|
|
2628
|
+
// Le lecteur d'un lien ANONYME est un visiteur quelconque : lui laisser demander un
|
|
2629
|
+
// envoi ferait des serveurs de l'hôte un relais de courrier non sollicité, avec SON
|
|
2630
|
+
// domaine dans l'en-tête. Ce qui coûte cher n'est pas le message parti, c'est la
|
|
2631
|
+
// réputation d'expéditeur : elle met des semaines à revenir, et pendant ce temps
|
|
2632
|
+
// AUCUN de ses emails n'arrive — factures, relances, notifications d'équipe
|
|
2633
|
+
// comprises. Une commodité sur une page publique mettrait en jeu tout son courrier
|
|
2634
|
+
// transactionnel.
|
|
2635
|
+
//
|
|
2636
|
+
// Un lien nominatif, lui, a été créé par quelqu'un qui s'est authentifié et qui
|
|
2637
|
+
// engage sa responsabilité. `recipient_email` est nul sur exactement les liens sans
|
|
2638
|
+
// membre derrière — c'est déjà la clé d'idempotence du chemin serveur à serveur.
|
|
2639
|
+
//
|
|
2640
|
+
// ⚠️ La garde est ICI, sur le chemin qui agit, et pas chez l'hôte à l'arrivée. Un
|
|
2641
|
+
// filtre à l'arrivée dépend d'une liste à jour ; un chemin qui ne sait pas formuler
|
|
2642
|
+
// la demande ne la formulera jamais par accident. Même raison que les trois verrous
|
|
2643
|
+
// de `docshare.create`. Demandé par le second hôte, qui l'a réclamée CHEZ NOUS alors
|
|
2644
|
+
// qu'il aurait pu la poser chez lui.
|
|
2645
|
+
if (!parent || !parent.recipient_email) {
|
|
2646
|
+
refusEnvoi = "no-recipient";
|
|
2647
|
+
throw new Error("envoi réservé aux liens nominatifs");
|
|
2648
|
+
}
|
|
2592
2649
|
const origin = `https://${req.headers.host}`;
|
|
2593
2650
|
const r = await sendReshareEmail({ parent, childSlug: out.slug, origin, toEmail: mail, toName: body.name });
|
|
2594
2651
|
sent = !!(r && r.sent);
|
|
2595
2652
|
} catch { /* best-effort : le lien existe quand même */ }
|
|
2596
2653
|
}
|
|
2597
|
-
|
|
2654
|
+
// Le refus se DIT : « rien n'est parti » et « l'envoi n'était pas permis » ne se
|
|
2655
|
+
// ressemblent pas, et une interface qui les confond propose un bouton qui ne marchera
|
|
2656
|
+
// jamais.
|
|
2657
|
+
return j(200, { ok: true, slug: out.slug, sent, ...(refusEnvoi ? { sendRefused: refusEnvoi } : {}) });
|
|
2598
2658
|
}
|
|
2599
2659
|
const ua0 = req.headers["user-agent"];
|
|
2600
2660
|
const ip0 = String(req.headers["x-forwarded-for"] || "").split(",")[0].trim() || req.socket?.remoteAddress || "";
|
|
@@ -2656,7 +2716,7 @@ async function handler(req, res) {
|
|
|
2656
2716
|
// muette sur les URL.
|
|
2657
2717
|
capabilities: [
|
|
2658
2718
|
"docshare", "presentations", "embed-denied", "host-fetch", "brand-reference", "host-auth",
|
|
2659
|
-
"host-share",
|
|
2719
|
+
"host-share", "host-mail",
|
|
2660
2720
|
],
|
|
2661
2721
|
// ⚠️ POUR QUELLES ORIGINES cette instance accepte d'être encadrée. Un booléen ne
|
|
2662
2722
|
// suffisait pas : un hôte a besoin de voir que SON domaine manque, pas seulement que
|
|
@@ -2673,6 +2733,7 @@ async function handler(req, res) {
|
|
|
2673
2733
|
// possible. Un hôte qui oublie le secret reçoit un 401 qui ressemble à un droit
|
|
2674
2734
|
// manquant ; ce booléen le lui dit sans qu'il ait à essayer.
|
|
2675
2735
|
hostShare: !!(PLAYER.config && PLAYER.config.hostShare),
|
|
2736
|
+
hostMail: !!(PLAYER.config && PLAYER.config.hostMail),
|
|
2676
2737
|
// Greffons de l'hôte : présents ou coupés (PLAYER_PLUGINS_OFF). Booléens uniquement.
|
|
2677
2738
|
plugins: {
|
|
2678
2739
|
bot: !!p.bot, visitors: !!p.visitors, brandIntro: !!p.brandIntro,
|
package/server/shares.js
CHANGED
|
@@ -43,10 +43,43 @@ async function createReshare(parentSlug, { email, name }) {
|
|
|
43
43
|
const parent = await getShareBySlug(parentSlug);
|
|
44
44
|
if (!parent) throw Object.assign(new Error("lien introuvable"), { statusCode: 404 });
|
|
45
45
|
const slug = newSlug();
|
|
46
|
+
|
|
47
|
+
// ⚠️ ON HÉRITE DE TOUT, ON N'ÉNUMÈRE QUE LES EXCEPTIONS — ET LE SENS DE CETTE INVERSION EST LA
|
|
48
|
+
// CORRECTION ELLE-MÊME.
|
|
49
|
+
//
|
|
50
|
+
// Cette ligne énumérait les colonnes à recopier. Une énumération se périme à chaque colonne
|
|
51
|
+
// ajoutée, en silence, ET DU MAUVAIS CÔTÉ : la nouveauté est oubliée. Les colonnes de cette
|
|
52
|
+
// table sont `not null default`, donc l'oubli ne produisait pas un trou — il produisait une
|
|
53
|
+
// VALEUR PAR DÉFAUT, c'est-à-dire la plus permissive :
|
|
54
|
+
//
|
|
55
|
+
// • `require_auth` (défaut `false`) — un document derrière le mur d'accès, une fois
|
|
56
|
+
// re-partagé, s'ouvrait SANS mur. Un destinataire pouvait donc lever la protection en se
|
|
57
|
+
// transmettant le document à lui-même. C'est le plus grave, et il n'était pas dans le
|
|
58
|
+
// rapport qui a mené ici.
|
|
59
|
+
// • `allow_download` (défaut `true`) — le bouton Télécharger revenait sur un document où il
|
|
60
|
+
// avait été refusé.
|
|
61
|
+
// • `brand_key` — la marque se perdait à l'endroit exact où le document commence à circuler :
|
|
62
|
+
// le lecteur d'un document VALONEUF transmettait un lien qui s'ouvre sous une autre marque.
|
|
63
|
+
//
|
|
64
|
+
// Aucun de ces trois n'était visible : le lien fonctionne, il est simplement plus permissif que
|
|
65
|
+
// son parent. Signalé par le second hôte, qui a vu la marque — celle qui SE VOIT — et a supposé
|
|
66
|
+
// que le reste suivait. Le reste suivait.
|
|
67
|
+
//
|
|
68
|
+
// Sens de l'inversion : une colonne ajoutée demain sera héritée sans que personne y pense. Si
|
|
69
|
+
// c'est une restriction, elle se propage ; si elle ne doit pas l'être, il faudra l'écrire ici,
|
|
70
|
+
// et ce sera une décision au lieu d'un oubli.
|
|
71
|
+
//
|
|
72
|
+
// `created_at` est retiré : la base le pose. `is_test` est hérité — un lien de répétition dont
|
|
73
|
+
// un enfant compterait dans les vraies statistiques les fausserait.
|
|
74
|
+
const { created_at: _cree, ...herite } = parent;
|
|
46
75
|
const row = {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
76
|
+
...herite,
|
|
77
|
+
slug,
|
|
78
|
+
recipient_email: low(email) || null,
|
|
79
|
+
recipient_name: (name || "").trim() || null,
|
|
80
|
+
created_by: parent.recipient_email || parent.created_by || null,
|
|
81
|
+
parent_slug: parent.slug,
|
|
82
|
+
revoked: false,
|
|
50
83
|
};
|
|
51
84
|
await PLAYER.db.request("commercial_doc_shares", { method: "POST", headers: { Prefer: "return=minimal" }, body: [row] });
|
|
52
85
|
return { slug, docTitle: parent.doc_title };
|
|
@@ -225,7 +258,25 @@ async function sendReshareEmail({ parent, childSlug, origin, toEmail, toName })
|
|
|
225
258
|
<p style="font-size:13px;color:#777">Vous pouvez répondre directement à cet email pour échanger avec ${e(forwarder)}.</p>
|
|
226
259
|
<p style="font-size:11px;color:#999;margin-top:22px">Propulsé par 3D Discovery — visualisation 3D & visites immersives.</p>
|
|
227
260
|
</div>`;
|
|
228
|
-
|
|
261
|
+
// ⚠️ LES CHAMPS STRUCTURÉS ACCOMPAGNENT LE HTML, ILS NE LE REMPLACENT PAS.
|
|
262
|
+
//
|
|
263
|
+
// Un hôte qui envoie avec sa propre identité voudra composer avec son gabarit — et surtout
|
|
264
|
+
// n'y laisser entrer AUCUN texte fourni par l'appelant. Notre HTML, lui, insère `toName` : un
|
|
265
|
+
// champ libre, échappé mais choisi par qui détient le lien, dans un message signé par l'hôte.
|
|
266
|
+
// Lui donner les éléments séparés, c'est lui permettre de n'en reprendre aucun.
|
|
267
|
+
//
|
|
268
|
+
// Le HTML reste là pour un hôte qui ne veut pas composer : rien ne casse pour l'existant.
|
|
269
|
+
return PLAYER.mail.send({
|
|
270
|
+
to: toEmail,
|
|
271
|
+
subject: `${forwarder} vous recommande : ${title}`,
|
|
272
|
+
html,
|
|
273
|
+
replyTo: parent.recipient_email || undefined,
|
|
274
|
+
kind: "reshare",
|
|
275
|
+
doc: { title, url },
|
|
276
|
+
from: { name: parent.recipient_name || null, email: parent.recipient_email || null },
|
|
277
|
+
// Fourni par l'appelant, donc à traiter comme tel : un hôte prudent l'ignore.
|
|
278
|
+
untrusted: { toName: (toName || "").trim() || null },
|
|
279
|
+
});
|
|
229
280
|
}
|
|
230
281
|
|
|
231
282
|
async function revokeShare(slug) {
|