diff --git a/.editorconfig b/.editorconfig index 9d38662e1..a45efe7af 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,12 +6,9 @@ root = true # Apply for all files [*] - charset = utf-8 - indent_style = space indent_size = 2 - end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore index 59ba3b556..5da7c1a4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,12 @@ # Files that might appear in the root of a volume .DocumentRevisions-V100 -.fseventsd .Spotlight-V100 .TemporaryItems .Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent # Editor directories and files .idea *.suo -*.ntvs* -*.njsproj *.sln # Generated files @@ -25,13 +20,9 @@ yarn-debug.log* yarn-error.log* # Runtime data -pids *.pid *.seed -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - # Coverage directory used by tools like nyc and istanbul .nyc_output coverage @@ -44,9 +35,6 @@ build/Release # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- node_modules -# Users Environment Variables -.lock-wscript - # macOS *.DS_Store .AppleDouble diff --git a/dist/core/rules/index.js b/dist/core/rules/index.js index aab57c99b..2a5f600a8 100644 --- a/dist/core/rules/index.js +++ b/dist/core/rules/index.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.titleRequire = exports.tagSelfClose = exports.tagsCheck = exports.tagPair = exports.tagnameSpecialChars = exports.tagnameLowercase = exports.tagNoObsolete = exports.styleDisabled = exports.srcNotEmpty = exports.specCharEscape = exports.spaceTabMixedDisabled = exports.scriptDisabled = exports.metaViewportRequire = exports.metaDescriptionRequire = exports.metaCharsetRequire = exports.mainRequire = exports.inputRequiresLabel = exports.inlineStyleDisabled = exports.inlineScriptDisabled = exports.idUnique = exports.idClassValue = exports.idClassAdDisabled = exports.htmlLangRequire = exports.hrefAbsOrRel = exports.headScriptDisabled = exports.h1Require = exports.frameTitleRequire = exports.emptyTagNotSelfClosed = exports.doctypeHTML5 = exports.doctypeFirst = exports.buttonTypeRequire = exports.attrWhitespace = exports.attrValueSingleQuotes = exports.attrValueNotEmpty = exports.attrValueDoubleQuotes = exports.attrUnsafeChars = exports.attrSort = exports.attrValueNoDuplication = exports.attrNoUnnecessaryWhitespace = exports.attrNoDuplication = exports.attrLowercase = exports.altRequire = void 0; +exports.titleRequire = exports.tagSelfClose = exports.tagsCheck = exports.tagPair = exports.tagnameSpecialChars = exports.tagnameLowercase = exports.tagNoObsolete = exports.styleDisabled = exports.srcNotEmpty = exports.specCharEscape = exports.spaceTabMixedDisabled = exports.scriptDisabled = exports.metaViewportRequire = exports.metaDescriptionRequire = exports.metaCharsetRequire = exports.mainRequire = exports.inputRequiresLabel = exports.inlineStyleDisabled = exports.inlineScriptDisabled = exports.idUnique = exports.idClassValue = exports.idClassAdDisabled = exports.htmlLangRequire = exports.hrefAbsOrRel = exports.headScriptDisabled = exports.h1Require = exports.frameTitleRequire = exports.formMethodRequire = exports.emptyTagNotSelfClosed = exports.doctypeHTML5 = exports.doctypeFirst = exports.buttonTypeRequire = exports.attrWhitespace = exports.attrValueSingleQuotes = exports.attrValueNotEmpty = exports.attrValueDoubleQuotes = exports.attrUnsafeChars = exports.attrSort = exports.attrValueNoDuplication = exports.attrNoUnnecessaryWhitespace = exports.attrNoDuplication = exports.attrLowercase = exports.altRequire = void 0; var alt_require_1 = require("./alt-require"); Object.defineProperty(exports, "altRequire", { enumerable: true, get: function () { return alt_require_1.default; } }); var attr_lowercase_1 = require("./attr-lowercase"); @@ -31,6 +31,8 @@ var doctype_html5_1 = require("./doctype-html5"); Object.defineProperty(exports, "doctypeHTML5", { enumerable: true, get: function () { return doctype_html5_1.default; } }); var empty_tag_not_self_closed_1 = require("./empty-tag-not-self-closed"); Object.defineProperty(exports, "emptyTagNotSelfClosed", { enumerable: true, get: function () { return empty_tag_not_self_closed_1.default; } }); +var form_method_require_1 = require("./form-method-require"); +Object.defineProperty(exports, "formMethodRequire", { enumerable: true, get: function () { return form_method_require_1.default; } }); var frame_title_require_1 = require("./frame-title-require"); Object.defineProperty(exports, "frameTitleRequire", { enumerable: true, get: function () { return frame_title_require_1.default; } }); var h1_require_1 = require("./h1-require"); @@ -85,4 +87,4 @@ var tag_self_close_1 = require("./tag-self-close"); Object.defineProperty(exports, "tagSelfClose", { enumerable: true, get: function () { return tag_self_close_1.default; } }); var title_require_1 = require("./title-require"); Object.defineProperty(exports, "titleRequire", { enumerable: true, get: function () { return title_require_1.default; } }); -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY29yZS9ydWxlcy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSw2Q0FBcUQ7QUFBNUMseUdBQUEsT0FBTyxPQUFjO0FBQzlCLG1EQUEyRDtBQUFsRCwrR0FBQSxPQUFPLE9BQWlCO0FBQ2pDLDZEQUFvRTtBQUEzRCx3SEFBQSxPQUFPLE9BQXFCO0FBQ3JDLG1GQUF5RjtBQUFoRiw2SUFBQSxPQUFPLE9BQStCO0FBQy9DLHlFQUErRTtBQUF0RSxtSUFBQSxPQUFPLE9BQTBCO0FBQzFDLDZDQUFtRDtBQUExQyx1R0FBQSxPQUFPLE9BQVk7QUFDNUIseURBQWdFO0FBQXZELG9IQUFBLE9BQU8sT0FBbUI7QUFDbkMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMsK0RBQXFFO0FBQTVELHlIQUFBLE9BQU8sT0FBcUI7QUFDckMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMscURBQTZEO0FBQXBELGlIQUFBLE9BQU8sT0FBa0I7QUFDbEMsNkRBQW9FO0FBQTNELHdIQUFBLE9BQU8sT0FBcUI7QUFDckMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0I7QUFDaEMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0I7QUFDaEMseUVBQThFO0FBQXJFLGtJQUFBLE9BQU8sT0FBeUI7QUFDekMsNkRBQW9FO0FBQTNELHdIQUFBLE9BQU8sT0FBcUI7QUFDckMsMkNBQW1EO0FBQTFDLHVHQUFBLE9BQU8sT0FBYTtBQUM3QiwrREFBc0U7QUFBN0QsMEhBQUEsT0FBTyxPQUFzQjtBQUN0QyxxREFBMkQ7QUFBbEQsK0dBQUEsT0FBTyxPQUFnQjtBQUNoQyx5REFBZ0U7QUFBdkQsb0hBQUEsT0FBTyxPQUFtQjtBQUNuQywrREFBcUU7QUFBNUQseUhBQUEsT0FBTyxPQUFxQjtBQUNyQyxtREFBMEQ7QUFBakQsOEdBQUEsT0FBTyxPQUFnQjtBQUNoQyx5Q0FBaUQ7QUFBeEMscUdBQUEsT0FBTyxPQUFZO0FBQzVCLG1FQUEwRTtBQUFqRSw4SEFBQSxPQUFPLE9BQXdCO0FBQ3hDLGlFQUF3RTtBQUEvRCw0SEFBQSxPQUFPLE9BQXVCO0FBQ3ZDLCtEQUFzRTtBQUE3RCwwSEFBQSxPQUFPLE9BQXNCO0FBQ3RDLCtDQUF1RDtBQUE5QywyR0FBQSxPQUFPLE9BQWU7QUFDL0IsK0RBQXNFO0FBQTdELDBIQUFBLE9BQU8sT0FBc0I7QUFDdEMsdUVBQThFO0FBQXJFLGtJQUFBLE9BQU8sT0FBMEI7QUFDMUMsaUVBQXdFO0FBQS9ELDRIQUFBLE9BQU8sT0FBdUI7QUFDdkMscURBQTZEO0FBQXBELGlIQUFBLE9BQU8sT0FBa0I7QUFDbEMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMsdURBQThEO0FBQXJELGtIQUFBLE9BQU8sT0FBa0I7QUFDbEMsaURBQXdEO0FBQS9DLDRHQUFBLE9BQU8sT0FBZTtBQUMvQixtREFBMkQ7QUFBbEQsK0dBQUEsT0FBTyxPQUFpQjtBQUNqQyxxREFBNEQ7QUFBbkQsZ0hBQUEsT0FBTyxPQUFpQjtBQUNqQyx5REFBaUU7QUFBeEQscUhBQUEsT0FBTyxPQUFvQjtBQUNwQywrREFBdUU7QUFBOUQsMkhBQUEsT0FBTyxPQUF1QjtBQUN2Qyx1Q0FBK0M7QUFBdEMsbUdBQUEsT0FBTyxPQUFXO0FBQzNCLDJDQUFtRDtBQUExQyx1R0FBQSxPQUFPLE9BQWE7QUFDN0IsbURBQTBEO0FBQWpELDhHQUFBLE9BQU8sT0FBZ0I7QUFDaEMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0IifQ== \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY29yZS9ydWxlcy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSw2Q0FBcUQ7QUFBNUMseUdBQUEsT0FBTyxPQUFjO0FBQzlCLG1EQUEyRDtBQUFsRCwrR0FBQSxPQUFPLE9BQWlCO0FBQ2pDLDZEQUFvRTtBQUEzRCx3SEFBQSxPQUFPLE9BQXFCO0FBQ3JDLG1GQUF5RjtBQUFoRiw2SUFBQSxPQUFPLE9BQStCO0FBQy9DLHlFQUErRTtBQUF0RSxtSUFBQSxPQUFPLE9BQTBCO0FBQzFDLDZDQUFtRDtBQUExQyx1R0FBQSxPQUFPLE9BQVk7QUFDNUIseURBQWdFO0FBQXZELG9IQUFBLE9BQU8sT0FBbUI7QUFDbkMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMsK0RBQXFFO0FBQTVELHlIQUFBLE9BQU8sT0FBcUI7QUFDckMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMscURBQTZEO0FBQXBELGlIQUFBLE9BQU8sT0FBa0I7QUFDbEMsNkRBQW9FO0FBQTNELHdIQUFBLE9BQU8sT0FBcUI7QUFDckMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0I7QUFDaEMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0I7QUFDaEMseUVBQThFO0FBQXJFLGtJQUFBLE9BQU8sT0FBeUI7QUFDekMsNkRBQW9FO0FBQTNELHdIQUFBLE9BQU8sT0FBcUI7QUFDckMsNkRBQW9FO0FBQTNELHdIQUFBLE9BQU8sT0FBcUI7QUFDckMsMkNBQW1EO0FBQTFDLHVHQUFBLE9BQU8sT0FBYTtBQUM3QiwrREFBc0U7QUFBN0QsMEhBQUEsT0FBTyxPQUFzQjtBQUN0QyxxREFBMkQ7QUFBbEQsK0dBQUEsT0FBTyxPQUFnQjtBQUNoQyx5REFBZ0U7QUFBdkQsb0hBQUEsT0FBTyxPQUFtQjtBQUNuQywrREFBcUU7QUFBNUQseUhBQUEsT0FBTyxPQUFxQjtBQUNyQyxtREFBMEQ7QUFBakQsOEdBQUEsT0FBTyxPQUFnQjtBQUNoQyx5Q0FBaUQ7QUFBeEMscUdBQUEsT0FBTyxPQUFZO0FBQzVCLG1FQUEwRTtBQUFqRSw4SEFBQSxPQUFPLE9BQXdCO0FBQ3hDLGlFQUF3RTtBQUEvRCw0SEFBQSxPQUFPLE9BQXVCO0FBQ3ZDLCtEQUFzRTtBQUE3RCwwSEFBQSxPQUFPLE9BQXNCO0FBQ3RDLCtDQUF1RDtBQUE5QywyR0FBQSxPQUFPLE9BQWU7QUFDL0IsK0RBQXNFO0FBQTdELDBIQUFBLE9BQU8sT0FBc0I7QUFDdEMsdUVBQThFO0FBQXJFLGtJQUFBLE9BQU8sT0FBMEI7QUFDMUMsaUVBQXdFO0FBQS9ELDRIQUFBLE9BQU8sT0FBdUI7QUFDdkMscURBQTZEO0FBQXBELGlIQUFBLE9BQU8sT0FBa0I7QUFDbEMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMsdURBQThEO0FBQXJELGtIQUFBLE9BQU8sT0FBa0I7QUFDbEMsaURBQXdEO0FBQS9DLDRHQUFBLE9BQU8sT0FBZTtBQUMvQixtREFBMkQ7QUFBbEQsK0dBQUEsT0FBTyxPQUFpQjtBQUNqQyxxREFBNEQ7QUFBbkQsZ0hBQUEsT0FBTyxPQUFpQjtBQUNqQyx5REFBaUU7QUFBeEQscUhBQUEsT0FBTyxPQUFvQjtBQUNwQywrREFBdUU7QUFBOUQsMkhBQUEsT0FBTyxPQUF1QjtBQUN2Qyx1Q0FBK0M7QUFBdEMsbUdBQUEsT0FBTyxPQUFXO0FBQzNCLDJDQUFtRDtBQUExQyx1R0FBQSxPQUFPLE9BQWE7QUFDN0IsbURBQTBEO0FBQWpELDhHQUFBLE9BQU8sT0FBZ0I7QUFDaEMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0IifQ== \ No newline at end of file diff --git a/src/core/rules/form-method-require.ts b/src/core/rules/form-method-require.ts new file mode 100644 index 000000000..7651695d6 --- /dev/null +++ b/src/core/rules/form-method-require.ts @@ -0,0 +1,45 @@ +import { Listener } from '../htmlparser' +import { Rule } from '../types' + +export default { + id: 'form-method-require', + description: + 'The method attribute of a
element must be present with a valid value: "get", "post", or "dialog".', + init(parser, reporter) { + const onTagStart: Listener = (event) => { + const tagName = event.tagName.toLowerCase() + + if (tagName === 'form') { + const mapAttrs = parser.getMapAttrs(event.attrs) + const col = event.col + tagName.length + 1 + + if (mapAttrs.method === undefined) { + reporter.warn( + 'The method attribute must be present on elements.', + event.line, + col, + this, + event.raw + ) + } else { + const methodValue = mapAttrs.method.toLowerCase() + if ( + methodValue !== 'get' && + methodValue !== 'post' && + methodValue !== 'dialog' + ) { + reporter.warn( + 'The method attribute of must have a valid value: "get", "post", or "dialog".', + event.line, + col, + this, + event.raw + ) + } + } + } + } + + parser.addListener('tagstart', onTagStart) + }, +} as Rule diff --git a/src/core/rules/index.ts b/src/core/rules/index.ts index d219031f7..4cd33ecef 100644 --- a/src/core/rules/index.ts +++ b/src/core/rules/index.ts @@ -13,6 +13,7 @@ export { default as buttonTypeRequire } from './button-type-require' export { default as doctypeFirst } from './doctype-first' export { default as doctypeHTML5 } from './doctype-html5' export { default as emptyTagNotSelfClosed } from './empty-tag-not-self-closed' +export { default as formMethodRequire } from './form-method-require' export { default as frameTitleRequire } from './frame-title-require' export { default as h1Require } from './h1-require' export { default as headScriptDisabled } from './head-script-disabled' diff --git a/src/core/types.ts b/src/core/types.ts index 72ddf22c6..377f2b098 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -22,6 +22,7 @@ export interface Ruleset { 'doctype-first'?: boolean 'doctype-html5'?: boolean 'empty-tag-not-self-closed'?: boolean + 'form-method-require'?: boolean 'head-script-disabled'?: boolean 'href-abs-or-rel'?: 'abs' | 'rel' 'id-class-ad-disabled'?: boolean diff --git a/test/rules/form-method-require.spec.js b/test/rules/form-method-require.spec.js new file mode 100644 index 000000000..a34993dce --- /dev/null +++ b/test/rules/form-method-require.spec.js @@ -0,0 +1,64 @@ +const HTMLHint = require('../../dist/htmlhint.js').HTMLHint + +const ruleId = 'form-method-require' +const ruleOptions = {} + +ruleOptions[ruleId] = true + +describe(`Rules: ${ruleId}`, () => { + it('Form with method="get" should not result in an error', () => { + const code = '
' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + }) + + it('Form with method="post" should not result in an error', () => { + const code = '
' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + }) + + it('Form with method="dialog" should not result in an error', () => { + const code = '
' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + }) + + it('Form without method attribute should result in an error', () => { + const code = '
' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(1) + expect(messages[0].rule.id).toBe(ruleId) + expect(messages[0].line).toBe(1) + expect(messages[0].col).toBe(6) + expect(messages[0].type).toBe('warning') + expect(messages[0].message).toBe( + 'The method attribute must be present on
elements.' + ) + }) + + it('Form with invalid method value should result in an error', () => { + const code = '
' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(1) + expect(messages[0].rule.id).toBe(ruleId) + expect(messages[0].line).toBe(1) + expect(messages[0].col).toBe(6) + expect(messages[0].type).toBe('warning') + expect(messages[0].message).toBe( + 'The method attribute of
must have a valid value: "get", "post", or "dialog".' + ) + }) + + it('Form with uppercase method value should not result in an error', () => { + const code = '
' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + }) + + it('Other elements should not be affected by this rule', () => { + const code = '
Not a form
' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + }) +}) diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 8c7bfb0e3..408ca846d 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -70,6 +70,8 @@ export default defineConfig({ attrs: { rel: 'manifest', href: '/site.webmanifest', + crossorigin: 'use-credentials', + fetchpriority: 'low', }, }, { diff --git a/website/src/content/docs/rules/form-method-require.mdx b/website/src/content/docs/rules/form-method-require.mdx new file mode 100644 index 000000000..706ca2be0 --- /dev/null +++ b/website/src/content/docs/rules/form-method-require.mdx @@ -0,0 +1,47 @@ +--- +id: form-method-require +title: form-method-require +description: Requires form elements to have a valid method attribute for better security and user experience. +sidebar: + badge: New + hidden: true +pagefind: false +--- + +import { Badge } from '@astrojs/starlight/components'; + +The method attribute of a `
` element must be present with a valid value: "get", "post", or "dialog". + +Level: + +## Config value + +- `true`: enable rule +- `false`: disable rule + +### The following patterns are **not** considered rule violations + +```html + +
+
+``` + +### The following patterns are considered rule violations + +```html +
No method specified
+
Invalid method
+``` + +## Why this rule is important + +The absence of the method attribute means the form will use the default `GET` method. With `GET`, form data is included in the URL (e.g., `?username=john&password=secret`), which can expose sensitive information in browser history, logs, or the network request. + +The HTML specification requires that form elements have one of three valid methods: + +- `get`: Appends form data to the URL (default, but not recommended for sensitive data) +- `post`: Sends form data in the request body (more secure for sensitive data) +- `dialog`: Used for dialog forms (HTML5 feature) + +This rule helps ensure that forms have explicit, valid methods for better security and user experience.