forked from rubocop/rubocop-rails
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlink_to_blank.rb
100 lines (84 loc) · 3.18 KB
/
link_to_blank.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# frozen_string_literal: true
module RuboCop
module Cop
module Rails
# This cop checks for calls to `link_to` that contain a
# `target: '_blank'` but no `rel: 'noopener'`. This can be a security
# risk as the loaded page will have control over the previous page
# and could change its location for phishing purposes.
#
# The option `rel: 'noreferrer'` also blocks this behavior
# and removes the http-referrer header.
#
# @example
# # bad
# link_to 'Click here', url, target: '_blank'
#
# # good
# link_to 'Click here', url, target: '_blank', rel: 'noopener'
#
# # good
# link_to 'Click here', url, target: '_blank', rel: 'noreferrer'
class LinkToBlank < Base
extend AutoCorrector
MSG = 'Specify a `:rel` option containing noopener.'
RESTRICT_ON_SEND = %i[link_to].freeze
def_node_matcher :blank_target?, <<~PATTERN
(pair {(sym :target) (str "target")} {(str "_blank") (sym :_blank)})
PATTERN
def_node_matcher :includes_noopener?, <<~PATTERN
(pair {(sym :rel) (str "rel")} ({str sym} #contains_noopener?))
PATTERN
def_node_matcher :rel_node?, <<~PATTERN
(pair {(sym :rel) (str "rel")} (str _))
PATTERN
def on_send(node)
option_nodes = node.each_child_node(:hash)
option_nodes.map(&:children).each do |options|
blank = options.find { |o| blank_target?(o) }
next unless blank && options.none? { |o| includes_noopener?(o) }
add_offense(blank) do |corrector|
autocorrect(corrector, node, blank, option_nodes)
end
end
end
private
def autocorrect(corrector, send_node, node, option_nodes)
rel_node = nil
option_nodes.map(&:children).each do |options|
rel_node ||= options.find { |o| rel_node?(o) }
end
if rel_node
append_to_rel(rel_node, corrector)
else
add_rel(send_node, node, corrector)
end
end
def append_to_rel(rel_node, corrector)
existing_rel = rel_node.children.last.value
str_range = rel_node.children.last.loc.expression.adjust(
begin_pos: 1,
end_pos: -1
)
corrector.replace(str_range, "#{existing_rel} noopener")
end
def add_rel(send_node, offence_node, corrector)
opening_quote = offence_node.children.last.source[0]
closing_quote = opening_quote == ':' ? '' : opening_quote
new_rel_exp = ", rel: #{opening_quote}noopener#{closing_quote}"
range = if (last_argument = send_node.last_argument).hash_type?
last_argument.pairs.last.source_range
else
last_argument.source_range
end
corrector.insert_after(range, new_rel_exp)
end
def contains_noopener?(value)
return false unless value
rel_array = value.to_s.split
rel_array.include?('noopener') || rel_array.include?('noreferrer')
end
end
end
end
end