diff --git a/README.md b/README.md index e2577ed5ee..c247d82d45 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,11 @@ You should also specify settings that will be shared across all the plugin rules "forbidExtraProps", {"property": "freeze", "object": "Object"}, {"property": "myFavoriteWrapper"} + ], + "linkComponents": [ + // Components used as alternatives to for linking, eg. + "Hyperlink", + {"name": "Link", "linkAttribute": "to"} ] } } diff --git a/docs/rules/jsx-no-target-blank.md b/docs/rules/jsx-no-target-blank.md index dd0cdb4c9b..a8bee1ac72 100644 --- a/docs/rules/jsx-no-target-blank.md +++ b/docs/rules/jsx-no-target-blank.md @@ -21,36 +21,59 @@ This rule aims to prevent user generated links from creating security vulnerabil * enabled: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0. * enforce: optional string, 'always' or 'never' -### always (default) +### `enforceDynamicLinks` + +#### always + `{"enforceDynamicLinks": "always"}` enforces the rule if the href is a dynamic link (default) When {"enforceDynamicLinks": "always"} is set, the following patterns are considered errors: ```jsx var Hello = -var Hello = +var Hello = ``` The following patterns are **not** considered errors: ```jsx -var Hello =

-var Hello = -var Hello = -var Hello = +var Hello =

+var Hello = +var Hello = +var Hello = var Hello = ``` -### never +#### never `{"enforceDynamicLinks": "never"}` does not enforce the rule if the href is a dynamic link When {"enforceDynamicLinks": "never"} is set, the following patterns are **not** considered errors: ```jsx -var Hello = +var Hello = +``` + +### Custom link components + +This rule supports the ability to use custom components for links, such as `` which is popular in libraries like `react-router`, `next.js` and `gatsby`. To enable this, define your custom link components in the global [shared settings](https://github.com/yannickcr/eslint-plugin-react/blob/master/README.md#configuration) under the `linkComponents` configuration area. Once configured, this rule will check those components as if they were `` elements. + +The following patterns are considered errors: + +```jsx +var Hello = +var Hello = +``` + +The following patterns are **not** considered errors: + +```jsx +var Hello = +var Hello = +var Hello = +var Hello = ``` ## When Not To Use It -If you do not have any external links, you can disable this rule \ No newline at end of file +If you do not have any external links, you can disable this rule diff --git a/lib/rules/jsx-no-target-blank.js b/lib/rules/jsx-no-target-blank.js index daee012551..07aae2fa1c 100644 --- a/lib/rules/jsx-no-target-blank.js +++ b/lib/rules/jsx-no-target-blank.js @@ -5,6 +5,7 @@ 'use strict'; const docsUrl = require('../util/docsUrl'); +const linkComponentsUtil = require('../util/linkComponents'); // ------------------------------------------------------------------------------ // Rule Definition @@ -18,16 +19,16 @@ function isTargetBlank(attr) { attr.value.value.toLowerCase() === '_blank'; } -function hasExternalLink(element) { +function hasExternalLink(element, linkAttribute) { return element.attributes.some(attr => attr.name && - attr.name.name === 'href' && + attr.name.name === linkAttribute && attr.value.type === 'Literal' && /^(?:\w+:|\/\/)/.test(attr.value.value)); } -function hasDynamicLink(element) { +function hasDynamicLink(element, linkAttribute) { return element.attributes.some(attr => attr.name && - attr.name.name === 'href' && + attr.name.name === linkAttribute && attr.value.type === 'JSXExpressionContainer'); } @@ -63,14 +64,17 @@ module.exports = { create: function(context) { const configuration = context.options[0] || {}; const enforceDynamicLinks = configuration.enforceDynamicLinks || 'always'; + const components = linkComponentsUtil.getLinkComponents(context); return { JSXAttribute: function(node) { - if (node.parent.name.name !== 'a' || !isTargetBlank(node) || hasSecureRel(node.parent)) { + if (!components.has(node.parent.name.name) || !isTargetBlank(node) || hasSecureRel(node.parent)) { return; } - if (hasExternalLink(node.parent) || (enforceDynamicLinks === 'always' && hasDynamicLink(node.parent))) { + const linkAttribute = components.get(node.parent.name.name); + + if (hasExternalLink(node.parent, linkAttribute) || (enforceDynamicLinks === 'always' && hasDynamicLink(node.parent, linkAttribute))) { context.report(node, 'Using target="_blank" without rel="noopener noreferrer" ' + 'is a security risk: see https://mathiasbynens.github.io/rel-noopener'); } diff --git a/lib/util/linkComponents.js b/lib/util/linkComponents.js new file mode 100644 index 0000000000..4abf502396 --- /dev/null +++ b/lib/util/linkComponents.js @@ -0,0 +1,21 @@ +/** + * @fileoverview Utility functions for propWrapperFunctions setting + */ +'use strict'; + +const DEFAULT_LINK_COMPONENTS = ['a']; +const DEFAULT_LINK_ATTRIBUTE = 'href'; + +function getLinkComponents(context) { + const settings = context.settings || {}; + return new Map(DEFAULT_LINK_COMPONENTS.concat(settings.linkComponents || []).map(value => { + if (typeof value === 'string') { + return [value, DEFAULT_LINK_ATTRIBUTE]; + } + return [value.name, value.linkAttribute]; + })); +} + +module.exports = { + getLinkComponents: getLinkComponents +}; diff --git a/tests/lib/rules/jsx-no-target-blank.js b/tests/lib/rules/jsx-no-target-blank.js index eb90cbd6e3..16ce255c55 100644 --- a/tests/lib/rules/jsx-no-target-blank.js +++ b/tests/lib/rules/jsx-no-target-blank.js @@ -47,6 +47,16 @@ ruleTester.run('jsx-no-target-blank', rule, { { code: '', options: [{enforceDynamicLinks: 'never'}] + }, + { + code: '', + options: [{enforceDynamicLinks: 'never'}], + settings: {linkComponents: ['Link']} + }, + { + code: '', + options: [{enforceDynamicLinks: 'never'}], + settings: {linkComponents: {name: 'Link', linkAttribute: 'to'}} } ], invalid: [{ @@ -83,5 +93,15 @@ ruleTester.run('jsx-no-target-blank', rule, { code: '', options: [{enforceDynamicLinks: 'always'}], errors: defaultErrors + }, { + code: '', + options: [{enforceDynamicLinks: 'always'}], + settings: {linkComponents: ['Link']}, + errors: defaultErrors + }, { + code: '', + options: [{enforceDynamicLinks: 'always'}], + settings: {linkComponents: {name: 'Link', linkAttribute: 'to'}}, + errors: defaultErrors }] }); diff --git a/tests/util/.eslintrc b/tests/util/.eslintrc new file mode 100644 index 0000000000..078e30c590 --- /dev/null +++ b/tests/util/.eslintrc @@ -0,0 +1,5 @@ +{ + "env": { + "mocha": true + } +} diff --git a/tests/util/linkComponents.js b/tests/util/linkComponents.js new file mode 100644 index 0000000000..bbfe82e798 --- /dev/null +++ b/tests/util/linkComponents.js @@ -0,0 +1,35 @@ +'use strict'; + +const assert = require('assert'); +const linkComponentsUtil = require('../../lib/util/linkComponents'); + +describe('linkComponentsFunctions', () => { + describe('getLinkComponents', () => { + it('returns a default map of components', () => { + const context = {}; + assert.deepStrictEqual(linkComponentsUtil.getLinkComponents(context), new Map([ + ['a', 'href'] + ])); + }); + + it('returns a map of components', () => { + const linkComponents = [ + 'Hyperlink', + { + name: 'Link', + linkAttribute: 'to' + } + ]; + const context = { + settings: { + linkComponents: linkComponents + } + }; + assert.deepStrictEqual(linkComponentsUtil.getLinkComponents(context), new Map([ + ['a', 'href'], + ['Hyperlink', 'href'], + ['Link', 'to'] + ])); + }); + }); +}); diff --git a/tests/util/propWrapper.js b/tests/util/propWrapper.js index 6cbef977ff..98fe3ea804 100644 --- a/tests/util/propWrapper.js +++ b/tests/util/propWrapper.js @@ -1,4 +1,3 @@ -/* eslint-env mocha */ 'use strict'; const assert = require('assert'); diff --git a/tests/util/version.js b/tests/util/version.js index f1550a94bb..65336bcf2f 100644 --- a/tests/util/version.js +++ b/tests/util/version.js @@ -1,4 +1,3 @@ -/* eslint-env mocha */ 'use strict'; const path = require('path');