Skip to content

Commit fca6fc5

Browse files
Merge pull request #2603 from NullVoxPopuli/nvp/template-lint-extract-rule-template-require-form-method
Extract rule: template-require-form-method
2 parents 7500a98 + 1ebfdf1 commit fca6fc5

4 files changed

Lines changed: 296 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ rules in templates can be disabled with eslint directives with mustache or html
253253
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
254254
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
255255
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |
256+
| [template-require-form-method](docs/rules/template-require-form-method.md) | require form method attribute | | 🔧 | |
256257
| [template-require-has-block-helper](docs/rules/template-require-has-block-helper.md) | require (has-block) helper usage instead of hasBlock property | | 🔧 | |
257258
| [template-require-iframe-src-attribute](docs/rules/template-require-iframe-src-attribute.md) | require iframe elements to have src attribute | | 🔧 | |
258259
| [template-require-splattributes](docs/rules/template-require-splattributes.md) | require splattributes usage in component templates | | | |
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# ember/template-require-form-method
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
This rule requires all `<form>` elements to have `method` attribute with `POST`, `GET` or `DIALOG` value.
8+
9+
By default `form` elements without `method` attribute are submitted as `GET` requests.
10+
In usual applications `submit` event listeners are attached to `form` elements and `event.preventDefault()` is called to avoid form submission.
11+
12+
However in case of failure to prevent default action, form submission as `GET` request can leak sensitive end-user information.
13+
14+
Example uses of `GET` requests:
15+
16+
- non-secure data
17+
- bookmarking the submission result
18+
- data search query strings
19+
20+
**Caution** - this rules does not check for `formmethod` attribute on `form` elements themselves.
21+
22+
## Examples
23+
24+
This rule **forbids** the following:
25+
26+
```gjs
27+
<template>
28+
<form>Hello world!</form>
29+
<form method=''></form>
30+
<form method='random'>Hello world!</form>
31+
</template>
32+
```
33+
34+
This rule **allows** the following:
35+
36+
```gjs
37+
<template>
38+
<form method='post'>Hello world!</form>
39+
<form method='get'>Hello world!</form>
40+
<form method='dialog'>Hello world!</form>
41+
</template>
42+
```
43+
44+
## Configuration
45+
46+
The following values are valid configuration:
47+
48+
- boolean - `true` to enable / `false` to disable
49+
- object -- An object with the following keys:
50+
- `allowedMethods` -- An array of allowed form `method` attribute values, default: `['POST', 'GET', 'DIALOG']`
51+
52+
## References
53+
54+
- [MDN - form method attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method)
55+
- [HTML spec - form method attribute](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-method)
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Form `method` attribute keywords:
2+
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-method
3+
const VALID_FORM_METHODS = ['POST', 'GET', 'DIALOG'];
4+
5+
const DEFAULT_CONFIG = {
6+
allowedMethods: VALID_FORM_METHODS,
7+
};
8+
9+
function parseConfig(config) {
10+
if (config === false || config === undefined) {
11+
return false;
12+
}
13+
14+
if (config === true) {
15+
return DEFAULT_CONFIG;
16+
}
17+
18+
if (typeof config === 'object' && Array.isArray(config.allowedMethods)) {
19+
const allowedMethods = config.allowedMethods.map((m) => String(m).toUpperCase());
20+
21+
// Check if all methods are valid
22+
const hasAllValid = allowedMethods.every((m) => VALID_FORM_METHODS.includes(m));
23+
24+
if (hasAllValid) {
25+
return { allowedMethods };
26+
}
27+
}
28+
29+
return false;
30+
}
31+
32+
function makeErrorMessage(methods) {
33+
return `All \`<form>\` elements should have \`method\` attribute with value of \`${methods.join(',')}\``;
34+
}
35+
36+
function getFixedMethod(config) {
37+
return config.allowedMethods[0];
38+
}
39+
40+
/** @type {import('eslint').Rule.RuleModule} */
41+
module.exports = {
42+
meta: {
43+
type: 'suggestion',
44+
docs: {
45+
description: 'require form method attribute',
46+
category: 'Best Practices',
47+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-require-form-method.md',
48+
templateMode: 'both',
49+
},
50+
fixable: 'code',
51+
schema: [
52+
{
53+
oneOf: [
54+
{
55+
type: 'object',
56+
properties: {
57+
allowedMethods: {
58+
type: 'array',
59+
items: {
60+
type: 'string',
61+
},
62+
},
63+
},
64+
additionalProperties: false,
65+
},
66+
],
67+
},
68+
],
69+
messages: {
70+
invalidMethod: '{{message}}',
71+
},
72+
originallyFrom: {
73+
name: 'ember-template-lint',
74+
rule: 'lib/rules/require-form-method.js',
75+
docs: 'docs/rule/require-form-method.md',
76+
tests: 'test/unit/rules/require-form-method-test.js',
77+
},
78+
},
79+
80+
create(context) {
81+
// If no options provided, use defaults
82+
let config = context.options[0];
83+
config = config ? parseConfig(config) : DEFAULT_CONFIG;
84+
85+
if (config === false) {
86+
return {};
87+
}
88+
89+
return {
90+
GlimmerElementNode(node) {
91+
if (node.tag !== 'form') {
92+
return;
93+
}
94+
95+
const methodAttribute = node.attributes.find((attr) => attr.name === 'method');
96+
97+
if (!methodAttribute) {
98+
context.report({
99+
node,
100+
messageId: 'invalidMethod',
101+
data: {
102+
message: makeErrorMessage(config.allowedMethods),
103+
},
104+
fix(fixer) {
105+
return fixer.insertTextAfterRange(
106+
[node.parts.at(-1).range[1], node.parts.at(-1).range[1]],
107+
` method="${getFixedMethod(config)}"`
108+
);
109+
},
110+
});
111+
return;
112+
}
113+
114+
// Check if it's a text value
115+
if (methodAttribute.value && methodAttribute.value.type === 'GlimmerTextNode') {
116+
const methodValue = methodAttribute.value.chars.toUpperCase();
117+
118+
if (!config.allowedMethods.includes(methodValue)) {
119+
context.report({
120+
node,
121+
messageId: 'invalidMethod',
122+
data: {
123+
message: makeErrorMessage(config.allowedMethods),
124+
},
125+
fix(fixer) {
126+
return fixer.replaceTextRange(
127+
methodAttribute.value.range,
128+
`"${getFixedMethod(config)}"`
129+
);
130+
},
131+
});
132+
}
133+
}
134+
// If it's a dynamic value (like {{foo}}), don't report
135+
},
136+
};
137+
},
138+
};
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
const rule = require('../../../lib/rules/template-require-form-method');
2+
const RuleTester = require('eslint').RuleTester;
3+
4+
const DEFAULT_ERROR =
5+
'All `<form>` elements should have `method` attribute with value of `POST,GET,DIALOG`';
6+
7+
const validHbs = [
8+
{
9+
options: [{ allowedMethods: ['get'] }],
10+
code: '<form method="GET"></form>',
11+
},
12+
'<form method="POST"></form>',
13+
'<form method="post"></form>',
14+
'<form method="GET"></form>',
15+
'<form method="get"></form>',
16+
'<form method="DIALOG"></form>',
17+
'<form method="dialog"></form>',
18+
'<form method="{{formMethod}}"></form>',
19+
'<form method={{formMethod}}></form>',
20+
'<div/>',
21+
'<div></div>',
22+
'<div method="randomType"></div>',
23+
];
24+
25+
const invalidHbs = [
26+
{
27+
options: [{ allowedMethods: ['get'] }],
28+
code: '<form method="POST"></form>',
29+
output: '<form method="GET"></form>',
30+
errors: [
31+
{ message: 'All `<form>` elements should have `method` attribute with value of `GET`' },
32+
],
33+
},
34+
{
35+
options: [{ allowedMethods: ['POST'] }],
36+
code: '<form method="GET"></form>',
37+
output: '<form method="POST"></form>',
38+
errors: [
39+
{ message: 'All `<form>` elements should have `method` attribute with value of `POST`' },
40+
],
41+
},
42+
{
43+
code: '<form></form>',
44+
output: '<form method="POST"></form>',
45+
errors: [{ message: DEFAULT_ERROR }],
46+
},
47+
{
48+
code: '<form method=""></form>',
49+
output: '<form method="POST"></form>',
50+
errors: [{ message: DEFAULT_ERROR }],
51+
},
52+
{
53+
code: '<form method=42></form>',
54+
output: '<form method="POST"></form>',
55+
errors: [{ message: DEFAULT_ERROR }],
56+
},
57+
{
58+
code: '<form method=" ge t "></form>',
59+
output: '<form method="POST"></form>',
60+
errors: [{ message: DEFAULT_ERROR }],
61+
},
62+
{
63+
code: '<form method=" pos t "></form>',
64+
output: '<form method="POST"></form>',
65+
errors: [{ message: DEFAULT_ERROR }],
66+
},
67+
];
68+
69+
function wrapTemplate(entry) {
70+
if (typeof entry === 'string') {
71+
return `<template>${entry}</template>`;
72+
}
73+
74+
return {
75+
...entry,
76+
code: `<template>${entry.code}</template>`,
77+
output: entry.output ? `<template>${entry.output}</template>` : entry.output,
78+
};
79+
}
80+
81+
const gjsRuleTester = new RuleTester({
82+
parser: require.resolve('ember-eslint-parser'),
83+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
84+
});
85+
86+
gjsRuleTester.run('template-require-form-method', rule, {
87+
valid: validHbs.map(wrapTemplate),
88+
invalid: invalidHbs.map(wrapTemplate),
89+
});
90+
91+
const hbsRuleTester = new RuleTester({
92+
parser: require.resolve('ember-eslint-parser/hbs'),
93+
parserOptions: {
94+
ecmaVersion: 2022,
95+
sourceType: 'module',
96+
},
97+
});
98+
99+
hbsRuleTester.run('template-require-form-method', rule, {
100+
valid: validHbs,
101+
invalid: invalidHbs,
102+
});

0 commit comments

Comments
 (0)