antisamy 0.0.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +6 -1
- data/lib/antisamy/css/css_filter.rb +187 -0
- data/lib/antisamy/css/css_scanner.rb +84 -0
- data/lib/antisamy/css/css_validator.rb +129 -0
- data/lib/antisamy/csspool/rsac/sac/conditions/attribute_condition.rb +50 -0
- data/lib/antisamy/csspool/rsac/sac/conditions/begin_hyphen_condition.rb +18 -0
- data/lib/antisamy/csspool/rsac/sac/conditions/class_condition.rb +18 -0
- data/lib/antisamy/csspool/rsac/sac/conditions/combinator_condition.rb +36 -0
- data/lib/antisamy/csspool/rsac/sac/conditions/condition.rb +29 -0
- data/lib/antisamy/csspool/rsac/sac/conditions/id_condition.rb +23 -0
- data/lib/antisamy/csspool/rsac/sac/conditions/one_of_condition.rb +18 -0
- data/lib/antisamy/csspool/rsac/sac/conditions/pseudo_class_condition.rb +20 -0
- data/lib/antisamy/csspool/rsac/sac/conditions.rb +5 -0
- data/lib/antisamy/csspool/rsac/sac/document_handler.rb +66 -0
- data/lib/antisamy/csspool/rsac/sac/error_handler.rb +13 -0
- data/lib/antisamy/csspool/rsac/sac/generated_parser.rb +1012 -0
- data/lib/antisamy/csspool/rsac/sac/generated_property_parser.rb +9284 -0
- data/lib/antisamy/csspool/rsac/sac/lexeme.rb +27 -0
- data/lib/antisamy/csspool/rsac/sac/lexical_unit.rb +201 -0
- data/lib/antisamy/csspool/rsac/sac/parse_exception.rb +4 -0
- data/lib/antisamy/csspool/rsac/sac/parser.rb +109 -0
- data/lib/antisamy/csspool/rsac/sac/property_parser.rb +44 -0
- data/lib/antisamy/csspool/rsac/sac/selectors/child_selector.rb +36 -0
- data/lib/antisamy/csspool/rsac/sac/selectors/conditional_selector.rb +45 -0
- data/lib/antisamy/csspool/rsac/sac/selectors/descendant_selector.rb +36 -0
- data/lib/antisamy/csspool/rsac/sac/selectors/element_selector.rb +35 -0
- data/lib/antisamy/csspool/rsac/sac/selectors/selector.rb +25 -0
- data/lib/antisamy/csspool/rsac/sac/selectors/sibling_selector.rb +35 -0
- data/lib/antisamy/csspool/rsac/sac/selectors/simple_selector.rb +21 -0
- data/lib/antisamy/csspool/rsac/sac/selectors.rb +5 -0
- data/lib/antisamy/csspool/rsac/sac/token.rb +25 -0
- data/lib/antisamy/csspool/rsac/sac/tokenizer.rb +185 -0
- data/lib/antisamy/csspool/rsac/sac.rb +14 -0
- data/lib/antisamy/csspool/rsac/stylesheet/rule.rb +20 -0
- data/lib/antisamy/csspool/rsac/stylesheet/stylesheet.rb +76 -0
- data/lib/antisamy/csspool/rsac/stylesheet.rb +3 -0
- data/lib/antisamy/csspool/rsac.rb +1 -0
- data/lib/antisamy/html/handler.rb +4 -0
- data/lib/antisamy/html/sax_filter.rb +49 -33
- data/lib/antisamy/html/scanner.rb +1 -43
- data/lib/antisamy/policy.rb +8 -3
- data/lib/antisamy/scan_results.rb +68 -0
- data/lib/antisamy.rb +4 -0
- data/spec/antisamy_spec.rb +111 -3
- metadata +39 -3
data/README.rdoc
CHANGED
@@ -5,7 +5,7 @@ user-supplier HTML and CSS. Please check out the AntiSamy project over at OWASP[
|
|
5
5
|
|
6
6
|
== TODO
|
7
7
|
|
8
|
-
*
|
8
|
+
* At some point support CSS3
|
9
9
|
|
10
10
|
== Synopsis
|
11
11
|
|
@@ -29,6 +29,11 @@ Please check policy-examples[https://github.com/washu/antisamy-ruby/tree/master/
|
|
29
29
|
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
30
30
|
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
31
31
|
|
32
|
+
== csspool
|
33
|
+
|
34
|
+
We use a forked version of csspool 0.2.6 within antysamy, you can find the license
|
35
|
+
for csspool in the rsac[https://github.com/washu/antisamy-ruby/tree/master/lib/antisamy/csspool] directory. csspool was re-namespaced to avoid any conflicts and updated for 1.9.2 ruby compatabilty
|
36
|
+
|
32
37
|
== Copyright
|
33
38
|
|
34
39
|
Copyright (c) 2011 Sal Scotto. See LICENSE.txt for
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'uri'
|
2
|
+
module AntiSamy
|
3
|
+
class CssFilter < RSAC::DocumentHandler
|
4
|
+
|
5
|
+
attr_accessor :clean, :errors, :style_sheets
|
6
|
+
|
7
|
+
def initialize(policy,tag)
|
8
|
+
@policy = policy
|
9
|
+
@validator = CssValidator.new(@policy)
|
10
|
+
@errors = []
|
11
|
+
@clean = ''
|
12
|
+
@tag = tag
|
13
|
+
@selector_open = false
|
14
|
+
@style_sheets = []
|
15
|
+
@inline = !tag.nil?
|
16
|
+
@media_open = false
|
17
|
+
@current_media = nil
|
18
|
+
end
|
19
|
+
# Start of document
|
20
|
+
def start_document(input_source) #:nodoc:
|
21
|
+
end
|
22
|
+
|
23
|
+
# Receive notification of the end of a style sheet.
|
24
|
+
def end_document(input_source) #:nodoc:
|
25
|
+
end
|
26
|
+
|
27
|
+
# Receive notification of a comment
|
28
|
+
def comment(text)
|
29
|
+
@errors << ScanMessage.new(ScanMessage::ERROR_COMMENT_REMOVED,@tag,text)
|
30
|
+
end
|
31
|
+
|
32
|
+
def error(exception)
|
33
|
+
#puts exception
|
34
|
+
end
|
35
|
+
|
36
|
+
def fatal_error(exception)
|
37
|
+
#puts exception
|
38
|
+
end
|
39
|
+
|
40
|
+
def error(t)
|
41
|
+
#puts t
|
42
|
+
end
|
43
|
+
|
44
|
+
def warn(t)
|
45
|
+
#puts t
|
46
|
+
end
|
47
|
+
|
48
|
+
def warning_error(exception)
|
49
|
+
#puts exception
|
50
|
+
end
|
51
|
+
# Receive notification of an unknown at rule not supported by this parser.
|
52
|
+
def ignorable_at_rule(at_rule)
|
53
|
+
if inline
|
54
|
+
@errors << ScanMessage.new(ScanMessage::ERROR_CSS_TAG_RULE_NOTFOUND,@tag,at_rule)
|
55
|
+
else
|
56
|
+
@errors << ScanMessage.new(ScanMessage::ERROR_STYLESHEET_RULE_NOTFOUND,@tag,at_rule)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Namespace declaration
|
61
|
+
def namespace_declaration(prefix, uri) #:nodoc:
|
62
|
+
end
|
63
|
+
|
64
|
+
# Called on an import statement
|
65
|
+
def import_style(uri, media, default_namespace_uri = nil)
|
66
|
+
# check directive
|
67
|
+
unless @policy.directive(Policy::EMBED_STYLESHEETS)
|
68
|
+
@errors << ScanMessage.new(ScanMessage::ERROR_CSS_IMPORT_DISABLED,@tag,uri)
|
69
|
+
return
|
70
|
+
end
|
71
|
+
# check for null uri
|
72
|
+
if uri.nil?
|
73
|
+
@errors << ScanMessage.new(ScanMessage::ERROR_CSS_IMPORT_URL_INVALID,@tag)
|
74
|
+
end
|
75
|
+
# check uri rules
|
76
|
+
begin
|
77
|
+
luri = RSAC::LexicalURI.new(uri)
|
78
|
+
link = URI.parse(luri.string_value)
|
79
|
+
link.normalize!
|
80
|
+
onsite = @policy.expression("offsiteURL")
|
81
|
+
offsite = @policy.expression("onsiteURL")
|
82
|
+
# bad uri
|
83
|
+
raise "Invalid URI Pattern" if link.to_s !~ onsite and link.to_s !~ offsite
|
84
|
+
raise "Invalid URI" unless link.absolute?
|
85
|
+
@style_sheets << link
|
86
|
+
rescue Exception => e
|
87
|
+
@errors << ScanMessage.new(ScanMessage::ERROR_CSS_IMPORT_URL_INVALID,@tag,uri)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Notification of the start of a media statement
|
92
|
+
def start_media(media)
|
93
|
+
@media_open = true
|
94
|
+
@current_media = media
|
95
|
+
end
|
96
|
+
|
97
|
+
# Notification of the end of a media statement
|
98
|
+
def end_media(media)
|
99
|
+
@media_open = false
|
100
|
+
@current_media = nil
|
101
|
+
end
|
102
|
+
|
103
|
+
# Notification of the start of a page statement
|
104
|
+
def start_page(name = nil, pseudo_page = nil) #:nodoc:
|
105
|
+
end
|
106
|
+
|
107
|
+
# Notification of the end of a page statement
|
108
|
+
def end_page(name = nil, pseudo_page = nil) # :nodoc:
|
109
|
+
end
|
110
|
+
|
111
|
+
# Notification of the beginning of a font face statement.
|
112
|
+
def start_font_face #:nodoc:
|
113
|
+
end
|
114
|
+
|
115
|
+
# Notification of the end of a font face statement.
|
116
|
+
def end_font_face #:nodoc:
|
117
|
+
end
|
118
|
+
|
119
|
+
# Notification of the beginning of a rule statement.
|
120
|
+
def start_selector(selectors)
|
121
|
+
count = 0
|
122
|
+
selectors.each do |s|
|
123
|
+
name = s.to_css
|
124
|
+
valid = false
|
125
|
+
begin
|
126
|
+
@validator.valid_selector?(name,s)
|
127
|
+
valid = true
|
128
|
+
rescue Exception => e
|
129
|
+
if @inline
|
130
|
+
@errors << ScanMessage.new(ScanMessage::ERROR_CSS_TAG_SELECTOR_NOTFOUND,@tag,name)
|
131
|
+
else
|
132
|
+
@errors << ScanMessage.new(ScanMessage::ERROR_STYLESHEET_SELECTOR_NOTFOUND,@tag,name)
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
if valid
|
137
|
+
if count > 0
|
138
|
+
clean << ", "
|
139
|
+
end
|
140
|
+
clean << name
|
141
|
+
count += 1
|
142
|
+
else
|
143
|
+
# not allowed selector
|
144
|
+
if @inline
|
145
|
+
@errors << ScanMessage.new(ScanMessage::ERROR_CSS_TAG_SELECTOR_DISALLOWED,@tag,name)
|
146
|
+
else
|
147
|
+
@errors << ScanMessage.new(ScanMessage::ERROR_STYLESHEET_SELECTOR_DISALLOWED,@tag,name)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
if count > 0
|
152
|
+
clean << " {\n"
|
153
|
+
@selector_open = true
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Notification of the end of a rule statement.
|
158
|
+
def end_selector(selectors)
|
159
|
+
if @selector_open
|
160
|
+
clean << "}\n"
|
161
|
+
end
|
162
|
+
@selector_open = false
|
163
|
+
end
|
164
|
+
|
165
|
+
# Notification of a declaration.
|
166
|
+
def property(name, value, important)
|
167
|
+
return unless @selector_open and @inline
|
168
|
+
if @validator.valid_property?(name,value)
|
169
|
+
clean << "\t" unless @inline
|
170
|
+
clean << "#{name}:"
|
171
|
+
value.each do |v|
|
172
|
+
clean << " #{v.to_s}"
|
173
|
+
end
|
174
|
+
clean << ";"
|
175
|
+
clean << "\n" unless @inline
|
176
|
+
else
|
177
|
+
cval = value.to_s
|
178
|
+
if @inline
|
179
|
+
@errors << ScanMessage.new(ScanMessage::ERROR_CSS_TAG_PROPERTY_INVALID,@tag,name,cval)
|
180
|
+
else
|
181
|
+
@errors << ScanMessage.new(ScanMessage::ERROR_STYLESHEET_PROPERTY_INVALID,@tag,name,cval)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
module AntiSamy
|
3
|
+
# Css Scanner class
|
4
|
+
class CssScanner
|
5
|
+
attr_accessor :policy, :errors
|
6
|
+
# Create a scanner with a given policy
|
7
|
+
def initialize(policy)
|
8
|
+
@policy = policy
|
9
|
+
@errors = []
|
10
|
+
end
|
11
|
+
# Scan the input using the provided input and output encoding
|
12
|
+
# will raise an error if nil input or the maximum input size is exceeded
|
13
|
+
def scan_inline(a_value,name,max_input)
|
14
|
+
return scan_sheet("#{name} { #{a_value} }",max_input,name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def scan_sheet(input,limit,tag = nil)
|
18
|
+
raise ArgumentError if input.nil?
|
19
|
+
raise ScanError, "Max input Exceeded #{input.size} > #{limit}" if input.size > limit
|
20
|
+
space_remaining = limit - input.size
|
21
|
+
# check poilcy stuff
|
22
|
+
if input =~ /^\s*<!\[CDATA\[(.*)\]\]>\s*$/
|
23
|
+
input = $1
|
24
|
+
end
|
25
|
+
# validator needs token sizes
|
26
|
+
filter = CssFilter.new(@policy,tag)
|
27
|
+
parser = RSAC::Parser.new(filter)
|
28
|
+
parser.error_handler = filter
|
29
|
+
parser.logger = filter
|
30
|
+
parser.parse(input)
|
31
|
+
# Populate the results
|
32
|
+
results = ScanResults.new(Time.now)
|
33
|
+
if @policy.directive(Policy::USE_XHTML)
|
34
|
+
result.clean_html = "<![CDATA[#{filter.clean}]]>"
|
35
|
+
else
|
36
|
+
results.clean_html = filter.clean
|
37
|
+
end
|
38
|
+
results.messages = filter.errors
|
39
|
+
# check for style sheets
|
40
|
+
sheets = filter.style_sheets
|
41
|
+
max_sheets = @policy.directive(Policy::MAX_SHEETS).to_i
|
42
|
+
max_sheets ||= 1
|
43
|
+
import_sheets = 0
|
44
|
+
if sheets.size > 0
|
45
|
+
timeout = 1000
|
46
|
+
if @policy.directive(Policy::CONN_TIMEOUT)
|
47
|
+
timeout = @policy.directive(Policy::CONN_TIMEOUT).to_i
|
48
|
+
end
|
49
|
+
timeout /= 1000
|
50
|
+
sheets.each do |sheet|
|
51
|
+
sheet_content = ''
|
52
|
+
begin
|
53
|
+
open(sheet,{:read_timeout => timeout}) do |f|
|
54
|
+
sheet_content = f.read(space_remaining)
|
55
|
+
end
|
56
|
+
space_remaining -= sheet_content.size
|
57
|
+
if import_sheets > max_sheets
|
58
|
+
# skip any remaing sheets if we exceeded the import count
|
59
|
+
results.messages << ScanMessage.new(ScanMessage::ERROR_CSS_IMPORT_EXCEEDED,"@import",sheet)
|
60
|
+
break;
|
61
|
+
end
|
62
|
+
|
63
|
+
if sheet_content.size > 0
|
64
|
+
#r = scan_sheet(sheet_content,space_remaining)
|
65
|
+
parser.parse(sheet_content)
|
66
|
+
#results.messages << r.messages
|
67
|
+
#results.messages.flatten!
|
68
|
+
import_sheets += 1
|
69
|
+
end
|
70
|
+
|
71
|
+
if space_remaining <= 0 or sheet_content.empty?
|
72
|
+
results.messages << ScanMessage.new(ScanMessage::ERROR_CSS_IMPORT_INPUT_SIZE,"@import",sheet)
|
73
|
+
break
|
74
|
+
end
|
75
|
+
rescue Exception => e
|
76
|
+
results.messages << ScanMessage.new(ScanMessage::ERROR_CSS_IMPORT_FAILURE,"@import",sheet)
|
77
|
+
end
|
78
|
+
# check the sheet rules
|
79
|
+
end
|
80
|
+
end
|
81
|
+
results
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module AntiSamy
|
2
|
+
class CssValidator
|
3
|
+
|
4
|
+
def initialize(policy)
|
5
|
+
@policy = policy
|
6
|
+
end
|
7
|
+
|
8
|
+
# Check to see if this selector is valid according to the policy
|
9
|
+
def valid_selector?(name,selector)
|
10
|
+
#puts selector.inspect
|
11
|
+
return false if selector.nil?
|
12
|
+
case selector.selector_type
|
13
|
+
when :SAC_CHILD_SELECTOR
|
14
|
+
return valid_selector?(name,selector.selector) && valid_selector?(name,selector.ancestor)
|
15
|
+
when :SAC_CONDITIONAL_SELECTOR
|
16
|
+
return valid_selector?(name,selector.selector) && valid_condition?(name,selector.condition)
|
17
|
+
when :SAC_DESCENDANT_SELECTOR
|
18
|
+
return valid_selector?(name,selector.selector) && valid_selector?(name,selector.ancestor)
|
19
|
+
when :SAC_ELEMENT_NODE_SELECTOR
|
20
|
+
return valid_simple_selector(selector)
|
21
|
+
when :SAC_DIRECT_ADJACENT_SELECTOR
|
22
|
+
return valid_selector?(name,selector.selector) && valid_selector?(name,selector.sibling)
|
23
|
+
when :SAC_ANY_NODE_SELECTOR
|
24
|
+
return valid_simple_selector(selector)
|
25
|
+
else
|
26
|
+
raise ScanError, name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Validate a simple selector
|
31
|
+
def valid_simple_selector(selector) #:nodoc:
|
32
|
+
valid = false
|
33
|
+
inclusion = @policy.expression("cssElementSelector")
|
34
|
+
exclusion = @policy.expression("cssElementExclusion")
|
35
|
+
begin
|
36
|
+
css = selector.to_css
|
37
|
+
valid = (css =~ inclusion) and (css !~ exclusion)
|
38
|
+
rescue Exception=> e
|
39
|
+
end
|
40
|
+
valid
|
41
|
+
end
|
42
|
+
|
43
|
+
# Check if a given condition is valid according to the policy
|
44
|
+
def valid_condition?(name,condition)
|
45
|
+
type = condition.condition_type
|
46
|
+
case type
|
47
|
+
when :SAC_AND_CONDITION
|
48
|
+
a = condition.first
|
49
|
+
b = condition.second
|
50
|
+
return valid_condition?(name,a) && valid_condition?(name,b)
|
51
|
+
when :SAC_CLASS_CONDITION
|
52
|
+
inclusion = @policy.expression("cssClassSelector")
|
53
|
+
exclusion = @policy.expression("cssClassExclusion")
|
54
|
+
return validate_condition(condition,inclusion,exclusion)
|
55
|
+
when :SAC_ID_CONDITION
|
56
|
+
inclusion = @policy.expression("cssIDSelector")
|
57
|
+
exclusion = @policy.expression("cssIDExclusion")
|
58
|
+
return validate_condition(condition,inclusion,exclusion)
|
59
|
+
when :SAC_PSEUDO_CLASS_CONDITION
|
60
|
+
inclusion = @policy.expression("cssPseudoElementSelector")
|
61
|
+
exclusion = @policy.expression("cssPsuedoElementExclusion")
|
62
|
+
return validate_condition(condition,inclusion,exclusion)
|
63
|
+
when :SAC_ONE_OF_ATTRIBUTE_CONDITION
|
64
|
+
inclusion = @policy.expression("cssAttributeSelector")
|
65
|
+
exclusion = @policy.expression("cssAttributeExclusion")
|
66
|
+
return validate_condition(condition,inclusion,exclusion)
|
67
|
+
when :SAC_ATTRIBUTE_CONDITION
|
68
|
+
inclusion = @policy.expression("cssAttributeSelector")
|
69
|
+
exclusion = @policy.expression("cssAttributeExclusion")
|
70
|
+
return validate_condition(condition,inclusion,exclusion)
|
71
|
+
when :SAC_BEGIN_HYPHEN_ATTRIBUTE_CONDITION
|
72
|
+
inclusion = @policy.expression("cssAttributeSelector")
|
73
|
+
exclusion = @policy.expression("cssAttributeExclusion")
|
74
|
+
return validate_condition(condition,inclusion,exclusion)
|
75
|
+
else
|
76
|
+
raise ScanError, name
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# validate the actual condition
|
81
|
+
def validate_condition(condition,inclusion,exclusion) #:nodoc:
|
82
|
+
valid = false
|
83
|
+
begin
|
84
|
+
css = condition.to_css
|
85
|
+
valid = (css =~ inclusion) and (css !~ exclusion)
|
86
|
+
rescue Exception=> e
|
87
|
+
end
|
88
|
+
valid
|
89
|
+
end
|
90
|
+
|
91
|
+
# Validate each property value according to teh policy
|
92
|
+
def valid_property?(name,value)
|
93
|
+
prop = @policy.property(name) unless name.nil?
|
94
|
+
return false if prop.nil?
|
95
|
+
value.each do |prop_value|
|
96
|
+
v = prop_value.string_value
|
97
|
+
return false unless validate_value(prop,v)
|
98
|
+
end
|
99
|
+
return true
|
100
|
+
end
|
101
|
+
|
102
|
+
# is this a valid property value
|
103
|
+
def validate_value(property,value) #:nodoc:
|
104
|
+
valid = false
|
105
|
+
# Check static strings
|
106
|
+
property.values.each do |al_val|
|
107
|
+
valid = true if al_val.downcase.eql?(value.downcase)
|
108
|
+
end
|
109
|
+
# Check regular expressions
|
110
|
+
unless valid
|
111
|
+
property.expressions.each do |xp_val|
|
112
|
+
valid = true if value =~ xp_value
|
113
|
+
end
|
114
|
+
end
|
115
|
+
# check short hand
|
116
|
+
unless valid
|
117
|
+
property.refs.each do |ref|
|
118
|
+
real = @policy.property(ref)
|
119
|
+
if real
|
120
|
+
valid = validate_value(real,value)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
# We will check media above.
|
125
|
+
return valid
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module RSAC
|
2
|
+
module Conditions
|
3
|
+
class AttributeCondition < Condition
|
4
|
+
attr_accessor :local_name, :value, :specified
|
5
|
+
alias :specified? :specified
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def build(name, raw)
|
9
|
+
condition, value = raw
|
10
|
+
case condition
|
11
|
+
when "~="
|
12
|
+
OneOfCondition.new(name, value)
|
13
|
+
when "|="
|
14
|
+
BeginHyphenCondition.new(name, value)
|
15
|
+
else
|
16
|
+
AttributeCondition.new(name, value, true)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(local_name, value, specified, condition_type=:SAC_ATTRIBUTE_CONDITION)
|
22
|
+
super(condition_type)
|
23
|
+
@local_name = local_name
|
24
|
+
@value = value
|
25
|
+
@specified = specified
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_css
|
29
|
+
"[#{local_name}#{value && "=#{value}"}]"
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_xpath
|
33
|
+
"[@#{local_name}#{value && "='#{value}'"}]"
|
34
|
+
end
|
35
|
+
|
36
|
+
def specificity
|
37
|
+
[0, 0, 1, 0]
|
38
|
+
end
|
39
|
+
|
40
|
+
def ==(other)
|
41
|
+
super && local_name == other.local_name && value == other.value &&
|
42
|
+
specified == other.specified
|
43
|
+
end
|
44
|
+
|
45
|
+
def hash
|
46
|
+
[local_name, value, specified].hash
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module RSAC
|
2
|
+
module Conditions
|
3
|
+
class BeginHyphenCondition < AttributeCondition
|
4
|
+
|
5
|
+
def initialize(local_name, value)
|
6
|
+
super(local_name, value, true, :SAC_BEGIN_HYPHEN_ATTRIBUTE_CONDITION)
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_css
|
10
|
+
"[#{local_name}|=#{value}]"
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_xpath
|
14
|
+
"[contains(@#{local_name}, '#{value}')]"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module RSAC
|
2
|
+
module Conditions
|
3
|
+
class ClassCondition < AttributeCondition
|
4
|
+
|
5
|
+
def initialize(klass)
|
6
|
+
super("class", klass, true, :SAC_CLASS_CONDITION)
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_css
|
10
|
+
".#{value}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_xpath
|
14
|
+
"[contains(@#{local_name}, '#{value}')]"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module RSAC
|
2
|
+
module Conditions
|
3
|
+
class CombinatorCondition < Condition
|
4
|
+
attr_accessor :first_condition, :second_condition
|
5
|
+
alias :first :first_condition
|
6
|
+
alias :second :second_condition
|
7
|
+
|
8
|
+
def initialize(first, second)
|
9
|
+
super(:SAC_AND_CONDITION)
|
10
|
+
|
11
|
+
@first_condition = first
|
12
|
+
@second_condition = second
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_css
|
16
|
+
"#{first.to_css}#{second.to_css}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_xpath
|
20
|
+
"#{first.to_xpath}#{second.to_xpath}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def specificity
|
24
|
+
first.specificity.zip(second.specificity).map { |x,y| x + y }
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
super && first == other.first && second == other.second
|
29
|
+
end
|
30
|
+
|
31
|
+
def hash
|
32
|
+
[first, second].hash
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module RSAC
|
2
|
+
module Conditions
|
3
|
+
class Condition
|
4
|
+
|
5
|
+
attr_accessor :condition_type
|
6
|
+
|
7
|
+
def initialize(condition_type)
|
8
|
+
@condition_type = condition_type
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
self.class === other && condition_type == other.condition_type
|
13
|
+
end
|
14
|
+
|
15
|
+
def hash
|
16
|
+
condition_type.hash
|
17
|
+
end
|
18
|
+
|
19
|
+
def eql?(other)
|
20
|
+
self == other
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_css
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module RSAC
|
2
|
+
module Conditions
|
3
|
+
class IDCondition < AttributeCondition
|
4
|
+
|
5
|
+
def initialize(id)
|
6
|
+
id = id[1..id.size] if id[0] == ?#
|
7
|
+
super("id", id, true, :SAC_ID_CONDITION)
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_css
|
11
|
+
"##{value}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_xpath
|
15
|
+
"[@id='#{value}']"
|
16
|
+
end
|
17
|
+
|
18
|
+
def specificity
|
19
|
+
[0, 1, 0, 0]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module RSAC
|
2
|
+
module Conditions
|
3
|
+
class OneOfCondition < AttributeCondition
|
4
|
+
|
5
|
+
def initialize(local_name, value)
|
6
|
+
super(local_name, value, true, :SAC_ONE_OF_ATTRIBUTE_CONDITION)
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_css
|
10
|
+
"[#{local_name}~=#{value}]"
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_xpath
|
14
|
+
"[contains(@#{local_name}, '#{value}')]"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module RSAC
|
2
|
+
module Conditions
|
3
|
+
class PseudoClassCondition < AttributeCondition
|
4
|
+
def initialize(pseudo_class)
|
5
|
+
super(nil, pseudo_class, false, :SAC_PSEUDO_CLASS_CONDITION)
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_css
|
9
|
+
":#{value}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_xpath
|
13
|
+
end
|
14
|
+
|
15
|
+
def specificity
|
16
|
+
[0, 0, 0, 0]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|