@@ -12,22 +12,28 @@ const linkComponentsUtil = require('../util/linkComponents');
12
12
// Rule Definition
13
13
// ------------------------------------------------------------------------------
14
14
15
- function lastIndexMatching ( arr , condition ) {
16
- return arr . map ( condition ) . lastIndexOf ( true ) ;
15
+ function findLastIndex ( arr , condition ) {
16
+ for ( let i = arr . length - 1 ; i >= 0 ; i -= 1 ) {
17
+ if ( condition ( arr [ i ] ) ) {
18
+ return i ;
19
+ }
20
+ }
21
+
22
+ return - 1 ;
17
23
}
18
24
19
25
function attributeValuePossiblyBlank ( attribute ) {
20
- if ( ! attribute . value ) {
26
+ if ( ! attribute || ! attribute . value ) {
21
27
return false ;
22
28
}
23
29
const value = attribute . value ;
24
- if ( value . type === 'Literal' && typeof value . value === 'string' && value . value . toLowerCase ( ) === '_blank' ) {
25
- return true ;
30
+ if ( value . type === 'Literal' ) {
31
+ return typeof value . value === 'string' && value . value . toLowerCase ( ) === '_blank' ;
26
32
}
27
33
if ( value . type === 'JSXExpressionContainer' ) {
28
34
const expr = value . expression ;
29
- if ( expr . type === 'Literal' && typeof expr . value === 'string' && expr . value . toLowerCase ( ) === '_blank' ) {
30
- return true ;
35
+ if ( expr . type === 'Literal' ) {
36
+ return typeof expr . value === 'string' && expr . value . toLowerCase ( ) === '_blank' ;
31
37
}
32
38
if ( expr . type === 'ConditionalExpression' ) {
33
39
if ( expr . alternate . type === 'Literal' && expr . alternate . value && expr . alternate . value . toLowerCase ( ) === '_blank' ) {
@@ -41,21 +47,15 @@ function attributeValuePossiblyBlank(attribute) {
41
47
return false ;
42
48
}
43
49
44
- function hasTargetBlank ( node , warnOnSpreadAttributes , spreadAttributeIndex ) {
45
- const targetIndex = lastIndexMatching ( node . attributes , ( attr ) => attr . name && attr . name . name === 'target' ) ;
46
- const foundTargetBlank = targetIndex !== - 1 && attributeValuePossiblyBlank ( node . attributes [ targetIndex ] ) ;
47
- return foundTargetBlank || ( warnOnSpreadAttributes && targetIndex < spreadAttributeIndex ) ;
48
- }
49
-
50
50
function hasExternalLink ( node , linkAttribute , warnOnSpreadAttributes , spreadAttributeIndex ) {
51
- const linkIndex = lastIndexMatching ( node . attributes , ( attr ) => attr . name && attr . name . name === linkAttribute ) ;
51
+ const linkIndex = findLastIndex ( node . attributes , ( attr ) => attr . name && attr . name . name === linkAttribute ) ;
52
52
const foundExternalLink = linkIndex !== - 1 && ( ( attr ) => attr . value . type === 'Literal' && / ^ (?: \w + : | \/ \/ ) / . test ( attr . value . value ) ) (
53
53
node . attributes [ linkIndex ] ) ;
54
54
return foundExternalLink || ( warnOnSpreadAttributes && linkIndex < spreadAttributeIndex ) ;
55
55
}
56
56
57
57
function hasDynamicLink ( node , linkAttribute ) {
58
- const dynamicLinkIndex = lastIndexMatching ( node . attributes , ( attr ) => attr . name
58
+ const dynamicLinkIndex = findLastIndex ( node . attributes , ( attr ) => attr . name
59
59
&& attr . name . name === linkAttribute
60
60
&& attr . value
61
61
&& attr . value . type === 'JSXExpressionContainer' ) ;
@@ -64,29 +64,36 @@ function hasDynamicLink(node, linkAttribute) {
64
64
}
65
65
}
66
66
67
- function hasSecureRel ( node , allowReferrer , warnOnSpreadAttributes , spreadAttributeIndex ) {
68
- const relIndex = lastIndexMatching ( node . attributes , ( attr ) => ( attr . type === 'JSXAttribute' && attr . name . name === 'rel' ) ) ;
67
+ function getStringFromValue ( value ) {
68
+ if ( value ) {
69
+ if ( value . type === 'Literal' ) {
70
+ return value . value ;
71
+ }
72
+ if ( value . type === 'JSXExpressionContainer' ) {
73
+ if ( value . expression . type === 'TemplateLiteral' ) {
74
+ return value . expression . quasis [ 0 ] . value . cooked ;
75
+ }
76
+ return value . expression && value . expression . value ;
77
+ }
78
+ }
79
+ return null ;
80
+ }
69
81
82
+ function hasSecureRel ( node , allowReferrer , warnOnSpreadAttributes , spreadAttributeIndex ) {
83
+ const relIndex = findLastIndex ( node . attributes , ( attr ) => ( attr . type === 'JSXAttribute' && attr . name . name === 'rel' ) ) ;
70
84
if ( relIndex === - 1 || ( warnOnSpreadAttributes && relIndex < spreadAttributeIndex ) ) {
71
85
return false ;
72
86
}
73
87
74
88
const relAttribute = node . attributes [ relIndex ] ;
75
- const value = relAttribute . value
76
- && ( (
77
- relAttribute . value . type === 'Literal'
78
- && relAttribute . value . value
79
- ) || (
80
- relAttribute . value . type === 'JSXExpressionContainer'
81
- && relAttribute . value . expression
82
- && relAttribute . value . expression . value
83
- ) ) ;
89
+ const value = getStringFromValue ( relAttribute . value ) ;
84
90
const tags = value && typeof value === 'string' && value . toLowerCase ( ) . split ( ' ' ) ;
85
91
return tags && ( allowReferrer ? tags . indexOf ( 'noopener' ) >= 0 : tags . indexOf ( 'noreferrer' ) >= 0 ) ;
86
92
}
87
93
88
94
module . exports = {
89
95
meta : {
96
+ fixable : 'code' ,
90
97
docs : {
91
98
description : 'Forbid `target="_blank"` attribute without `rel="noreferrer"`' ,
92
99
category : 'Best Practices' ,
@@ -123,9 +130,17 @@ module.exports = {
123
130
return ;
124
131
}
125
132
126
- const spreadAttributeIndex = lastIndexMatching ( node . attributes , ( attr ) => ( attr . type === 'JSXSpreadAttribute' ) ) ;
127
- if ( ! hasTargetBlank ( node , warnOnSpreadAttributes , spreadAttributeIndex ) ) {
128
- return ;
133
+ const targetIndex = findLastIndex ( node . attributes , ( attr ) => attr . name && attr . name . name === 'target' ) ;
134
+ const spreadAttributeIndex = findLastIndex ( node . attributes , ( attr ) => ( attr . type === 'JSXSpreadAttribute' ) ) ;
135
+
136
+ if ( ! attributeValuePossiblyBlank ( node . attributes [ targetIndex ] ) ) {
137
+ const hasSpread = spreadAttributeIndex >= 0 ;
138
+
139
+ if ( warnOnSpreadAttributes && hasSpread ) {
140
+ // continue to check below
141
+ } else if ( ( hasSpread && targetIndex < spreadAttributeIndex ) || ! hasSpread ) {
142
+ return ;
143
+ }
129
144
}
130
145
131
146
const linkAttribute = components . get ( node . name . name ) ;
@@ -135,7 +150,48 @@ module.exports = {
135
150
context . report ( {
136
151
node,
137
152
message : 'Using target="_blank" without rel="noreferrer" '
138
- + 'is a security risk: see https://html.spec.whatwg.org/multipage/links.html#link-type-noopener'
153
+ + 'is a security risk: see https://html.spec.whatwg.org/multipage/links.html#link-type-noopener' ,
154
+ fix ( fixer ) {
155
+ // eslint 5 uses `node.attributes`; eslint 6+ uses `node.parent.attributes`
156
+ const nodeWithAttrs = node . parent . attributes ? node . parent : node ;
157
+ // eslint 5 does not provide a `name` property on JSXSpreadElements
158
+ const relAttribute = nodeWithAttrs . attributes . find ( ( attr ) => attr . name && attr . name . name === 'rel' ) ;
159
+
160
+ if ( targetIndex < spreadAttributeIndex || ( spreadAttributeIndex >= 0 && ! relAttribute ) ) {
161
+ return null ;
162
+ }
163
+
164
+ if ( ! relAttribute ) {
165
+ return fixer . insertTextAfter ( nodeWithAttrs . attributes . slice ( - 1 ) [ 0 ] , ' rel="noreferrer"' ) ;
166
+ }
167
+
168
+ if ( ! relAttribute . value ) {
169
+ return fixer . insertTextAfter ( relAttribute , '="noreferrer"' ) ;
170
+ }
171
+
172
+ if ( relAttribute . value . type === 'Literal' ) {
173
+ const parts = relAttribute . value . value
174
+ . split ( 'noreferrer' )
175
+ . filter ( Boolean ) ;
176
+ return fixer . replaceText ( relAttribute . value , `"${ parts . concat ( 'noreferrer' ) . join ( ' ' ) } "` ) ;
177
+ }
178
+
179
+ if ( relAttribute . value . type === 'JSXExpressionContainer' ) {
180
+ if ( relAttribute . value . expression . type === 'Literal' ) {
181
+ if ( typeof relAttribute . value . expression . value === 'string' ) {
182
+ const parts = relAttribute . value . expression . value
183
+ . split ( 'noreferrer' )
184
+ . filter ( Boolean ) ;
185
+ return fixer . replaceText ( relAttribute . value . expression , `"${ parts . concat ( 'noreferrer' ) . join ( ' ' ) } "` ) ;
186
+ }
187
+
188
+ // for undefined, boolean, number, symbol, bigint, and null
189
+ return fixer . replaceText ( relAttribute . value , '"noreferrer"' ) ;
190
+ }
191
+ }
192
+
193
+ return null ;
194
+ }
139
195
} ) ;
140
196
}
141
197
}
0 commit comments