svg_conform 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/rake.yml +4 -1
- data/.github/workflows/release.yml +6 -2
- data/.rubocop_todo.yml +273 -10
- data/Gemfile +1 -0
- data/README.adoc +54 -37
- data/config/profiles/metanorma.yml +4 -4
- data/docs/remediation.adoc +541 -542
- data/docs/requirements.adoc +800 -357
- data/examples/readme_usage.rb +67 -0
- data/examples/requirements_demo.rb +4 -4
- data/lib/svg_conform/document.rb +7 -1
- data/lib/svg_conform/element_proxy.rb +101 -0
- data/lib/svg_conform/fast_document_analyzer.rb +82 -0
- data/lib/svg_conform/node_index_builder.rb +47 -0
- data/lib/svg_conform/remediations/no_external_css_remediation.rb +4 -4
- data/lib/svg_conform/requirements/allowed_elements_requirement.rb +202 -0
- data/lib/svg_conform/requirements/base_requirement.rb +27 -0
- data/lib/svg_conform/requirements/color_restrictions_requirement.rb +53 -0
- data/lib/svg_conform/requirements/font_family_requirement.rb +18 -0
- data/lib/svg_conform/requirements/forbidden_content_requirement.rb +26 -0
- data/lib/svg_conform/requirements/id_reference_requirement.rb +96 -0
- data/lib/svg_conform/requirements/invalid_id_references_requirement.rb +91 -0
- data/lib/svg_conform/requirements/link_validation_requirement.rb +30 -0
- data/lib/svg_conform/requirements/namespace_attributes_requirement.rb +59 -0
- data/lib/svg_conform/requirements/namespace_requirement.rb +74 -0
- data/lib/svg_conform/requirements/no_external_css_requirement.rb +74 -0
- data/lib/svg_conform/requirements/no_external_fonts_requirement.rb +58 -0
- data/lib/svg_conform/requirements/no_external_images_requirement.rb +40 -0
- data/lib/svg_conform/requirements/style_requirement.rb +12 -0
- data/lib/svg_conform/requirements/viewbox_required_requirement.rb +72 -0
- data/lib/svg_conform/sax_document.rb +46 -0
- data/lib/svg_conform/sax_validation_handler.rb +158 -0
- data/lib/svg_conform/validation_context.rb +84 -2
- data/lib/svg_conform/validator.rb +74 -6
- data/lib/svg_conform/version.rb +1 -1
- data/lib/svg_conform.rb +1 -0
- data/spec/fixtures/namespace/repair/basic_violations.svg +3 -3
- data/spec/fixtures/namespace_attributes/repair/basic_violations.svg +2 -2
- data/spec/fixtures/no_external_css/repair/basic_violations.svg +2 -2
- data/spec/fixtures/style_promotion/repair/basic_test.svg +2 -2
- data/svg_conform.gemspec +1 -1
- metadata +12 -6
|
@@ -46,6 +46,24 @@ module SvgConform
|
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
def validate_sax_element(element, context)
|
|
50
|
+
# Check font-family attribute only
|
|
51
|
+
font_family = element.raw_attributes["font-family"]
|
|
52
|
+
return unless font_family
|
|
53
|
+
|
|
54
|
+
if svgcheck_compatibility
|
|
55
|
+
check_font_family_svgcheck_mode(element, context, font_family, "font-family")
|
|
56
|
+
elsif !valid_font_family?(font_family)
|
|
57
|
+
context.add_error(
|
|
58
|
+
requirement_id: id,
|
|
59
|
+
message: "Font family '#{font_family}' is not allowed in this profile",
|
|
60
|
+
node: element,
|
|
61
|
+
severity: :error,
|
|
62
|
+
data: { attribute: "font-family", value: font_family }
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
49
67
|
private
|
|
50
68
|
|
|
51
69
|
def check_font_family_svgcheck_mode(node, context, font_family_value,
|
|
@@ -55,6 +55,32 @@ module SvgConform
|
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
|
+
|
|
59
|
+
def validate_sax_element(element, context)
|
|
60
|
+
# Check if this is a forbidden element
|
|
61
|
+
if forbidden_elements.include?(element.name)
|
|
62
|
+
context.add_error(
|
|
63
|
+
requirement_id: id,
|
|
64
|
+
message: "Forbidden element '#{element.name}' is not allowed",
|
|
65
|
+
node: element,
|
|
66
|
+
severity: :error
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Check for forbidden attributes
|
|
71
|
+
element.attributes.each do |attr|
|
|
72
|
+
attr_name = attr.name
|
|
73
|
+
|
|
74
|
+
if forbidden_attributes.include?(attr_name)
|
|
75
|
+
context.add_error(
|
|
76
|
+
requirement_id: id,
|
|
77
|
+
message: "Forbidden attribute '#{attr_name}' is not allowed",
|
|
78
|
+
node: element,
|
|
79
|
+
severity: :error
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
58
84
|
end
|
|
59
85
|
end
|
|
60
86
|
end
|
|
@@ -6,6 +6,102 @@ require "set"
|
|
|
6
6
|
module SvgConform
|
|
7
7
|
module Requirements
|
|
8
8
|
class IdReferenceRequirement < BaseRequirement
|
|
9
|
+
def needs_deferred_validation?
|
|
10
|
+
true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def collect_sax_data(element, context)
|
|
14
|
+
# Initialize collections on first call
|
|
15
|
+
@collected_ids ||= Set.new
|
|
16
|
+
@collected_url_refs ||= []
|
|
17
|
+
@collected_href_refs ||= []
|
|
18
|
+
@collected_other_refs ||= []
|
|
19
|
+
|
|
20
|
+
# Collect IDs
|
|
21
|
+
id_value = element.raw_attributes["id"]
|
|
22
|
+
@collected_ids.add(id_value) if id_value && !id_value.empty?
|
|
23
|
+
|
|
24
|
+
# Collect url() references
|
|
25
|
+
url_attributes = %w[fill stroke marker-start marker-mid marker-end clip-path mask filter]
|
|
26
|
+
url_attributes.each do |attr_name|
|
|
27
|
+
attr_value = element.raw_attributes[attr_name]
|
|
28
|
+
next unless attr_value
|
|
29
|
+
|
|
30
|
+
url_refs = extract_url_references(attr_value)
|
|
31
|
+
url_refs.each do |ref_id|
|
|
32
|
+
@collected_url_refs << [element, ref_id, attr_name]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Check style attribute for url() references
|
|
37
|
+
style_attr = element.raw_attributes["style"]
|
|
38
|
+
if style_attr
|
|
39
|
+
url_refs = extract_url_references(style_attr)
|
|
40
|
+
url_refs.each do |ref_id|
|
|
41
|
+
@collected_url_refs << [element, ref_id, "style"]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Collect href references
|
|
46
|
+
href_value = element.raw_attributes["href"] || element.raw_attributes["xlink:href"]
|
|
47
|
+
if href_value&.start_with?("#")
|
|
48
|
+
ref_id = href_value[1..] # Remove #
|
|
49
|
+
@collected_href_refs << [element, ref_id]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Collect other ID references
|
|
53
|
+
id_ref_attributes = %w[for aria-labelledby aria-describedby aria-controls aria-owns]
|
|
54
|
+
id_ref_attributes.each do |attr_name|
|
|
55
|
+
attr_value = element.raw_attributes[attr_name]
|
|
56
|
+
next unless attr_value
|
|
57
|
+
|
|
58
|
+
ref_ids = attr_value.split(/\s+/)
|
|
59
|
+
ref_ids.each do |ref_id|
|
|
60
|
+
next if ref_id.empty?
|
|
61
|
+
@collected_other_refs << [element, ref_id, attr_name]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate_sax_complete(context)
|
|
67
|
+
# Validate all collected references
|
|
68
|
+
@collected_url_refs.each do |element, ref_id, attr_name|
|
|
69
|
+
next if @collected_ids.include?(ref_id)
|
|
70
|
+
|
|
71
|
+
message = if attr_name == "style"
|
|
72
|
+
"Reference to undefined ID '#{ref_id}' in style attribute"
|
|
73
|
+
else
|
|
74
|
+
"Reference to undefined ID '#{ref_id}' in attribute '#{attr_name}'"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
context.add_error(
|
|
78
|
+
node: element,
|
|
79
|
+
message: message,
|
|
80
|
+
requirement_id: id
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
@collected_href_refs.each do |element, ref_id|
|
|
85
|
+
next if @collected_ids.include?(ref_id)
|
|
86
|
+
|
|
87
|
+
context.add_error(
|
|
88
|
+
node: element,
|
|
89
|
+
message: "Reference to undefined ID '#{ref_id}' in href attribute",
|
|
90
|
+
requirement_id: id
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
@collected_other_refs.each do |element, ref_id, attr_name|
|
|
95
|
+
next if @collected_ids.include?(ref_id)
|
|
96
|
+
|
|
97
|
+
context.add_error(
|
|
98
|
+
node: element,
|
|
99
|
+
message: "Reference to undefined ID '#{ref_id}' in #{attr_name} attribute",
|
|
100
|
+
requirement_id: id
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
9
105
|
def validate_document(document, context)
|
|
10
106
|
# Collect all IDs in the document
|
|
11
107
|
ids = Set.new
|
|
@@ -21,6 +21,97 @@ module SvgConform
|
|
|
21
21
|
map "strict_mode", to: :strict_mode
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
def initialize(*args)
|
|
25
|
+
super
|
|
26
|
+
@collected_ids = Set.new
|
|
27
|
+
@use_element_refs = [] # [element, ref_id, href]
|
|
28
|
+
@other_refs = [] # [element, ref_id, attr_name, value]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def needs_deferred_validation?
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def collect_sax_data(element, context)
|
|
36
|
+
# Initialize collections on first call
|
|
37
|
+
@collected_ids ||= Set.new
|
|
38
|
+
@use_element_refs ||= []
|
|
39
|
+
@other_refs ||= []
|
|
40
|
+
|
|
41
|
+
# Collect IDs
|
|
42
|
+
id_attr = element.raw_attributes["id"]
|
|
43
|
+
@collected_ids.add(id_attr) if id_attr && !id_attr.empty?
|
|
44
|
+
|
|
45
|
+
# Collect use element references
|
|
46
|
+
if check_use_elements && element.name == "use"
|
|
47
|
+
href = element.raw_attributes["xlink:href"] || element.raw_attributes["href"]
|
|
48
|
+
if href&.start_with?("#")
|
|
49
|
+
ref_id = href[1..]
|
|
50
|
+
@use_element_refs << [element, ref_id, href] unless ref_id.empty?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Collect other ID references if enabled
|
|
55
|
+
if check_other_references
|
|
56
|
+
id_reference_attributes = %w[clip-path mask filter marker-start marker-mid marker-end fill stroke]
|
|
57
|
+
|
|
58
|
+
id_reference_attributes.each do |attr_name|
|
|
59
|
+
attr_value = element.raw_attributes[attr_name]
|
|
60
|
+
next unless attr_value&.match?(/^url\(#(.+)\)$/)
|
|
61
|
+
|
|
62
|
+
ref_id = Regexp.last_match(1)
|
|
63
|
+
@other_refs << [element, ref_id, attr_name, attr_value]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Check style attribute
|
|
67
|
+
style_value = element.raw_attributes["style"]
|
|
68
|
+
if style_value
|
|
69
|
+
styles = parse_style(style_value)
|
|
70
|
+
styles.each do |property, value|
|
|
71
|
+
next unless value&.match?(/^url\(#(.+)\)$/)
|
|
72
|
+
|
|
73
|
+
ref_id = Regexp.last_match(1)
|
|
74
|
+
@other_refs << [element, ref_id, "style:#{property}", value]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def validate_sax_complete(context)
|
|
81
|
+
# Validate use element references
|
|
82
|
+
@use_element_refs.each do |element, ref_id, href|
|
|
83
|
+
next if @collected_ids.include?(ref_id)
|
|
84
|
+
|
|
85
|
+
context.add_error(
|
|
86
|
+
requirement_id: id,
|
|
87
|
+
node: element,
|
|
88
|
+
message: "use element references non-existent ID: #{ref_id}",
|
|
89
|
+
severity: :error,
|
|
90
|
+
data: { invalid_id: ref_id, href: href }
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Validate other references if enabled
|
|
95
|
+
@other_refs.each do |element, ref_id, attr_name, value|
|
|
96
|
+
next if @collected_ids.include?(ref_id)
|
|
97
|
+
|
|
98
|
+
message = if attr_name.start_with?("style:")
|
|
99
|
+
property = attr_name.split(":", 2)[1]
|
|
100
|
+
"style property #{property} references non-existent ID: #{ref_id}"
|
|
101
|
+
else
|
|
102
|
+
"#{attr_name} references non-existent ID: #{ref_id}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
context.add_error(
|
|
106
|
+
requirement_id: id,
|
|
107
|
+
node: element,
|
|
108
|
+
message: message,
|
|
109
|
+
severity: :error,
|
|
110
|
+
data: { invalid_id: ref_id, attribute: attr_name, value: value }
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
24
115
|
def validate_document(document, context)
|
|
25
116
|
# Collect all existing IDs in the document
|
|
26
117
|
existing_ids = collect_existing_ids(document)
|
|
@@ -45,6 +45,36 @@ module SvgConform
|
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
def validate_sax_element(element, context)
|
|
49
|
+
# Check href attributes
|
|
50
|
+
href_value = element.raw_attributes["href"] || element.raw_attributes["xlink:href"]
|
|
51
|
+
|
|
52
|
+
if href_value && !ascii_only?(href_value)
|
|
53
|
+
context.add_error(
|
|
54
|
+
requirement_id: id,
|
|
55
|
+
message: "Link href '#{href_value}' contains non-ASCII characters",
|
|
56
|
+
node: element,
|
|
57
|
+
severity: :error
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check other IRI attributes
|
|
62
|
+
iri_attributes = %w[src action formaction cite longdesc usemap]
|
|
63
|
+
iri_attributes.each do |attr_name|
|
|
64
|
+
iri_value = element.raw_attributes[attr_name]
|
|
65
|
+
next unless iri_value
|
|
66
|
+
|
|
67
|
+
next if ascii_only?(iri_value)
|
|
68
|
+
|
|
69
|
+
context.add_error(
|
|
70
|
+
requirement_id: id,
|
|
71
|
+
message: "IRI attribute '#{attr_name}' value '#{iri_value}' contains non-ASCII characters",
|
|
72
|
+
node: element,
|
|
73
|
+
severity: :error
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
48
78
|
private
|
|
49
79
|
|
|
50
80
|
def ascii_only?(string)
|
|
@@ -41,6 +41,65 @@ module SvgConform
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
def validate_sax_element(element, context)
|
|
45
|
+
# Skip validation for exempt elements (e.g., RDF metadata elements)
|
|
46
|
+
return if exempt_elements.include?(element.name)
|
|
47
|
+
|
|
48
|
+
# Check all attributes for namespace violations
|
|
49
|
+
element.attributes.each do |attr|
|
|
50
|
+
check_sax_attribute(attr, element, context)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def check_sax_attribute(attr, element, context)
|
|
57
|
+
attr_name = attr.name
|
|
58
|
+
|
|
59
|
+
# Check if this is a namespaced attribute by looking for colon in name
|
|
60
|
+
return unless attr_name.include?(":")
|
|
61
|
+
|
|
62
|
+
prefix, = attr_name.split(":", 2)
|
|
63
|
+
|
|
64
|
+
# Find the namespace URI for this prefix by walking up parent chain
|
|
65
|
+
namespace_uri = find_namespace_uri_sax(element, prefix)
|
|
66
|
+
return unless namespace_uri
|
|
67
|
+
|
|
68
|
+
# Determine if this namespace is invalid based on configuration
|
|
69
|
+
invalid_namespace = if allowed_namespaces.empty?
|
|
70
|
+
# Blacklist mode: disallowed namespaces are forbidden
|
|
71
|
+
disallowed_namespaces.include?(namespace_uri)
|
|
72
|
+
else
|
|
73
|
+
# Whitelist mode: only allowed namespaces are permitted
|
|
74
|
+
!allowed_namespaces.include?(namespace_uri)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
return unless invalid_namespace
|
|
78
|
+
|
|
79
|
+
context.add_error(
|
|
80
|
+
requirement_id: id,
|
|
81
|
+
message: "Element '#{element.name}' does not allow attributes with namespace '#{namespace_uri}'",
|
|
82
|
+
node: element,
|
|
83
|
+
severity: :error,
|
|
84
|
+
data: { attribute: attr_name, namespace: namespace_uri }
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def find_namespace_uri_sax(element, prefix)
|
|
89
|
+
# Check current element and ancestors for xmlns:prefix declarations
|
|
90
|
+
current = element
|
|
91
|
+
while current
|
|
92
|
+
# Check for xmlns:prefix attribute in raw_attributes
|
|
93
|
+
xmlns_value = current.raw_attributes["xmlns:#{prefix}"]
|
|
94
|
+
return xmlns_value if xmlns_value
|
|
95
|
+
|
|
96
|
+
# Move to parent
|
|
97
|
+
current = current.parent
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
44
103
|
private
|
|
45
104
|
|
|
46
105
|
def check_attribute_nodes(node, context)
|
|
@@ -190,8 +190,82 @@ module SvgConform
|
|
|
190
190
|
)
|
|
191
191
|
end
|
|
192
192
|
|
|
193
|
+
def validate_sax_element(element, context)
|
|
194
|
+
# Check if this element has a namespace
|
|
195
|
+
element_namespace = get_element_namespace_sax(element)
|
|
196
|
+
|
|
197
|
+
# Skip if no namespace (default SVG namespace)
|
|
198
|
+
return if element_namespace.nil? || element_namespace.empty?
|
|
199
|
+
|
|
200
|
+
# Check against allowed namespaces if configured
|
|
201
|
+
# If allow_rdf_metadata is enabled, also allow RDF namespaces
|
|
202
|
+
effective_allowed_namespaces = allowed_namespaces
|
|
203
|
+
if allow_rdf_metadata
|
|
204
|
+
effective_allowed_namespaces = allowed_namespaces + RDF_NAMESPACES
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
if effective_allowed_namespaces && !effective_allowed_namespaces.empty? && !effective_allowed_namespaces.include?(element_namespace)
|
|
208
|
+
context.add_error(
|
|
209
|
+
requirement_id: id,
|
|
210
|
+
message: "The namespace #{element_namespace} is not permitted for svg elements.",
|
|
211
|
+
node: element,
|
|
212
|
+
severity: :error,
|
|
213
|
+
data: {
|
|
214
|
+
element_name: element.name,
|
|
215
|
+
namespace: element_namespace,
|
|
216
|
+
allowed_namespaces: effective_allowed_namespaces
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
return
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Check against disallowed namespaces if configured
|
|
223
|
+
return unless disallowed_namespaces && !disallowed_namespaces.empty? && disallowed_namespaces.include?(element_namespace)
|
|
224
|
+
|
|
225
|
+
context.add_error(
|
|
226
|
+
requirement_id: id,
|
|
227
|
+
message: "The namespace #{element_namespace} is not permitted for svg elements.",
|
|
228
|
+
node: element,
|
|
229
|
+
severity: :error,
|
|
230
|
+
data: {
|
|
231
|
+
element_name: element.name,
|
|
232
|
+
namespace: element_namespace,
|
|
233
|
+
disallowed_namespaces: disallowed_namespaces
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
|
|
193
238
|
private
|
|
194
239
|
|
|
240
|
+
def get_element_namespace_sax(element)
|
|
241
|
+
# Try to get namespace from the element
|
|
242
|
+
namespace = element.namespace
|
|
243
|
+
return namespace if namespace && !namespace.empty?
|
|
244
|
+
|
|
245
|
+
# If no namespace found, check if element has a prefix (indicating it's namespaced)
|
|
246
|
+
if element.name.include?(":")
|
|
247
|
+
prefix = element.name.split(":").first
|
|
248
|
+
return find_namespace_uri_for_prefix_sax(element, prefix)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
nil
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def find_namespace_uri_for_prefix_sax(element, prefix)
|
|
255
|
+
# Check current element and ancestors for namespace declarations
|
|
256
|
+
current = element
|
|
257
|
+
while current
|
|
258
|
+
# Check for xmlns:prefix attribute
|
|
259
|
+
xmlns_attr = "xmlns:#{prefix}"
|
|
260
|
+
return current.raw_attributes[xmlns_attr] if current.raw_attributes[xmlns_attr]
|
|
261
|
+
|
|
262
|
+
# Move to parent
|
|
263
|
+
current = current.parent
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
nil
|
|
267
|
+
end
|
|
268
|
+
|
|
195
269
|
def check_all_elements(document, context)
|
|
196
270
|
# Recursively check all elements in the document
|
|
197
271
|
traverse_elements(document.root, context)
|
|
@@ -44,6 +44,17 @@ module SvgConform
|
|
|
44
44
|
has_style_attribute?(node)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
def validate_sax_element(element, context)
|
|
48
|
+
case element.name
|
|
49
|
+
when "style"
|
|
50
|
+
check_style_element_sax(element, context) if check_style_elements
|
|
51
|
+
when "link"
|
|
52
|
+
check_link_element_sax(element, context) if check_link_elements
|
|
53
|
+
else
|
|
54
|
+
check_style_attribute_sax(element, context) if check_style_attributes
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
47
58
|
private
|
|
48
59
|
|
|
49
60
|
def check_style_element(node, context)
|
|
@@ -109,6 +120,69 @@ module SvgConform
|
|
|
109
120
|
)
|
|
110
121
|
end
|
|
111
122
|
|
|
123
|
+
def check_style_element_sax(element, context)
|
|
124
|
+
# Check for @import rules in style elements
|
|
125
|
+
content = element.text_content
|
|
126
|
+
|
|
127
|
+
if content =~ /@import\s+url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
|
|
128
|
+
url = ::Regexp.last_match(1)
|
|
129
|
+
unless allowed_url?(url)
|
|
130
|
+
context.add_error(
|
|
131
|
+
requirement_id: id,
|
|
132
|
+
message: "External CSS import not allowed: #{url}",
|
|
133
|
+
node: element,
|
|
134
|
+
severity: :error
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
return unless content =~ /@import\s+['"]([^'"]+)['"]/i
|
|
140
|
+
|
|
141
|
+
url = ::Regexp.last_match(1)
|
|
142
|
+
return if allowed_url?(url)
|
|
143
|
+
|
|
144
|
+
context.add_error(
|
|
145
|
+
requirement_id: id,
|
|
146
|
+
message: "External CSS import not allowed: #{url}",
|
|
147
|
+
node: element,
|
|
148
|
+
severity: :error
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def check_link_element_sax(element, context)
|
|
153
|
+
rel = element.raw_attributes["rel"]
|
|
154
|
+
href = element.raw_attributes["href"]
|
|
155
|
+
|
|
156
|
+
return unless rel&.downcase == "stylesheet" && href
|
|
157
|
+
|
|
158
|
+
return if allowed_url?(href)
|
|
159
|
+
|
|
160
|
+
context.add_error(
|
|
161
|
+
requirement_id: id,
|
|
162
|
+
message: "External CSS link not allowed: #{href}",
|
|
163
|
+
node: element,
|
|
164
|
+
severity: :error
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def check_style_attribute_sax(element, context)
|
|
169
|
+
style_value = element.raw_attributes["style"]
|
|
170
|
+
return unless style_value
|
|
171
|
+
|
|
172
|
+
# Check for url() references in style attributes
|
|
173
|
+
return unless style_value =~ /url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
|
|
174
|
+
|
|
175
|
+
url = ::Regexp.last_match(1)
|
|
176
|
+
return if allowed_url?(url)
|
|
177
|
+
|
|
178
|
+
context.add_error(
|
|
179
|
+
requirement_id: id,
|
|
180
|
+
message: "External URL reference in style attribute not allowed: #{url}",
|
|
181
|
+
node: element,
|
|
182
|
+
severity: :error
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
112
186
|
def has_style_attribute?(node)
|
|
113
187
|
!get_attribute(node, "style").nil?
|
|
114
188
|
end
|
|
@@ -41,6 +41,17 @@ module SvgConform
|
|
|
41
41
|
has_style_attribute?(node)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
def validate_sax_element(element, context)
|
|
45
|
+
case element.name
|
|
46
|
+
when "style"
|
|
47
|
+
check_style_element_sax(element, context) if check_style_fonts
|
|
48
|
+
when "font-face"
|
|
49
|
+
check_font_face_element_sax(element, context) if check_font_face
|
|
50
|
+
else
|
|
51
|
+
check_style_attribute_sax(element, context) if check_style_fonts
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
44
55
|
private
|
|
45
56
|
|
|
46
57
|
def check_style_element(node, context)
|
|
@@ -100,6 +111,53 @@ module SvgConform
|
|
|
100
111
|
)
|
|
101
112
|
end
|
|
102
113
|
|
|
114
|
+
def check_style_element_sax(element, context)
|
|
115
|
+
# Check for @font-face with external src in style elements
|
|
116
|
+
content = element.text_content
|
|
117
|
+
|
|
118
|
+
# Match @font-face blocks
|
|
119
|
+
content.scan(/@font-face\s*\{([^}]+)\}/m) do |match|
|
|
120
|
+
font_face_content = match[0]
|
|
121
|
+
|
|
122
|
+
# Check for src with url() that is not data: URI
|
|
123
|
+
if font_face_content =~ /src\s*:\s*url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
|
|
124
|
+
url = ::Regexp.last_match(1)
|
|
125
|
+
unless embedded_font?(url)
|
|
126
|
+
context.add_error(
|
|
127
|
+
requirement_id: id,
|
|
128
|
+
message: "External font reference not allowed: #{url}. Fonts must be embedded as data URIs.",
|
|
129
|
+
node: element,
|
|
130
|
+
severity: :error
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def check_font_face_element_sax(element, context)
|
|
138
|
+
# Note: For SAX, we can't traverse children yet
|
|
139
|
+
# This would need to be handled differently or deferred
|
|
140
|
+
# For now, skip XPath-based checking in SAX mode
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def check_style_attribute_sax(element, context)
|
|
144
|
+
style_value = element.raw_attributes["style"]
|
|
145
|
+
return unless style_value
|
|
146
|
+
|
|
147
|
+
# Check for font-family with url() references
|
|
148
|
+
return unless style_value =~ /font-family\s*:\s*.*url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
|
|
149
|
+
|
|
150
|
+
url = ::Regexp.last_match(1)
|
|
151
|
+
return if embedded_font?(url)
|
|
152
|
+
|
|
153
|
+
context.add_error(
|
|
154
|
+
requirement_id: id,
|
|
155
|
+
message: "External font URL in style attribute not allowed: #{url}. Fonts must be embedded as data URIs.",
|
|
156
|
+
node: element,
|
|
157
|
+
severity: :error
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
103
161
|
def has_style_attribute?(node)
|
|
104
162
|
!get_attribute(node, "style").nil?
|
|
105
163
|
end
|
|
@@ -37,6 +37,15 @@ module SvgConform
|
|
|
37
37
|
node.name == "image" || has_style_attribute?(node)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
def validate_sax_element(element, context)
|
|
41
|
+
case element.name
|
|
42
|
+
when "image"
|
|
43
|
+
check_image_element_sax(element, context) if check_image_elements
|
|
44
|
+
else
|
|
45
|
+
check_style_attribute_sax(element, context) if check_style_images
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
40
49
|
private
|
|
41
50
|
|
|
42
51
|
def check_image_element(node, context)
|
|
@@ -70,6 +79,37 @@ module SvgConform
|
|
|
70
79
|
end
|
|
71
80
|
end
|
|
72
81
|
|
|
82
|
+
def check_image_element_sax(element, context)
|
|
83
|
+
# Check href and xlink:href attributes
|
|
84
|
+
href = element.raw_attributes["href"] || element.raw_attributes["xlink:href"]
|
|
85
|
+
return unless href && !embedded_image?(href)
|
|
86
|
+
|
|
87
|
+
context.add_error(
|
|
88
|
+
requirement_id: id,
|
|
89
|
+
message: "External image reference not allowed: #{href}. Images must be embedded as data URIs.",
|
|
90
|
+
node: element,
|
|
91
|
+
severity: :error
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def check_style_attribute_sax(element, context)
|
|
96
|
+
style_value = element.raw_attributes["style"]
|
|
97
|
+
return unless style_value
|
|
98
|
+
|
|
99
|
+
# Check for url() references to images in background, background-image, etc.
|
|
100
|
+
style_value.scan(/url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i) do
|
|
101
|
+
url = ::Regexp.last_match(1)
|
|
102
|
+
next if embedded_image?(url)
|
|
103
|
+
|
|
104
|
+
context.add_error(
|
|
105
|
+
requirement_id: id,
|
|
106
|
+
message: "External image URL in style attribute not allowed: #{url}. Images must be embedded as data URIs.",
|
|
107
|
+
node: element,
|
|
108
|
+
severity: :error
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
73
113
|
def has_style_attribute?(node)
|
|
74
114
|
!get_attribute(node, "style").nil?
|
|
75
115
|
end
|
|
@@ -43,6 +43,18 @@ module SvgConform
|
|
|
43
43
|
check_style_properties(style_value, node, context)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
def validate_sax_element(element, context)
|
|
47
|
+
style_value = element.raw_attributes["style"]
|
|
48
|
+
return unless style_value
|
|
49
|
+
return if style_value.strip.empty?
|
|
50
|
+
|
|
51
|
+
# 1. Check for malformed style syntax
|
|
52
|
+
check_malformed_syntax(style_value, element, context)
|
|
53
|
+
|
|
54
|
+
# 2. Check for allowed/disallowed properties and validate their values
|
|
55
|
+
check_style_properties(style_value, element, context)
|
|
56
|
+
end
|
|
57
|
+
|
|
46
58
|
private
|
|
47
59
|
|
|
48
60
|
def check_malformed_syntax(style_value, node, context)
|