Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(unplugin-vue-i18n): support strict message and escape html #249

Merged
merged 3 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/unplugin-vue-i18n/README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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