GitLab Commit is coming up on August 3-4. Learn how to innovate together using GitLab, the DevOps platform. Register for free: gitlabcommitvirtual2021.com

Commit d8762060 authored by David Sveningsson's avatar David Sveningsson
Browse files

feat(rules): new rule allowed-links

parent f251a361
Pipeline #182395040 passed with stages
in 9 minutes and 31 seconds
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/allowed-links.md inline validation: absolute-invalid 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 10,
"context": "absolute",
"line": 1,
"message": "Link destination must not be absolute url",
"offset": 9,
"ruleId": "allowed-links",
"selector": "a",
"severity": 2,
"size": 4,
},
],
"source": "<a href=\\"/foo\\">",
"warningCount": 0,
},
]
`;
exports[`docs/rules/allowed-links.md inline validation: absolute-valid 1`] = `Array []`;
exports[`docs/rules/allowed-links.md inline validation: base-invalid 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 10,
"context": "relative-base",
"line": 1,
"message": "Relative links must be relative to current folder",
"offset": 9,
"ruleId": "allowed-links",
"selector": "a",
"severity": 2,
"size": 3,
},
],
"source": "<a href=\\"foo\\">",
"warningCount": 0,
},
]
`;
exports[`docs/rules/allowed-links.md inline validation: base-valid 1`] = `Array []`;
exports[`docs/rules/allowed-links.md inline validation: external-invalid 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 10,
"context": "external",
"line": 1,
"message": "Link destination must not be external url",
"offset": 9,
"ruleId": "allowed-links",
"selector": "a",
"severity": 2,
"size": 22,
},
],
"source": "<a href=\\"http://example.net/foo\\">",
"warningCount": 0,
},
]
`;
exports[`docs/rules/allowed-links.md inline validation: external-valid 1`] = `Array []`;
exports[`docs/rules/allowed-links.md inline validation: relative-invalid 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 10,
"context": "relative-path",
"line": 1,
"message": "Link destination must not be relative url",
"offset": 9,
"ruleId": "allowed-links",
"selector": "a",
"severity": 2,
"size": 6,
},
],
"source": "<a href=\\"../foo\\">",
"warningCount": 0,
},
]
`;
exports[`docs/rules/allowed-links.md inline validation: relative-valid 1`] = `Array []`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["external-invalid"] = `<a href="http://example.net/foo">`;
markup["external-valid"] = `<a href="./foo">`;
markup["relative-invalid"] = `<a href="../foo">`;
markup["relative-valid"] = `<a href="/foo">`;
markup["absolute-invalid"] = `<a href="/foo">`;
markup["absolute-valid"] = `<a href="../foo">`;
markup["base-invalid"] = `<a href="foo">`;
markup["base-valid"] = `<a href="./foo">`;
describe("docs/rules/allowed-links.md", () => {
it("inline validation: external-invalid", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"allowed-links":["error",{"allowExternal":false}]}});
const report = htmlvalidate.validateString(markup["external-invalid"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: external-valid", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"allowed-links":["error",{"allowExternal":false}]}});
const report = htmlvalidate.validateString(markup["external-valid"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: relative-invalid", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"allowed-links":["error",{"allowRelative":false}]}});
const report = htmlvalidate.validateString(markup["relative-invalid"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: relative-valid", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"allowed-links":["error",{"allowRelative":false}]}});
const report = htmlvalidate.validateString(markup["relative-valid"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: absolute-invalid", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"allowed-links":["error",{"allowAbsolute":false}]}});
const report = htmlvalidate.validateString(markup["absolute-invalid"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: absolute-valid", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"allowed-links":["error",{"allowAbsolute":false}]}});
const report = htmlvalidate.validateString(markup["absolute-valid"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: base-invalid", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"allowed-links":["error",{"allowBase":false}]}});
const report = htmlvalidate.validateString(markup["base-invalid"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: base-valid", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"allowed-links":["error",{"allowBase":false}]}});
const report = htmlvalidate.validateString(markup["base-valid"]);
expect(report.results).toMatchSnapshot();
});
});
---
docType: rule
name: allowed-links
category: document
summary: Disallow link types
---
# Disallows link types (`allowed-links`)
This rules checks the link destination and disallows certain categories of links:
- External links
- Relative paths
- Relative to document base url
The rule checks links from:
- `<a href=""></a>`
- `<img src="..">`
- `<link src="..">`
- `<script src=".."></script>`
Anchor links are ignored by this rule.
## Rule details
This rules requires additional configuration to yield errors.
By default all links are allowed even when this rule is enabled.
## Options
This rule takes an optional object:
```json
{
"allowExternal": true,
"allowRelative": true,
"allowAbsolute": true,
"allowBase": true
}
```
### `allowExternal`
By setting `allowExternal` to `false` any link to a external resource will be disallowed.
<validate name="external-invalid" rules="allowed-links" allowed-links='{"allowExternal": false}'>
<a href="http://example.net/foo">
</validate>
<validate name="external-valid" rules="allowed-links" allowed-links='{"allowExternal": false}'>
<a href="./foo">
</validate>
### `allowRelative`
By setting `allowRelative` to `false` any link with a relative url will be disallowed.
<validate name="relative-invalid" rules="allowed-links" allowed-links='{"allowRelative": false}'>
<a href="../foo">
</validate>
<validate name="relative-valid" rules="allowed-links" allowed-links='{"allowRelative": false}'>
<a href="/foo">
</validate>
### `allowAbsolute`
By setting `allowAbsolute` to `false` any link with a absolute url will be disallowed.
<validate name="absolute-invalid" rules="allowed-links" allowed-links='{"allowAbsolute": false}'>
<a href="/foo">
</validate>
<validate name="absolute-valid" rules="allowed-links" allowed-links='{"allowAbsolute": false}'>
<a href="../foo">
</validate>
### `allowBase`
By setting `allowBase` to `false` relative urls can be used only if using an explicit path but not when relative to document base url.
This is useful when wanting to use relative urls but not rely on `<base href="..">` being set correctly.
Effectively this also means that links to files in the same folder must use `./target` even if `target` is valid.
<validate name="base-invalid" rules="allowed-links" allowed-links='{"allowBase": false}'>
<a href="foo">
</validate>
<validate name="base-valid" rules="allowed-links" allowed-links='{"allowBase": false}'>
<a href="./foo">
</validate>
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule allowed-links should contain contextual documentation absolute 1`] = `
Object {
"description": "Absolute links are not allowed by current configuration.",
"url": "https://html-validate.org/rules/allowed-links.html",
}
`;
exports[`rule allowed-links should contain contextual documentation external 1`] = `
Object {
"description": "External links are not allowed by current configuration.",
"url": "https://html-validate.org/rules/allowed-links.html",
}
`;
exports[`rule allowed-links should contain contextual documentation relative to base 1`] = `
Object {
"description": "Links relative to <base> are not allowed by current configuration.",
"url": "https://html-validate.org/rules/allowed-links.html",
}
`;
exports[`rule allowed-links should contain contextual documentation relative to path 1`] = `
Object {
"description": "Relative links are not allowed by current configuration.",
"url": "https://html-validate.org/rules/allowed-links.html",
}
`;
exports[`rule allowed-links should contain documentation 1`] = `
Object {
"description": "This link type is not allowed by current configuration",
"url": "https://html-validate.org/rules/allowed-links.html",
}
`;
import HtmlValidate from "../htmlvalidate";
import "../matchers";
import { processAttribute } from "../transform/mocks/attribute";
import { Style } from "./allowed-links";
describe("rule allowed-links", () => {
let htmlvalidate: HtmlValidate;
describe("should report error for", () => {
it.each`
description | markup
${"<a href>"} | ${'<a href="/foo"></a>'}
${"<img src>"} | ${'<img src="/foo">'}
${"<link href>"} | ${'<link href="/foo">'}
${"<script src>"} | ${'<script src="/foo"></script>'}
`("$description", ({ markup }) => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
rules: { "allowed-links": ["error", { allowAbsolute: false }] },
});
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
});
});
it("should not report error for anchor links", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
rules: { "allowed-links": ["error", { allowAbsolute: false }] },
});
const report = htmlvalidate.validateString('<a href="#foo"></a>');
expect(report).toBeValid();
});
it("should not report error for link is dynamic", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
rules: {
"allowed-links": ["error", { allowRelative: false }],
},
});
const report = htmlvalidate.validateString(
'<a dynamic-src="{{ expr }}"></a>',
null,
{
processAttribute,
}
);
expect(report).toBeValid();
});
describe("allowExternal: false", () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "allowed-links": ["error", { allowExternal: false }] },
});
});
it("should report error when link is external using //", () => {
expect.assertions(2);
const report = htmlvalidate.validateString(
'<a href="//example.net/foo"></a>'
);
expect(report).toBeInvalid();
expect(report).toHaveError(
"allowed-links",
"Link destination must not be external url"
);
});
it("should report error when link is external using protocol://", () => {
expect.assertions(2);
const report = htmlvalidate.validateString(
'<a href="http://example.net/foo"></a>'
);
expect(report).toBeInvalid();
expect(report).toHaveError(
"allowed-links",
"Link destination must not be external url"
);
});
it("should not report error link is absolute", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<a href="/foo"></a>');
expect(report).toBeValid();
});
it("should not report error link is relative to path", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<a href="./foo"></a>');
expect(report).toBeValid();
});
it("should not report error link is relative to base", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<a href="foo"></a>');
expect(report).toBeValid();
});
});
describe("allowRelative: false", () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "allowed-links": ["error", { allowRelative: false }] },
});
});
it("should not report error when link is external using //", () => {
expect.assertions(1);
const report = htmlvalidate.validateString(
'<a href="//example.net/foo"></a>'
);
expect(report).toBeValid();
});
it("should not report error when link is external using protocol://", () => {
expect.assertions(1);
const report = htmlvalidate.validateString(
'<a href="http://example.net/foo"></a>'
);
expect(report).toBeValid();
});
it("should not report error link is absolute", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<a href="/foo"></a>');
expect(report).toBeValid();
});
it("should report error link is relative to path", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<a href="./foo"></a>');
expect(report).toBeInvalid();
expect(report).toHaveError(
"allowed-links",
"Link destination must not be relative url"
);
});
it("should report error link is relative to base", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<a href="foo"></a>');
expect(report).toBeInvalid();
expect(report).toHaveError(
"allowed-links",
"Link destination must not be relative url"
);
});
});
describe("allowBase: false", () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "allowed-links": ["error", { allowBase: false }] },
});
});
it("should not report error when link is external using //", () => {
expect.assertions(1);
const report = htmlvalidate.validateString(
'<a href="//example.net/foo"></a>'
);
expect(report).toBeValid();
});
it("should not report error when link is external using protocol://", () => {
expect.assertions(1);
const report = htmlvalidate.validateString(
'<a href="http://example.net/foo"></a>'
);
expect(report).toBeValid();
});
it("should not report error link is absolute", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<a href="/foo"></a>');
expect(report).toBeValid();
});
it("should not report error link is relative to path", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<a href="./foo"></a>');
expect(report).toBeValid();
});
it("should report error link is relative to base", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<a href="foo"></a>');
expect(report).toBeInvalid();
expect(report).toHaveError(
"allowed-links",
"Relative links must be relative to current folder"
);
});
});
describe("allowAbsolute: false", () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "allowed-links": ["error", { allowAbsolute: false }] },
});
});
it("should not report error when link is external using //", () => {
expect.assertions(1);
const report = htmlvalidate.validateString(
'<a href="//example.net/foo"></a>'
);
expect(report).toBeValid();
});
it("should not report error when link is external using protocol://", () => {
expect.assertions(1);
const report = htmlvalidate.validateString(
'<a href="http://example.net/foo"></a>'
);
expect(report).toBeValid();
});
it("should report error link is absolute", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<a href="/foo"></a>');
expect(report).toBeInvalid();
expect(report).toHaveError(
"allowed-links",
"Link destination must not be absolute url"
);
});
it("should not report error link is relative to path", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<a href="./foo"></a>');
expect(report).toBeValid();
});
it("should report error link is relative to base", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<a href="foo"></a>');
expect(report).toBeValid();
});
});
it("should contain documentation", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
rules: { "allowed-links": "error" },
});
expect(
htmlvalidate.getRuleDocumentation("allowed-links")
).toMatchSnapshot();
});
describe("should contain contextual documentation", () => {
it.each`
style | value
${"external"} | ${Style.EXTERNAL}
${"relative to base"} | ${Style.RELATIVE_BASE}
${"relative to path"} | ${Style.RELATIVE_PATH}
${"absolute"} | ${Style.ABSOLUTE}
`("$style", ({ value }) => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
rules: { "allowed-links": "error" },
});
expect(
htmlvalidate.getRuleDocumentation("allowed-links", null, value)
).toMatchSnapshot();
});
});
});
import { DynamicValue } from "../dom";
import { AttributeEvent } from "../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
export const enum Style {
EXTERNAL = "external",
RELATIVE_BASE = "relative-base",
RELATIVE_PATH = "relative-path",
ABSOLUTE = "absolute",
ANCHOR = "anchor",
}
interface RuleOptions {
allowExternal: boolean;
allowRelative: boolean;
allowAbsolute: boolean;