Skip to content

Commit

Permalink
feat(unplugin-vue-i18n): support strict message and escape html (#249)
Browse files Browse the repository at this point in the history
* feat(unplugin-vue-i18n): support strict message and escape html

* docs: update readme

* chore: update lock
  • Loading branch information
kazupon committed Mar 22, 2023
1 parent 2714695 commit 56d5b87
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 5 deletions.
25 changes: 25 additions & 0 deletions packages/unplugin-vue-i18n/README.md
Expand Up @@ -300,6 +300,31 @@ This plugin will automatically select and bundle `petite-vue-i18n` build accordi
> ⚠️ NOTE:
If you use the `js` and `ts` resources formats, set the paths, so your application code is not targeted. We recommend that resources be isolated from the application code.


### `strictMessage`

- **Type:** `boolean`
- **Default:** `true`

Strictly checks that the locale message does not contain html tags.

If html tags are included, an error is thrown.

If you would not the error to be thrown, you can work around it by setting it to `false`, but **it means that the locale message might cause security problems with XSS**.

In that case, we recommend setting the `escapeHtml` option to `true`.


### `escapeHtml`

- **Type:** `boolean`
- **Default:** `false`

Whether to escape html tags if they are included in the locale message.

If `strictMessage` is disabled by `false`, we recommend this option be enabled.


### `allowDynamic`

- **Type:** `boolean`
Expand Down
2 changes: 1 addition & 1 deletion packages/unplugin-vue-i18n/package.json
Expand Up @@ -26,7 +26,7 @@
}
},
"dependencies": {
"@intlify/bundle-utils": "^5.3.1",
"@intlify/bundle-utils": "^5.4.0",
"@intlify/shared": "9.3.0-beta.17",
"@rollup/pluginutils": "^5.0.2",
"@vue/compiler-sfc": "^3.2.47",
Expand Down
31 changes: 30 additions & 1 deletion packages/unplugin-vue-i18n/src/index.ts
Expand Up @@ -121,6 +121,15 @@ export const unplugin = createUnplugin<PluginOptions>((options = {}, meta) => {
debug('esm', esm)

const allowDynamic = !!options.allowDynamic
debug('allowDynamic', allowDynamic)

const strictMessage = isBoolean(options.strictMessage)
? options.strictMessage
: true
debug('strictMessage', strictMessage)

const escapeHtml = !!options.escapeHtml
debug('escapeHtml', escapeHtml)

let isProduction = false
let sourceMap = false
Expand Down Expand Up @@ -294,6 +303,8 @@ export const unplugin = createUnplugin<PluginOptions>((options = {}, meta) => {
isGlobal: globalSFCScope,
useClassComponent,
allowDynamic,
strictMessage,
escapeHtml,
bridge,
exportESM: esm,
forceStringify
Expand Down Expand Up @@ -449,6 +460,8 @@ export const unplugin = createUnplugin<PluginOptions>((options = {}, meta) => {
{
forceStringify,
bridge,
strictMessage,
escapeHtml,
exportESM: esm,
useClassComponent
}
Expand Down Expand Up @@ -500,6 +513,8 @@ export const unplugin = createUnplugin<PluginOptions>((options = {}, meta) => {
isGlobal: globalSFCScope,
useClassComponent,
allowDynamic,
strictMessage,
escapeHtml,
bridge,
exportESM: esm,
forceStringify
Expand Down Expand Up @@ -555,6 +570,8 @@ export const unplugin = createUnplugin<PluginOptions>((options = {}, meta) => {
isGlobal: globalSFCScope,
useClassComponent,
bridge,
strictMessage,
escapeHtml,
exportESM: esm,
forceStringify
}
Expand Down Expand Up @@ -644,12 +661,16 @@ async function generateBundleResources(
isGlobal = false,
bridge = false,
exportESM = true,
strictMessage = true,
escapeHtml = false,
useClassComponent = false
}: {
forceStringify?: boolean
isGlobal?: boolean
bridge?: boolean
exportESM?: boolean
strictMessage?: boolean
escapeHtml?: boolean
useClassComponent?: boolean
}
) {
Expand All @@ -666,6 +687,8 @@ async function generateBundleResources(
useClassComponent,
bridge,
exportESM,
strictMessage,
escapeHtml,
forceStringify
}) as CodeGenOptions
parseOptions.type = 'bare'
Expand Down Expand Up @@ -743,7 +766,9 @@ function getOptions(
bridge = false,
exportESM = true,
useClassComponent = false,
allowDynamic = false
allowDynamic = false,
strictMessage = true,
escapeHtml = false
}: {
inSourceMap?: RawSourceMap
forceStringify?: boolean
Expand All @@ -752,6 +777,8 @@ function getOptions(
exportESM?: boolean
useClassComponent?: boolean
allowDynamic?: boolean
strictMessage?: boolean
escapeHtml?: boolean
}
): Record<string, unknown> {
const mode: DevEnv = isProduction ? 'production' : 'development'
Expand All @@ -763,6 +790,8 @@ function getOptions(
forceStringify,
useClassComponent,
allowDynamic,
strictMessage,
escapeHtml,
bridge,
exportESM,
env: mode,
Expand Down
2 changes: 2 additions & 0 deletions packages/unplugin-vue-i18n/src/types.ts
Expand Up @@ -12,4 +12,6 @@ export interface PluginOptions {
bridge?: boolean
useClassComponent?: boolean
useVueI18nImportName?: boolean
strictMessage?: boolean
escapeHtml?: boolean
}
4 changes: 4 additions & 0 deletions packages/unplugin-vue-i18n/test/fixtures/locales/html.json
@@ -0,0 +1,4 @@
{
"hi": "<p>hi there!</p>",
"alert": "<script>window.alert('hi there!')</script>"
}
8 changes: 8 additions & 0 deletions packages/unplugin-vue-i18n/test/utils.ts
Expand Up @@ -38,6 +38,10 @@ export async function bundleVite(
? 'info'
: 'silent'
: 'silent'
options.strictMessage = isBoolean(options.strictMessage)
? options.strictMessage
: true
options.escapeHtml = !!options.escapeHtml

const alias: Record<string, string> = {
vue: 'vue/dist/vue.runtime.esm-browser.js'
Expand Down Expand Up @@ -154,6 +158,10 @@ export async function bundleAndRun(
options.useClassComponent = isBoolean(options.useClassComponent) || false
options.bridge = isBoolean(options.bridge) || false
options.allowDynamic = isBoolean(options.allowDynamic) || false
options.strictMessage = isBoolean(options.strictMessage)
? options.strictMessage
: true
options.escapeHtml = !!options.escapeHtml

const { code, map } = await bundler(fixture, options)

Expand Down
1 change: 1 addition & 0 deletions packages/unplugin-vue-i18n/test/vite/bundle-import.test.ts
Expand Up @@ -16,6 +16,7 @@ import { createMessageContext } from '@intlify/core-base'
test(testcase, async () => {
const options = {
input,
strictMessage: false,
include: [resolve(__dirname, '../fixtures/locales/**')]
}
const { exports: messages } = await bundleAndRun(
Expand Down
@@ -1,8 +1,19 @@
import { resolve } from 'pathe'
import { bundleVite, bundleAndRun } from '../utils'
import { isFunction } from '@intlify/shared'
import { isFunction, assign } from '@intlify/shared'
import { createMessageContext } from '@intlify/core-base'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let spyConsoleError: any
beforeEach(() => {
spyConsoleError = jest.spyOn(global.console, 'error').mockImplementation()
})

afterEach(() => {
spyConsoleError.mockReset()
spyConsoleError.mockRestore()
})

const options = {
target: './fixtures/locales/',
include: [resolve(__dirname, '../fixtures/locales/**')]
Expand Down Expand Up @@ -57,3 +68,30 @@ test('dynamical resource with js / ts', async () => {
})
expect(isFunction(module)).toBe(true)
})

test('strict message', async () => {
let occured = false
try {
await bundleAndRun('html.json', bundleVite, {
...options
})
} catch (e) {
occured = true
}
expect(occured).toBe(true)
})

test('escape message', async () => {
const { module } = await bundleAndRun(
'html.json',
bundleVite,
assign(options, {
strictMessage: false,
escapeHtml: true
})
)
expect(module.hi(createMessageContext())).toBe(`&lt;p&gt;hi there!&lt;/p&gt;`)
expect(module.alert(createMessageContext())).toBe(
`&lt;script&gt;window.alert(&apos;hi there!&apos;)&lt;/script&gt;`
)
})
4 changes: 2 additions & 2 deletions yarn.lock
Expand Up @@ -969,7 +969,7 @@ __metadata:
languageName: unknown
linkType: soft

"@intlify/bundle-utils@^5.3.1, @intlify/bundle-utils@workspace:packages/bundle-utils":
"@intlify/bundle-utils@^5.4.0, @intlify/bundle-utils@workspace:packages/bundle-utils":
version: 0.0.0-use.local
resolution: "@intlify/bundle-utils@workspace:packages/bundle-utils"
dependencies:
Expand Down Expand Up @@ -1114,7 +1114,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@intlify/unplugin-vue-i18n@workspace:packages/unplugin-vue-i18n"
dependencies:
"@intlify/bundle-utils": ^5.3.1
"@intlify/bundle-utils": ^5.4.0
"@intlify/shared": 9.3.0-beta.17
"@rollup/pluginutils": ^5.0.2
"@vue/compiler-sfc": ^3.2.47
Expand Down

0 comments on commit 56d5b87

Please sign in to comment.