]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(runtime-dom): Trusted Types compatibility (#10844)
authorHaoqun Jiang <haoqunjiang@gmail.com>
Fri, 2 Aug 2024 04:46:12 +0000 (12:46 +0800)
committerGitHub <noreply@github.com>
Fri, 2 Aug 2024 04:46:12 +0000 (12:46 +0800)
package.json
packages/runtime-core/src/compat/global.ts
packages/runtime-dom/package.json
packages/runtime-dom/src/index.ts
packages/runtime-dom/src/nodeOps.ts
packages/vue/__tests__/e2e/trusted-types.html [new file with mode: 0644]
packages/vue/__tests__/e2e/trusted-types.spec.ts [new file with mode: 0644]
pnpm-lock.yaml

index 1e6b7d7691a7afb3d76388a44470e358884233f2..539a304c75ca96b018329f06440ae9c4f6ac3444 100644 (file)
@@ -70,6 +70,7 @@
     "@types/hash-sum": "^1.0.2",
     "@types/node": "^20.14.13",
     "@types/semver": "^7.5.8",
+    "@types/serve-handler": "^6.1.4",
     "@vitest/coverage-istanbul": "^1.6.0",
     "@vue/consolidate": "1.0.0",
     "conventional-changelog-cli": "^5.0.0",
     "rollup-plugin-polyfill-node": "^0.13.0",
     "semver": "^7.6.3",
     "serve": "^14.2.3",
+    "serve-handler": "^6.1.5",
     "simple-git-hooks": "^2.11.1",
     "todomvc-app-css": "^2.4.3",
     "tslib": "^2.6.3",
index f64e7adf210d2be27463fe15da4d2a097c878c88..c21aca58fc58f6c9e89efcfd5dc6d3b53f147ba3 100644 (file)
@@ -548,7 +548,7 @@ function installCompatMount(
       }
 
       // clear content before mounting
-      container.innerHTML = ''
+      container.textContent = ''
 
       // TODO hydration
       render(vnode, container, namespace)
index cf09ba6e0f72542e75620622c255faa949bbaa0f..5245a12be0922648ad9e86987b855c80e4d25e2a 100644 (file)
@@ -53,5 +53,8 @@
     "@vue/runtime-core": "workspace:*",
     "@vue/reactivity": "workspace:*",
     "csstype": "^3.1.3"
+  },
+  "devDependencies": {
+    "@types/trusted-types": "^2.0.7"
   }
 }
