Преглед изворни кода

feat: support for TrustedTypes API

Sv443 пре 8 месеци
родитељ
комит
1bfaf88ecb
8 измењених фајлова са 68 додато и 4 уклоњено
  1. 4 0
      assets/require.json
  2. 1 1
      contributing.md
  3. 2 0
      package.json
  4. 23 0
      pnpm-lock.yaml
  5. 10 0
      src/types.ts
  6. 19 3
      src/utils/dom.ts
  7. 5 0
      src/utils/misc.ts
  8. 4 0
      tsconfig.json

+ 4 - 0
assets/require.json

@@ -13,5 +13,9 @@
     "pkgName": "compare-versions",
     "path": "lib/umd/index.js",
     "global": "compareVersions"
+  },
+  {
+    "pkgName": "dompurify",
+    "global": "DOMPurify"
   }
 ]

+ 1 - 1
contributing.md

@@ -1441,8 +1441,8 @@ Functions marked with 🔒 need to be passed a per-session and per-plugin authen
 >   small: true,
 >   verticalAlign: "top", // if the content's height changes, it's better to anchor it to the top or bottom
 >   // add elements to the header, body and footer here, in one of these ways:
+>   // - setInnerHtmlTrusted(foo, "..."); // (see contributing guide)
 >   // - foo.appendChild(document.createElement("..."));
->   // - foo.innerHTML = "..."
 >   // - ReactDOM.render(<MyComponent />, foo);
 >   // - etc.
 >   renderHeader: () => {

+ 2 - 0
package.json

@@ -66,6 +66,7 @@
   "dependencies": {
     "@sv443-network/userutils": "^7.1.0",
     "compare-versions": "^6.1.0",
+    "dompurify": "^3.1.6",
     "marked": "^12.0.2",
     "tslib": "^2.6.3"
   },
@@ -83,6 +84,7 @@
     "@storybook/html": "^8.1.10",
     "@storybook/html-vite": "^8.1.10",
     "@storybook/test": "^8.1.10",
+    "@types/dompurify": "^3.0.5",
     "@types/express": "^4.17.21",
     "@types/greasemonkey": "^4.0.7",
     "@types/node": "^20.14.8",

+ 23 - 0
pnpm-lock.yaml

@@ -14,6 +14,9 @@ importers:
       compare-versions:
         specifier: ^6.1.0
         version: 6.1.0
+      dompurify:
+        specifier: ^3.1.6
+        version: 3.1.6
       marked:
         specifier: ^12.0.2
         version: 12.0.2
@@ -60,6 +63,9 @@ importers:
       '@storybook/test':
         specifier: ^8.1.10
         version: 8.1.10
+      '@types/dompurify':
+        specifier: ^3.0.5
+        version: 3.0.5
       '@types/express':
         specifier: ^4.17.21
         version: 4.17.21
@@ -1733,6 +1739,9 @@ packages:
   '@types/[email protected]':
     resolution: {integrity: sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==}
 
+  '@types/[email protected]':
+    resolution: {integrity: sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==}
+
   '@types/[email protected]':
     resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==}
 
@@ -1805,6 +1814,9 @@ packages:
   '@types/[email protected]':
     resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==}
 
+  '@types/[email protected]':
+    resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
+
   '@types/[email protected]':
     resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==}
 
@@ -2460,6 +2472,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
 
+  [email protected]:
+    resolution: {integrity: sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==}
+
   [email protected]:
     resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==}
     engines: {node: '>=12'}
@@ -6656,6 +6671,10 @@ snapshots:
 
   '@types/[email protected]': {}
 
+  '@types/[email protected]':
+    dependencies:
+      '@types/trusted-types': 2.0.7
+
   '@types/[email protected]': {}
 
   '@types/[email protected]': {}
@@ -6730,6 +6749,8 @@ snapshots:
       '@types/node': 20.14.8
       '@types/send': 0.17.4
 
+  '@types/[email protected]': {}
+
   '@types/[email protected]': {}
 
   '@types/[email protected]': {}
@@ -7459,6 +7480,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]: {}

+ 10 - 0
src/types.ts

@@ -127,11 +127,21 @@ export type BytmObject =
     compareVersions: typeof import("compare-versions");
   };
 
+export type TTPolicy = {
+  createHTML: (to_sanitize: string) => string;
+};
+
 declare global {
   interface Window {
     // to see the expanded type, install the VS Code extension "MylesMurphy.prettify-ts" and hover over the property below
     // alternatively navigate with ctrl+click to find the types
     BYTM: BytmObject;
+    // polyfill for the new Trusted Types API
+    trustedTypes: {
+      createPolicy(name: string, policy: {
+        createHTML: (to_sanitize: string) => string;
+      }): TTPolicy;
+    };
   }
 }
 

+ 19 - 3
src/utils/dom.ts

@@ -1,14 +1,15 @@
 import { addGlobalStyle, getUnsafeWindow, randomId, type Stringifiable } from "@sv443-network/userutils";
-import { error, fetchCss, getDomain, t } from "./index.js";
+import { error, fetchCss, getBrowserType, getDomain, t } from "./index.js";
 import { addSelectorListener } from "../observers.js";
-import type { ResourceKey } from "../types.js";
+import type { ResourceKey, TTPolicy } from "../types.js";
 import { siteEvents } from "../siteEvents.js";
+import DOMPurify from "dompurify";
 
 /** Whether the DOM has finished loading and elements can be added or modified */
 export let domLoaded = false;
 document.addEventListener("DOMContentLoaded", () => domLoaded = true);
 
-//#region video time, volume
+//#region vid time & vol.
 
 /** Returns the video element selector string based on the current domain */
 export const getVideoSelector = () => getDomain() === "ytm" ? "ytmusic-player video" : "#player-container ytd-player video";
@@ -231,3 +232,18 @@ export function copyToClipboard(text: Stringifiable) {
     alert(t("copy_to_clipboard_error", String(text)));
   }
 }
+
+let ttPolicy: TTPolicy | undefined;
+
+/** On Firefox, sets innerHTML directly, on Chromium, uses a [TrustedTypes](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API) policy to set the HTML */
+export function setInnerHtmlTrusted(element: HTMLElement, html: string) {
+  if(!ttPolicy && window?.trustedTypes?.createPolicy)
+    ttPolicy = window.trustedTypes?.createPolicy("default", {
+      createHTML: (unsafeHtml) =>
+        DOMPurify.sanitize(unsafeHtml, {
+          RETURN_TRUSTED_TYPE: getBrowserType() === "chromium",
+        }),
+    });
+
+  element.innerHTML = ttPolicy ? ttPolicy!.createHTML(html) : html;
+}

+ 5 - 0
src/utils/misc.ts

@@ -184,6 +184,11 @@ export async function tryToDecompressAndParse<TData = Record<string, unknown>>(i
   return parsed;
 }
 
+/** Returns the browser type as either "firefox" or "chromium" */
+export function getBrowserType(): "firefox" | "chromium" {
+  return navigator.userAgent.match(/firefox/i) ? "firefox" : "chromium";
+}
+
 //#region resources
 
 /**

+ 4 - 0
tsconfig.json

@@ -29,6 +29,10 @@
     "esm": true,
     "preferTsExts": true,
   },
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.d.ts",
+  ],
   "exclude": [
     "**/*.js",
     "dist/**",