Skip to content

Commit

Permalink
feat: add prefer-https rule (#250)
Browse files Browse the repository at this point in the history
* feat: add prefer-https rule

* add tests

* add docs

* Update prefer-https.test.js

* update docs
  • Loading branch information
yeonjuan authored Dec 15, 2024
1 parent 22325ef commit 097360f
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
| [no-restricted-attrs](rules/no-restricted-attrs) | Disallow specified attributes | |
| [no-script-style-type](rules/no-script-style-type) | Enforce to omit type attributes for style sheets and scripts | 🔧 |
| [no-target-blank](rules/no-target-blank) | Disallow usage of unsafe `target='_blank'` | |
| [prefer-https](rules/prefer-https) | Prefer to use HTTPS for embedded resources | |
| [require-attrs](rules/require-attrs) | Require specified attributes | |
| [require-button-type](rules/require-button-type) | Require use of button element with a valid type attribute. | |
| [require-closing-tags](rules/require-closing-tags) | Require closing tags. | ⭐🔧 |
Expand Down
41 changes: 41 additions & 0 deletions docs/rules/prefer-https.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# prefer-https

This rule enforces to use `HTTPS` for embedded resources (image, media, style sheet and script).

## Why?

Using `HTTPS` instead of `HTTP` provides several advantages:

- Security: `HTTPS` encrypts data, protecting it from being intercepted or tampered with during transmission.
- SEO: Search engines prefer `HTTPS` websites, which can improve your site's ranking.
- User Trust: Visitors feel safer browsing a website that uses `HTTPS`.

## How to use

```js,.eslintrc.js
module.exports = {
rules: {
"@html-eslint/prefer-https": "error",
},
};
```

## Rule Details

Examples of **incorrect** code for this rule:

```html,incorrect
<script src="http://ajax.googleapis.com/ajax/libs/jquery/3.4.0/jquery.min.js"></script>
<img src="http://html-eslint.org/logo.svg">
<link rel="stylesheet" href="http://style.css">
```

Examples of **correct** code for this rule:

```html,correct
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.0/jquery.min.js"></script>
<img src="https://html-eslint.org/logo.svg">
<img src="/logo.svg">
<link rel="stylesheet" href="https://style.css">
<link rel="stylesheet" href="./style.css">
```
2 changes: 2 additions & 0 deletions packages/eslint-plugin/lib/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const noScriptStyleType = require("./no-script-style-type");
const lowercase = require("./lowercase");
const requireOpenGraphProtocol = require("./require-open-graph-protocol");
const sortAttrs = require("./sort-attrs");
const preferHttps = require("./prefer-https");

module.exports = {
"require-lang": requireLang,
Expand Down Expand Up @@ -78,4 +79,5 @@ module.exports = {
lowercase: lowercase,
"require-open-graph-protocol": requireOpenGraphProtocol,
"sort-attrs": sortAttrs,
"prefer-https": preferHttps,
};
106 changes: 106 additions & 0 deletions packages/eslint-plugin/lib/rules/prefer-https.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* @typedef { import("../types").RuleModule } RuleModule
* @typedef { import("../types").Tag } Tag
* @typedef { import("../types").ScriptTag } ScriptTag
* @typedef { import("../types").Attribute } Attribute
* @typedef { import("../types").AttributeValue } AttributeValue
*/

const { RULE_CATEGORY } = require("../constants");
const { findAttr, isScript } = require("./utils/node");
const { createVisitors } = require("./utils/visitors");

const MESSAGE_IDS = {
UNEXPECTED: "unexpected",
};

/**
* @param {string} url
*/
function getProtocol(url) {
try {
return new URL(url).protocol;
} catch (e) {
return null;
}
}

/**
* @param {Tag | ScriptTag} node
* @returns {AttributeValue | undefined}
*/
function getResourceAttributeValue(node) {
/**
* @type {Attribute | undefined}
*/
let attribute;
if (isScript(node)) {
attribute = findAttr(node, "src");
} else {
switch (node.name.toLowerCase()) {
case "img":
case "iframe":
case "audio":
case "video":
case "source":
case "embed": {
attribute = findAttr(node, "src");
break;
}
case "link": {
attribute = findAttr(node, "href");
break;
}
case "object": {
attribute = findAttr(node, "data");
break;
}
}
}
if (attribute) {
return attribute.value;
}
return undefined;
}

/**
* @type {RuleModule}
*/
module.exports = {
meta: {
type: "code",
docs: {
description: "Prefer to use HTTPS for embedded resources",
recommended: false,
category: RULE_CATEGORY.BEST_PRACTICE,
},
fixable: false,
schema: [],
messages: {
[MESSAGE_IDS.UNEXPECTED]: "Unexpected use 'HTTP' protocol.",
},
},

create(context) {
/**
* @param {Tag | ScriptTag} node
*/
function check(node) {
const attributeValue = getResourceAttributeValue(node);
if (attributeValue && !attributeValue.templates.length) {
const protocol = getProtocol(attributeValue.value);
if (protocol === "http:") {
context.report({
node: attributeValue,
messageId: MESSAGE_IDS.UNEXPECTED,
});
}
}
}

return createVisitors(context, {
ScriptTag: check,
Tag: check,
});
},
};
9 changes: 9 additions & 0 deletions packages/eslint-plugin/lib/rules/utils/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@ function isTag(node) {
return node.type === NODE_TYPES.Tag;
}

/**
* @param {AnyNode} node
* @returns {node is ScriptTag}
*/
function isScript(node) {
return node.type === NODE_TYPES.ScriptTag;
}

/**
* @param {AnyNode} node
* @returns {node is Comment}
Expand Down Expand Up @@ -223,6 +231,7 @@ module.exports = {
isComment,
isText,
isLine,
isScript,
isOverlapWithTemplates,
codeToLines,
isRangesOverlap,
Expand Down
89 changes: 89 additions & 0 deletions packages/eslint-plugin/tests/rules/prefer-https.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const createRuleTester = require("../rule-tester");
const rule = require("../../lib/rules/prefer-https");

const ruleTester = createRuleTester();
const templateRuleTester = createRuleTester("espree");

ruleTester.run("prefer-https", rule, {
valid: [
{
code: `<script></script>`,
},
{
code: `<script src="https://script.js">`,
},
{
code: `<img src="https://image.png">`,
},
{
code: `<iframe src="/absolute.html"></iframe>`,
},
{
code: `<audio src="relative.mp3"></iframe>`,
},
{
code: `<video src="./relative.mp3"></iframe>`,
},
{
code: `<link href="https://foo.css" rel="stylesheet" />`,
},
{
code: `<object type="application/pdf" data="/media/examples/In-CC0.pdf"></object>`,
},
],
invalid: [
{
code: `<script src="http://script.js">`,
errors: [
{
messageId: "unexpected",
},
],
},
{
code: `<img src="http://image.png">`,
errors: [
{
messageId: "unexpected",
},
],
},
{
code: `<link href="http://foo.css" rel="stylesheet" />`,
errors: [
{
messageId: "unexpected",
},
],
},
{
code: `<object type="application/pdf" data="http://media/examples/In-CC0.pdf"></object>`,
errors: [
{
messageId: "unexpected",
},
],
},
],
});

templateRuleTester.run("[template] prefer-https", rule, {
valid: [
{
code: "html`<script></script>`",
},
{
code: `html\`<img src="\${variableLink}">\``,
},
],
invalid: [
{
code: `html\`<script src="http://script.js">\``,
errors: [
{
messageId: "unexpected",
},
],
},
],
});

0 comments on commit 097360f

Please sign in to comment.