Skip to content
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |
| [template-require-form-method](docs/rules/template-require-form-method.md) | require form method attribute | | 🔧 | |
| [template-require-has-block-helper](docs/rules/template-require-has-block-helper.md) | require (has-block) helper usage instead of hasBlock property | | 🔧 | |
| [template-require-iframe-src-attribute](docs/rules/template-require-iframe-src-attribute.md) | require iframe elements to have src attribute | | 🔧 | |
| [template-require-splattributes](docs/rules/template-require-splattributes.md) | require splattributes usage in component templates | | | |
Expand Down
55 changes: 55 additions & 0 deletions docs/rules/template-require-form-method.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# ember/template-require-form-method

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

This rule requires all `<form>` elements to have `method` attribute with `POST`, `GET` or `DIALOG` value.

By default `form` elements without `method` attribute are submitted as `GET` requests.
In usual applications `submit` event listeners are attached to `form` elements and `event.preventDefault()` is called to avoid form submission.

However in case of failure to prevent default action, form submission as `GET` request can leak sensitive end-user information.

Example uses of `GET` requests:

- non-secure data
- bookmarking the submission result
- data search query strings

**Caution** - this rules does not check for `formmethod` attribute on `form` elements themselves.

## Examples

This rule **forbids** the following:

```gjs
<template>
<form>Hello world!</form>
<form method=''></form>
<form method='random'>Hello world!</form>
</template>
```

This rule **allows** the following:

```gjs
<template>
<form method='post'>Hello world!</form>
<form method='get'>Hello world!</form>
<form method='dialog'>Hello world!</form>
</template>
```

## Configuration

The following values are valid configuration:

- boolean - `true` to enable / `false` to disable
- object -- An object with the following keys:
- `allowedMethods` -- An array of allowed form `method` attribute values, default: `['POST', 'GET', 'DIALOG']`

## References

- [MDN - form method attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method)
- [HTML spec - form method attribute](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-method)
138 changes: 138 additions & 0 deletions lib/rules/template-require-form-method.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Form `method` attribute keywords:
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-method
const VALID_FORM_METHODS = ['POST', 'GET', 'DIALOG'];

const DEFAULT_CONFIG = {
allowedMethods: VALID_FORM_METHODS,
};

function parseConfig(config) {
if (config === false || config === undefined) {
return false;
}

if (config === true) {
return DEFAULT_CONFIG;
}

if (typeof config === 'object' && Array.isArray(config.allowedMethods)) {
const allowedMethods = config.allowedMethods.map((m) => String(m).toUpperCase());

// Check if all methods are valid
const hasAllValid = allowedMethods.every((m) => VALID_FORM_METHODS.includes(m));

if (hasAllValid) {
return { allowedMethods };
}
}

return false;
}

function makeErrorMessage(methods) {
return `All \`<form>\` elements should have \`method\` attribute with value of \`${methods.join(',')}\``;
}

function getFixedMethod(config) {
return config.allowedMethods[0];
}

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'require form method attribute',
category: 'Best Practices',
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-require-form-method.md',
templateMode: 'both',
},
fixable: 'code',
schema: [
{
oneOf: [
{
type: 'object',
properties: {
allowedMethods: {
type: 'array',
items: {
type: 'string',
},
},
},
additionalProperties: false,
},
],
},
],
messages: {
invalidMethod: '{{message}}',
},
originallyFrom: {
name: 'ember-template-lint',
rule: 'lib/rules/require-form-method.js',
docs: 'docs/rule/require-form-method.md',
tests: 'test/unit/rules/require-form-method-test.js',
},
},

create(context) {
// If no options provided, use defaults
let config = context.options[0];
config = config ? parseConfig(config) : DEFAULT_CONFIG;

if (config === false) {
return {};
}

return {
GlimmerElementNode(node) {
if (node.tag !== 'form') {
return;
}

const methodAttribute = node.attributes.find((attr) => attr.name === 'method');

if (!methodAttribute) {
context.report({
node,
messageId: 'invalidMethod',
data: {
message: makeErrorMessage(config.allowedMethods),
},
fix(fixer) {
return fixer.insertTextAfterRange(
[node.parts.at(-1).range[1], node.parts.at(-1).range[1]],
` method="${getFixedMethod(config)}"`
);
},
});
return;
}

// Check if it's a text value
if (methodAttribute.value && methodAttribute.value.type === 'GlimmerTextNode') {
const methodValue = methodAttribute.value.chars.toUpperCase();

if (!config.allowedMethods.includes(methodValue)) {
context.report({
node,
messageId: 'invalidMethod',
data: {
message: makeErrorMessage(config.allowedMethods),
},
fix(fixer) {
return fixer.replaceTextRange(
methodAttribute.value.range,
`"${getFixedMethod(config)}"`
);
},
});
}
}
// If it's a dynamic value (like {{foo}}), don't report
},
};
},
};
102 changes: 102 additions & 0 deletions tests/lib/rules/template-require-form-method.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
const rule = require('../../../lib/rules/template-require-form-method');
const RuleTester = require('eslint').RuleTester;

const DEFAULT_ERROR =
'All `<form>` elements should have `method` attribute with value of `POST,GET,DIALOG`';

const validHbs = [
{
options: [{ allowedMethods: ['get'] }],
code: '<form method="GET"></form>',
},
'<form method="POST"></form>',
'<form method="post"></form>',
'<form method="GET"></form>',
'<form method="get"></form>',
'<form method="DIALOG"></form>',
'<form method="dialog"></form>',
'<form method="{{formMethod}}"></form>',
'<form method={{formMethod}}></form>',
'<div/>',
'<div></div>',
'<div method="randomType"></div>',
];

const invalidHbs = [
{
options: [{ allowedMethods: ['get'] }],
code: '<form method="POST"></form>',
output: '<form method="GET"></form>',
errors: [
{ message: 'All `<form>` elements should have `method` attribute with value of `GET`' },
],
},
{
options: [{ allowedMethods: ['POST'] }],
code: '<form method="GET"></form>',
output: '<form method="POST"></form>',
errors: [
{ message: 'All `<form>` elements should have `method` attribute with value of `POST`' },
],
},
{
code: '<form></form>',
output: '<form method="POST"></form>',
errors: [{ message: DEFAULT_ERROR }],
},
{
code: '<form method=""></form>',
output: '<form method="POST"></form>',
errors: [{ message: DEFAULT_ERROR }],
},
{
code: '<form method=42></form>',
output: '<form method="POST"></form>',
errors: [{ message: DEFAULT_ERROR }],
},
{
code: '<form method=" ge t "></form>',
output: '<form method="POST"></form>',
errors: [{ message: DEFAULT_ERROR }],
},
{
code: '<form method=" pos t "></form>',
output: '<form method="POST"></form>',
errors: [{ message: DEFAULT_ERROR }],
},
];

function wrapTemplate(entry) {
if (typeof entry === 'string') {
return `<template>${entry}</template>`;
}

return {
...entry,
code: `<template>${entry.code}</template>`,
output: entry.output ? `<template>${entry.output}</template>` : entry.output,
};
}

const gjsRuleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser'),
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
});

gjsRuleTester.run('template-require-form-method', rule, {
valid: validHbs.map(wrapTemplate),
invalid: invalidHbs.map(wrapTemplate),
});

const hbsRuleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser/hbs'),
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
});

hbsRuleTester.run('template-require-form-method', rule, {
valid: validHbs,
invalid: invalidHbs,
});
Loading