-
-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
⭐ new(rule): add no-html-messages rule
- Loading branch information
Showing
12 changed files
with
375 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
# vue-i18n/no-html-messages | ||
|
||
> disallow use HTML localization messages | ||
- :star: The `"extends": "plugin:vue-i18n/recommended"` property in a configuration file enables this rule. | ||
|
||
This rule reports in order to reduce the risk of injecting potentially unsafe localization message into the browser leading to supply-chain attack or XSS attack. | ||
|
||
## :book: Rule Details | ||
|
||
This rule is aimed at eliminating HTML localization messages. | ||
|
||
:-1: Examples of **incorrect** code for this rule: | ||
|
||
locale messages: | ||
```js | ||
// ✗ BAD | ||
{ | ||
"hello": "Hello! DIO!", | ||
"hi": "Hi! <span>DIO!</span>", | ||
"contenst": { | ||
"banner": "banner: <iframe src=\"https://banner.domain.com\" frameBorder=\"0\" style=\"z-index:100001;position:fixed;bottom:0;right:0\"/>", | ||
"modal": "modal: <span onmouseover=\"alert(document.cookie);\">modal content</span>" | ||
} | ||
} | ||
``` | ||
|
||
In localization codes of application: | ||
|
||
```vue | ||
<template> | ||
<div class="app"> | ||
<p>{{ $t('hello') }}</p> | ||
<!-- supply-chain attack --> | ||
<div v-html="$t('contents.banner')"></div> | ||
<!-- XSS attack --> | ||
<div v-html="$t('contents.modal')"></div> | ||
</div> | ||
</template> | ||
``` | ||
|
||
```js | ||
const i18n = new VueI18n({ | ||
locale: 'en', | ||
messages: { | ||
en: require('./locales/en.json') | ||
} | ||
}) | ||
|
||
new Vue({ | ||
i18n, | ||
// ... | ||
}).$mount('#app') | ||
``` | ||
|
||
:+1: Examples of **correct** code for this rule: | ||
|
||
locale messages: | ||
// ✓ GOOD | ||
```js | ||
{ | ||
"hello": "Hello! DIO!", | ||
"hi": "Hi! DIO!", | ||
"contents": { | ||
"banner": "banner: {0}", | ||
"modal": "modal: {0}" | ||
} | ||
} | ||
``` | ||
|
||
In localization codes of application: | ||
|
||
```vue | ||
<template> | ||
<div class="app"> | ||
<p>{{ $t('hello') }}</p> | ||
<i18n path="contents.banner"> | ||
<Banner :url="bannerURL"/> | ||
</i18n> | ||
<i18n path="contents.modal"> | ||
<Modal :url="modalDataURL"/> | ||
</i18n> | ||
</div> | ||
</template> | ||
``` | ||
|
||
```js | ||
// import some components used in i18n component | ||
import Banner from './path/to/components/Banner.vue' | ||
import Modal from './path/to/components/Modal.vue' | ||
|
||
// register imprted components (in this example case, Vue.component) | ||
Vue.component('Banner', Banner) | ||
Vue.component('Modal', Modal) | ||
|
||
const i18n = new VueI18n({ | ||
locale: 'en', | ||
messages: { | ||
en: require('./locales/en.json') | ||
} | ||
}) | ||
|
||
new Vue({ | ||
i18n, | ||
data () { | ||
return { | ||
bannerURL: 'https://banner.domain.com', | ||
modalDataURL: 'https://fetch.domain.com' | ||
} | ||
} | ||
// ... | ||
}).$mount('#app') | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
/** | ||
* @author kazuya kawaguchi (a.k.a. kazupon) | ||
*/ | ||
'use strict' | ||
|
||
const { extname } = require('path') | ||
const jsonAstParse = require('json-to-ast') | ||
const parse5 = require('parse5') | ||
const { UNEXPETECD_ERROR_LOCATION, loadLocaleMessages } = require('../utils/index') | ||
const debug = require('debug')('eslint-plugin-vue-i18n:no-html-messages') | ||
|
||
let localeMessages = null // used locale messages | ||
let localeDir = null | ||
|
||
function findExistLocaleMessage (fullpath, localeMessages) { | ||
return localeMessages.find(message => message.fullpath === fullpath) | ||
} | ||
|
||
function extractJsonInfo (context, node) { | ||
try { | ||
const [str, filename] = node.comments | ||
return [ | ||
Buffer.from(str.value, 'base64').toString(), | ||
Buffer.from(filename.value, 'base64').toString() | ||
] | ||
} catch (e) { | ||
context.report({ | ||
loc: UNEXPETECD_ERROR_LOCATION, | ||
message: e.message | ||
}) | ||
return [] | ||
} | ||
} | ||
|
||
function generateJsonAst (context, json, filename) { | ||
let ast = null | ||
|
||
try { | ||
ast = jsonAstParse(json, { loc: true, source: filename }) | ||
} catch (e) { | ||
const { message, line, column } = e | ||
context.report({ | ||
message, | ||
loc: { line, column } | ||
}) | ||
} | ||
|
||
return ast | ||
} | ||
|
||
function traverseNode (node, fn) { | ||
if (node.type === 'Object' && node.children.length > 0) { | ||
node.children.forEach(child => { | ||
if (child.type === 'Property') { | ||
const keyNode = child.key | ||
const valueNode = child.value | ||
if (keyNode.type === 'Identifier' && valueNode.type === 'Object') { | ||
return traverseNode(valueNode, fn) | ||
} else { | ||
return fn(valueNode) | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
function findHTMLNode (node) { | ||
return node.childNodes.find(child => { | ||
if (child.nodeName !== '#text' && child.tagName) { | ||
return true | ||
} | ||
}) | ||
} | ||
|
||
function create (context) { | ||
const filename = context.getFilename() | ||
if (extname(filename) !== '.json') { | ||
debug(`ignore ${filename} in no-html-messages`) | ||
return {} | ||
} | ||
|
||
const { settings } = context | ||
if (!settings['vue-i18n'] || !settings['vue-i18n'].localeDir) { | ||
context.report({ | ||
loc: UNEXPETECD_ERROR_LOCATION, | ||
message: `You need to 'localeDir' at 'settings. See the 'eslint-plugin-vue-i18n documentation` | ||
}) | ||
return {} | ||
} | ||
|
||
if (localeDir !== settings['vue-i18n'].localeDir) { | ||
debug(`change localeDir: ${localeDir} -> ${settings['vue-i18n'].localeDir}`) | ||
localeDir = settings['vue-i18n'].localeDir | ||
localeMessages = loadLocaleMessages(localeDir) | ||
} else { | ||
localeMessages = localeMessages || loadLocaleMessages(settings['vue-i18n'].localeDir) | ||
} | ||
|
||
const targetLocaleMessage = findExistLocaleMessage(filename, localeMessages) | ||
if (!targetLocaleMessage) { | ||
debug(`ignore ${filename} in no-html-messages`) | ||
return {} | ||
} | ||
|
||
return { | ||
Program (node) { | ||
const [jsonString, jsonFilename] = extractJsonInfo(context, node) | ||
if (!jsonString || !jsonFilename) { return } | ||
|
||
const ast = generateJsonAst(context, jsonString, jsonFilename) | ||
if (!ast) { return } | ||
|
||
traverseNode(ast, messageNode => { | ||
const htmlNode = parse5.parseFragment(messageNode.value, { sourceCodeLocationInfo: true }) | ||
const foundNode = findHTMLNode(htmlNode) | ||
if (!foundNode) { return } | ||
context.report({ | ||
message: `used HTML localization message in '${targetLocaleMessage.path}'`, | ||
loc: { | ||
line: messageNode.loc.start.line, | ||
column: messageNode.loc.start.column + foundNode.sourceCodeLocation.startOffset | ||
} | ||
}) | ||
}) | ||
} | ||
} | ||
} | ||
|
||
module.exports = { | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: 'disallow use HTML localization messages', | ||
category: 'Recommended', | ||
recommended: true | ||
}, | ||
fixable: null, | ||
schema: [] | ||
}, | ||
create | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"hello": "Hello! DIO!", | ||
"hi": "Hi! <span>DIO!</span>", | ||
"contents": { | ||
"banner": "banner: <iframe src=\"https://banner.domain.com\" frameBorder=\"0\" style=\"z-index:100001;position:fixed;bottom:0;right:0\"/>", | ||
"modal": "modal: <span onmouseover=\"alert(document.cookie);\">modal content</span>" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"hello": "Hello! DIO!", | ||
"hi": "Hi! DIO!", | ||
"contents": { | ||
"banner": "banner: {0}", | ||
"modal": "modal: {0}" | ||
} | ||
} |
Oops, something went wrong.