index ab85720faa8fa900a56e740f82bc11fdd8684ce0..989a5fd3b80701680b427db88a63c6405f95e44d 100644 (file)
@@ -123,7 +123,7 @@ export const createApp = ((...args) => {
     }
 
     // clear content before mounting
-    container.innerHTML = ''
+    container.textContent = ''
     const proxy = mount(container, false, resolveRootNamespace(container))
     if (container instanceof Element) {
       container.removeAttribute('v-cloak')
index ef3ef0748c111eba5097444ab8ae9fc940c8165e..8ed70c7d330c1ebd8345b52d15a2772cea8554dc 100644 (file)
@@ -1,4 +1,39 @@
+import { warn } from '@vue/runtime-core'
 import type { RendererOptions } from '@vue/runtime-core'
+import type {
+  TrustedHTML,
+  TrustedTypePolicy,
+  TrustedTypesWindow,
+} from 'trusted-types/lib'
+
+let policy: Pick<TrustedTypePolicy, 'name' | 'createHTML'> | undefined =
+  undefined
+
+const tt =
+  typeof window !== 'undefined' &&
+  (window as unknown as TrustedTypesWindow).trustedTypes
+
+if (tt) {
+  try {
+    policy = /*#__PURE__*/ tt.createPolicy('vue', {
+      createHTML: val => val,
+    })
+  } catch (e: unknown) {
+    // `createPolicy` throws a TypeError if the name is a duplicate
+    // and the CSP trusted-types directive is not using `allow-duplicates`.
+    // So we have to catch that error.
+    __DEV__ && warn(`Error creating trusted types policy: ${e}`)
+  }
+}
+
+// __UNSAFE__
+// Reason: potentially setting innerHTML.
+// This function merely perform a type-level trusted type conversion
+// for use in `innerHTML` assignment, etc.
+// Be careful of whatever value passed to this function.
+const unsafeToTrustedHTML: (value: string) => TrustedHTML | string = policy
+  ? val => policy.createHTML(val)
+  : val => val
 
 export const svgNS = 'http://www.w3.org/2000/svg'
 export const mathmlNS = 'http://www.w3.org/1998/Math/MathML'
@@ -76,12 +111,13 @@ export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
       }
     } else {
       // fresh insert
-      templateContainer.innerHTML =
+      templateContainer.innerHTML = unsafeToTrustedHTML(
         namespace === 'svg'
           ? `<svg>${content}</svg>`
           : namespace === 'mathml'
             ? `<math>${content}</math>`
-            : content
+            : content,
+      ) as string
 
       const template = templateContainer.content
       if (namespace === 'svg' || namespace === 'mathml') {
diff --git a/packages/vue/__tests__/e2e/trusted-types.html b/packages/vue/__tests__/e2e/trusted-types.html
new file mode 100644 (file)
index 0000000..a5743a6
--- /dev/null
@@ -0,0 +1,17 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="data:;base64,iVBORw0KGgo=">
+    <meta
+      http-equiv="content-security-policy"
+      content="require-trusted-types-for 'script'"
+    />
+    <title>Vue App</title>
+    <script src="../../dist/vue.global.js"></script>
+  </head>
+
+  <body>
+    <div id="app"></div>
+  </body>
+</html>
diff --git a/packages/vue/__tests__/e2e/trusted-types.spec.ts b/packages/vue/__tests__/e2e/trusted-types.spec.ts
new file mode 100644 (file)
index 0000000..927f594
--- /dev/null
@@ -0,0 +1,103 @@
+import { once } from 'node:events'
+import { createServer } from 'node:http'
+import path from 'node:path'
+import { beforeAll } from 'vitest'
+import serveHandler from 'serve-handler'
+
+import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
+
+// use the `vue` package root as the public directory
+// because we need to serve the Vue runtime for the tests
+const serverRoot = path.resolve(import.meta.dirname, '../../')
+const testPort = 9090
+const basePath = path.relative(
+  serverRoot,
+  path.resolve(import.meta.dirname, './trusted-types.html'),
+)
+const baseUrl = `http://localhost:${testPort}/${basePath}`
+
+const { page, html } = setupPuppeteer()
+
+let server: ReturnType<typeof createServer>
+beforeAll(async () => {
+  // sets up the static server
+  server = createServer((req, res) => {
+    return serveHandler(req, res, {
+      public: serverRoot,
+      cleanUrls: false,
+    })
+  })
+
+  server.listen(testPort)
+  await once(server, 'listening')
+})
+
+afterAll(async () => {
+  server.close()
+  await once(server, 'close')
+})
+
+describe('e2e: trusted types', () => {
+  beforeEach(async () => {
+    await page().goto(baseUrl)
+    await page().waitForSelector('#app')
+  })
+
+  test(
+    'should render the hello world app',
+    async () => {
+      await page().evaluate(() => {
+        const { createApp, ref, h } = (window as any).Vue
+        createApp({
+          setup() {
+            const msg = ref('✅success: hello world')
+            return function render() {
+              return h('div', msg.value)
+            }
+          },
+        }).mount('#app')
+      })
+      expect(await html('#app')).toContain('<div>✅success: hello world</div>')
+    },
+    E2E_TIMEOUT,
+  )
+
+  test(
+    'should render static vnode without error',
+    async () => {
+      await page().evaluate(() => {
+        const { createApp, createStaticVNode } = (window as any).Vue
+        createApp({
+          render() {
+            return createStaticVNode('<div>✅success: static vnode</div>')
+          },
+        }).mount('#app')
+      })
+      expect(await html('#app')).toContain('<div>✅success: static vnode</div>')
+    },
+    E2E_TIMEOUT,
+  )
+
+  test(
+    'should accept v-html with custom policy',
+    async () => {
+      await page().evaluate(() => {
+        const testPolicy = (window as any).trustedTypes.createPolicy('test', {
+          createHTML: (input: string): string => input,
+        })
+
+        const { createApp, ref, h } = (window as any).Vue
+        createApp({
+          setup() {
+            const msg = ref('✅success: v-html')
+            return function render() {
+              return h('div', { innerHTML: testPolicy.createHTML(msg.value) })
+            }
+          },
+        }).mount('#app')
+      })
+      expect(await html('#app')).toContain('<div>✅success: v-html</div>')
+    },
+    E2E_TIMEOUT,
+  )
+})
index 4b23377766c4af9b180d9fb593543b846f19ac60..f1f5881b22fee3039a5ac7e1bbc04b55fd32ac61 100644 (file)
@@ -62,6 +62,9 @@ importers:
       '@types/semver':
         specifier: ^7.5.8
         version: 7.5.8
+      '@types/serve-handler':
+        specifier: ^6.1.4
+        version: 6.1.4
       '@vitest/coverage-istanbul':
         specifier: ^1.6.0
         version: 1.6.0(vitest@1.6.0(@types/node@20.14.13)(jsdom@24.1.1)(sass@1.77.8)(terser@5.31.1))
@@ -149,6 +152,9 @@ importers:
       serve:
         specifier: ^14.2.3
         version: 14.2.3
+      serve-handler:
+        specifier: ^6.1.5
+        version: 6.1.5
       simple-git-hooks:
         specifier: ^2.11.1
         version: 2.11.1
@@ -325,6 +331,10 @@ importers:
       csstype:
         specifier: ^3.1.3
         version: 3.1.3
+    devDependencies:
+      '@types/trusted-types':
+        specifier: ^2.0.7
+        version: 2.0.7
 
   packages/runtime-test:
     dependencies:
@@ -1220,6 +1230,12 @@ packages:
   '@types/semver@7.5.8':
     resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
 
+  '@types/serve-handler@6.1.4':
+    resolution: {integrity: sha512-aXy58tNie0NkuSCY291xUxl0X+kGYy986l4kqW6Gi4kEXgr6Tx0fpSH7YwUSa5usPpG3s9DBeIR6hHcDtL2IvQ==}
+
+  '@types/trusted-types@2.0.7':
+    resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
+
   '@types/yauzl@2.10.3':
     resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
 
@@ -4204,6 +4220,12 @@ snapshots:
 
   '@types/semver@7.5.8': {}
 
+  '@types/serve-handler@6.1.4':
+    dependencies:
+      '@types/node': 20.14.13
+
+  '@types/trusted-types@2.0.7': {}
+
   '@types/yauzl@2.10.3':
     dependencies:
       '@types/node': 20.14.13