@ecency/render-helper 2.5.11 → 2.5.13

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.
@@ -205,6 +205,96 @@ var ALLOWED_ATTRIBUTES = {
205
205
  "del": [],
206
206
  "ins": []
207
207
  };
208
+
209
+ // src/consts/embed-hosts.const.ts
210
+ var ALLOWED_EMBED_HOSTS = /* @__PURE__ */ new Set([
211
+ // YouTube
212
+ "www.youtube.com",
213
+ "youtube.com",
214
+ "www.youtube-nocookie.com",
215
+ "youtube-nocookie.com",
216
+ // Vimeo
217
+ "player.vimeo.com",
218
+ // Twitch
219
+ "player.twitch.tv",
220
+ // DTube
221
+ "emb.d.tube",
222
+ // 3Speak (video + audio)
223
+ "play.3speak.tv",
224
+ "3speak.tv",
225
+ "audio.3speak.tv",
226
+ // Loom
227
+ "www.loom.com",
228
+ // Spotify
229
+ "open.spotify.com",
230
+ // SoundCloud
231
+ "w.soundcloud.com",
232
+ // BitChute
233
+ "www.bitchute.com",
234
+ "bitchute.com",
235
+ // Rumble
236
+ "www.rumble.com",
237
+ "rumble.com",
238
+ // Brighteon
239
+ "www.brighteon.com",
240
+ "brighteon.com",
241
+ // VIMM
242
+ "www.vimm.tv",
243
+ // BrandNewTube
244
+ "brandnewtube.com",
245
+ // LBRY / Odysee
246
+ "lbry.tv",
247
+ "odysee.com",
248
+ // Skatehive / Skatehype
249
+ "ipfs.skatehive.app",
250
+ "www.skatehype.com",
251
+ "skatehype.com",
252
+ // archive.org
253
+ "archive.org",
254
+ // Truvvl
255
+ "embed.truvvl.com",
256
+ // Aureal
257
+ "aureal-embed.web.app",
258
+ "www.aureal-embed.web.app"
259
+ // Dapplr (player.*.dapplr.in / *.dapplr.in) — host suffix, handled below
260
+ ]);
261
+ var ALLOWED_EMBED_HOST_SUFFIXES = [".dapplr.in"];
262
+ var EMBED_HOST_PATH_PATTERNS = {
263
+ "www.youtube.com": /^\/embed\//,
264
+ "youtube.com": /^\/embed\//,
265
+ "www.youtube-nocookie.com": /^\/embed\//,
266
+ "youtube-nocookie.com": /^\/embed\//,
267
+ "player.vimeo.com": /^\/video\//,
268
+ "player.twitch.tv": /^\/$/,
269
+ // channel/video carried in the query string
270
+ "emb.d.tube": /^\/$/,
271
+ // dtube carries the ref in the #! fragment
272
+ "play.3speak.tv": /^\/(watch|embed)/,
273
+ "open.spotify.com": /^\/embed\//,
274
+ "www.loom.com": /^\/embed\//,
275
+ "www.bitchute.com": /^\/embed\//,
276
+ "bitchute.com": /^\/embed\//,
277
+ "www.rumble.com": /^\/embed\//,
278
+ "rumble.com": /^\/embed\//,
279
+ "www.brighteon.com": /^\/embed\//,
280
+ "brighteon.com": /^\/embed\//
281
+ };
282
+ function isAllowedEmbedSrc(value) {
283
+ if (!value) return false;
284
+ let url;
285
+ try {
286
+ url = new URL(value.trim());
287
+ } catch {
288
+ return false;
289
+ }
290
+ if (url.protocol !== "https:") return false;
291
+ const host = url.hostname.toLowerCase();
292
+ const hostAllowed = ALLOWED_EMBED_HOSTS.has(host) || ALLOWED_EMBED_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix));
293
+ if (!hostAllowed) return false;
294
+ const pathPattern = EMBED_HOST_PATH_PATTERNS[host];
295
+ if (pathPattern && !pathPattern.test(url.pathname)) return false;
296
+ return true;
297
+ }
208
298
  function createParser() {
209
299
  return new xmldom.DOMParser({
210
300
  onError(level, msg) {
@@ -600,6 +690,14 @@ function buildPictureSources(rawUrl) {
600
690
  }
601
691
 
602
692
  // src/methods/sanitize-html.method.ts
693
+ var EMBED_SRC_DATA_ATTRS = /* @__PURE__ */ new Set(["data-embed-src", "data-video-href"]);
694
+ var isSafeNavValue = (value) => {
695
+ const trimmed = value.trim().replace(/[\t\n\r\f\v\0]/g, "").toLowerCase();
696
+ if (!trimmed) return false;
697
+ const isSafeScheme = /^(https?|mailto|hive|tel|web\+[a-z0-9.+-]+):/i.test(trimmed);
698
+ const isRelative = /^(\/\/|\/[^/]?|#|\?|[a-z0-9._\-]+(\/|$))/i.test(trimmed);
699
+ return isSafeScheme || isRelative;
700
+ };
603
701
  var decodeEntities = (input) => input.replace(/&#(\d+);?/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
604
702
  var isProxyPSrcset = (srcset) => {
605
703
  const base = trimTrailingSlash(getProxyBase());
@@ -627,6 +725,8 @@ function sanitizeHtml(html) {
627
725
  if (tag === "video" && ["src", "poster"].includes(name) && !/^https?:\/\//.test(decodedLower)) return "";
628
726
  if (tag === "img" && ["dynsrc", "lowsrc"].includes(name)) return "";
629
727
  if (tag === "span" && name === "class" && decoded.toLowerCase().trim() === "wr") return "";
728
+ if (EMBED_SRC_DATA_ATTRS.has(name) && !isAllowedEmbedSrc(decoded)) return "";
729
+ if (name === "data-href" && !isSafeNavValue(decoded)) return "";
630
730
  if (name === "id") {
631
731
  if (!ID_WHITELIST.test(decoded)) return "";
632
732
  }
@@ -1142,14 +1242,15 @@ function a(el, forApp, parentDomain = "ecency.com", seoContext, renderOptions) {
1142
1242
  el.setAttribute("data-start-time", startTime);
1143
1243
  }
1144
1244
  if (renderOptions?.embedVideosDirectly) {
1245
+ const directSrc = `https://www.youtube.com/embed/${vid}`;
1145
1246
  const wrapper = el.ownerDocument.createElement("span");
1146
1247
  wrapper.setAttribute("class", "er-youtube-frame");
1147
1248
  wrapper.setAttribute("style", "display:block");
1148
1249
  const iframe2 = el.ownerDocument.createElement("iframe");
1149
1250
  iframe2.setAttribute("class", "youtube-player");
1150
- iframe2.setAttribute("src", embedSrc);
1251
+ iframe2.setAttribute("src", directSrc);
1151
1252
  iframe2.setAttribute("title", "YouTube video");
1152
- iframe2.setAttribute("allow", "accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; web-share");
1253
+ iframe2.setAttribute("allow", "accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share");
1153
1254
  iframe2.setAttribute("allowfullscreen", "");
1154
1255
  wrapper.appendChild(iframe2);
1155
1256
  el.appendChild(wrapper);
@@ -1286,12 +1387,13 @@ function a(el, forApp, parentDomain = "ecency.com", seoContext, renderOptions) {
1286
1387
  el.textContent = "";
1287
1388
  }
1288
1389
  if (renderOptions?.embedVideosDirectly) {
1390
+ const directSrc = `${videoHref}&autoplay=false`;
1289
1391
  const wrapper = el.ownerDocument.createElement("span");
1290
1392
  wrapper.setAttribute("class", "er-speak-frame");
1291
1393
  wrapper.setAttribute("style", "display:block");
1292
1394
  const iframe2 = el.ownerDocument.createElement("iframe");
1293
1395
  iframe2.setAttribute("class", "speak-iframe");
1294
- iframe2.setAttribute("src", videoHref);
1396
+ iframe2.setAttribute("src", directSrc);
1295
1397
  iframe2.setAttribute("title", "3Speak video");
1296
1398
  iframe2.setAttribute("allow", "accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share");
1297
1399
  iframe2.setAttribute("allowfullscreen", "");
@@ -1404,7 +1506,7 @@ function a(el, forApp, parentDomain = "ecency.com", seoContext, renderOptions) {
1404
1506
  }
1405
1507
 
1406
1508
  // src/methods/iframe.method.ts
1407
- function iframe(el, parentDomain = "ecency.com", forApp = false) {
1509
+ function iframe(el, parentDomain = "ecency.com", forApp = false, renderOptions) {
1408
1510
  if (!el || !el.parentNode) {
1409
1511
  return;
1410
1512
  }
@@ -1447,7 +1549,12 @@ function iframe(el, parentDomain = "ecency.com", forApp = false) {
1447
1549
  normalizedSrc = `${normalizedSrc}&mode=iframe`;
1448
1550
  }
1449
1551
  const hasAutoplay = /[?&]autoplay=/.test(normalizedSrc);
1450
- let s = hasAutoplay ? normalizedSrc : `${normalizedSrc}&autoplay=true`;
1552
+ let s;
1553
+ if (renderOptions?.embedVideosDirectly) {
1554
+ s = hasAutoplay ? normalizedSrc.replace(/([?&]autoplay=)[^&]*/i, "$1false") : `${normalizedSrc}&autoplay=false`;
1555
+ } else {
1556
+ s = hasAutoplay ? normalizedSrc : `${normalizedSrc}&autoplay=true`;
1557
+ }
1451
1558
  if (forApp && !/[?&]layout=/.test(s)) {
1452
1559
  s = `${s}&layout=mobile`;
1453
1560
  }
@@ -1761,7 +1868,7 @@ function traverse(node, forApp, depth = 0, state = { firstImageFound: false }, p
1761
1868
  a(child, forApp, parentDomain, seoContext, renderOptions);
1762
1869
  }
1763
1870
  if (child.nodeName.toLowerCase() === "iframe") {
1764
- iframe(child, parentDomain, forApp);
1871
+ iframe(child, parentDomain, forApp, renderOptions);
1765
1872
  }
1766
1873
  if (child.nodeName === "#text") {
1767
1874
  text(child, forApp);
@@ -2239,6 +2346,7 @@ exports.buildSrcSet = buildSrcSet;
2239
2346
  exports.buildSrcSetForFormat = buildSrcSetForFormat;
2240
2347
  exports.catchPostImage = catchPostImage;
2241
2348
  exports.getEntryImageRawUrl = getEntryImageRawUrl;
2349
+ exports.isAllowedEmbedSrc = isAllowedEmbedSrc;
2242
2350
  exports.isPictureEligibleRawUrl = isPictureEligibleRawUrl;
2243
2351
  exports.isValidPermlink = isValidPermlink;
2244
2352
  exports.postBodySummary = getPostBodySummary;