qti 2.13.1 → 2.15.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 012e43c78ded5cf4f1ec586f8bbd321cadb532facc73e3b06bf9ecd884ab724f
4
- data.tar.gz: d95a01adb90683fa13c2bd769581edc53771c8a8157c692954090d38936fc318
3
+ metadata.gz: fcafa61127adf8dafdd3084cd6c22d56630473c0d01e42a7e6cddf6ee7b4ba96
4
+ data.tar.gz: 68fdf75e1b11fae2ca2ac55aca9daf936c2754ee1a0a244ccc94a51953c3957f
5
5
  SHA512:
6
- metadata.gz: bfd79edf504cedad28c37fb5c34ce67b54e2421ba8db324e25d905d23881c66d44f64f700d6803c8853f70b68864f249eb9725d47b3f3b232c66d3a9b2217f39
7
- data.tar.gz: 197b13f906c3604df179ab2276d69fcd8e0183f8988046bcd73df35653c312c02f0e37127874d6d371d7320e7e9d0b2bea5a6bbc324d419d1c3067907fa8e1d9
6
+ metadata.gz: 06543ad3a11e643f4c9731a641da3495afa53d94defa9ac8b40a34015d152ca24e5943fb1b33d9fe0db0714915f9a2e86dd2511821197f1b29395b071adf1ce0
7
+ data.tar.gz: 7b3ad6b2fa593ca9f2be62f524777fef0d41d9ddc8833af38ec445a89a3ffe5c855dd80312000978a0bd55bada68b8d711220a54b46d1b231223b0e41727b0b3
data/lib/qti/sanitizer.rb CHANGED
@@ -40,7 +40,7 @@ module Qti
40
40
  'object' => MEDIA_ATTR,
41
41
  'embed' => %w[name src type allowfullscreen pluginspage wmode
42
42
  allowscriptaccess width height],
43
- 'iframe' => %w[src width height name align frameborder scrolling sandbox
43
+ 'iframe' => %w[src style width height name align frameborder scrolling sandbox
44
44
  allowfullscreen webkitallowfullscreen mozallowfullscreen
45
45
  allow] + ALL_DATA_ATTR, # TODO: remove explicit allow with domain whitelist account setting
46
46
  'a' => relaxed_config('a', ['target'] + ALL_DATA_ATTR),
@@ -92,7 +92,7 @@ module Qti
92
92
  lambda do |env|
93
93
  return unless FILTER_TAGS.include?(env[:node_name])
94
94
  return if env[:is_whitelisted] || !env[:node].element?
95
- Sanitize.node!(env[:node], CONFIG)
95
+ Sanitize.node!(env[:node], Sanitize::Config.merge(Sanitize::Config::RELAXED, CONFIG))
96
96
  { node_whitelist: [env[:node]] }
97
97
  end
98
98
  end
@@ -18,7 +18,7 @@ module Qti
18
18
  node = @doc.dup
19
19
  presentation = node.at_xpath('.//xmlns:presentation')
20
20
  mattext = presentation.at_xpath('.//xmlns:mattext')
21
- prompt = return_inner_content!(mattext)
21
+ prompt = sanitize_attributes(return_inner_content!(mattext))
22
22
  sanitize_content!(prompt)
23
23
  end
24
24
  end
@@ -83,11 +83,15 @@ module Qti
83
83
  end
84
84
 
85
85
  def feedback
86
- @feedback ||= interaction_model.canvas_item_feedback
86
+ @feedback ||= interaction_model.canvas_item_feedback&.transform_values { |v| sanitize_content!(v) }
87
87
  end
88
88
 
89
89
  def answer_feedback
90
- @answer_feedback ||= interaction_model.answer_feedback
90
+ @answer_feedback ||= interaction_model.answer_feedback&.map do |answer|
91
+ duped = answer.dup
92
+ duped[:feedback] = sanitize_content!(duped[:feedback])
93
+ duped
94
+ end
91
95
  end
92
96
  end
93
97
  end
@@ -2,6 +2,8 @@ module Qti
2
2
  module V1
3
3
  module Models
4
4
  class Base < Qti::Models::Base
5
+ CANVAS_BLANK_REGEX ||= /(\[[A-Za-z0-9_\-.]+\])/.freeze
6
+
5
7
  def qti_version
6
8
  1
7
9
  end
@@ -12,6 +14,20 @@ module Qti
12
14
  node.inner_html
13
15
  end
14
16
 
17
+ def sanitize_attributes(html)
18
+ node = Nokogiri::HTML.fragment(html)
19
+ sanitize_attributes_by_node(node)
20
+ node.to_html
21
+ end
22
+
23
+ def sanitize_attributes_by_node(node)
24
+ node.attribute_nodes.each do |a|
25
+ matches = a.value.match(CANVAS_BLANK_REGEX) || []
26
+ a.value = a.value.gsub!('[', '&#91;').gsub!(']', '&#93;') if matches.length.positive?
27
+ end
28
+ node.children.each { |c| sanitize_attributes_by_node(c) }
29
+ end
30
+
15
31
  private
16
32
 
17
33
  def text_node?(node)
@@ -3,10 +3,9 @@ module Qti
3
3
  module Models
4
4
  module Interactions
5
5
  class BaseFillBlankInteraction < BaseInteraction
6
- CANVAS_REGEX ||= /(\[[A-Za-z0-9_\-.]+\])/.freeze
7
-
8
6
  def canvas_stem_items(item_prompt)
9
- item_prompt.split(CANVAS_REGEX).map.with_index do |stem_item, index|
7
+ item_prompt = sanitize_attributes(item_prompt)
8
+ item_prompt.split(CANVAS_BLANK_REGEX).map.with_index do |stem_item, index|
10
9
  if canvas_fib_response_ids.include?(stem_item)
11
10
  # Strip the brackets before searching
12
11
  value = stem_item[1..-2]
data/lib/qti/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Qti
2
- VERSION = '2.13.1'.freeze
2
+ VERSION = '2.15.0'.freeze
3
3
  end
@@ -3,6 +3,18 @@
3
3
  <assessment title="1.2 Import Quiz" ident="A1001">
4
4
  <section title="Main" ident="S1002">
5
5
  <item title="Grading - specific - 3 pt score" ident="QUE_1003">
6
+ <itemmetadata>
7
+ <qtimetadata>
8
+ <qtimetadatafield>
9
+ <fieldlabel>question_type</fieldlabel>
10
+ <fieldentry>true_false_question</fieldentry>
11
+ </qtimetadatafield>
12
+ <qtimetadatafield>
13
+ <fieldlabel>assessment_question_identifierref</fieldlabel>
14
+ <fieldentry>iff205c7405d3b36bdca9b75b51198932</fieldentry>
15
+ </qtimetadatafield>
16
+ </qtimetadata>
17
+ </itemmetadata>
6
18
  <presentation>
7
19
  <material>
8
20
  <mattext texttype="text/html"><![CDATA[If I get a 3, I must have done something wrong. <img data-equation-content="sample equation" script="alert('bad')" align="bottom" alt="image.png" src="org0/images/image.png" border="0"/> <img script="alert('bad')" align="bottom" alt="image.png" src="org0/images/image.png" border="0"/>]]></mattext>
@@ -37,8 +49,37 @@
37
49
  <varequal respident="QUE_1004_RL">QUE_1006_A2</varequal>
38
50
  </conditionvar>
39
51
  <setvar varname="que_score" action="Set">10.00</setvar>
52
+ <displayfeedback feedbacktype="Response" linkrefid="fb_QUE_1006_A2"/>
40
53
  </respcondition>
41
54
  </resprocessing>
55
+ <itemfeedback ident="general_fb">
56
+ <flow_mat>
57
+ <material>
58
+ <mattext texttype="text/html">&lt;p&gt;Neutral&lt;/p&gt;&lt;img script="alert('bad')" alt="image.png" src="org0/images/image.png"/&gt;</mattext>
59
+ </material>
60
+ </flow_mat>
61
+ </itemfeedback>
62
+ <itemfeedback ident="correct_fb">
63
+ <flow_mat>
64
+ <material>
65
+ <mattext texttype="text/html">&lt;p&gt;Correct&lt;/p&gt;&lt;img script="alert('bad')" alt="image.png" src="org0/images/image.png"/&gt;</mattext>
66
+ </material>
67
+ </flow_mat>
68
+ </itemfeedback>
69
+ <itemfeedback ident="general_incorrect_fb">
70
+ <flow_mat>
71
+ <material>
72
+ <mattext texttype="text/html">&lt;p&gt;Incorrect&lt;/p&gt;&lt;img script="alert('bad')" alt="image.png" src="org0/images/image.png"/&gt;</mattext>
73
+ </material>
74
+ </flow_mat>
75
+ </itemfeedback>
76
+ <itemfeedback ident="fb_QUE_1006_A2">
77
+ <flow_mat>
78
+ <material>
79
+ <mattext texttype="text/html">&lt;p&gt;Answer was Correct&lt;/p&gt;&lt;img script="alert('bad')" alt="image.png" src="org0/images/image.png"/&gt;</mattext>
80
+ </material>
81
+ </flow_mat>
82
+ </itemfeedback>
42
83
  </item>
43
84
  </section>
44
85
  </assessment>
@@ -54,5 +54,14 @@ describe Qti::Sanitizer do
54
54
 
55
55
  expect(sanitizer.clean(html)).to include 'target="_blank"'
56
56
  end
57
+
58
+ it 'allows style attributes on iframe' do
59
+ html = '<iframe style="width: 523px; height: 294px; display: inline-block;"></iframe>'
60
+
61
+ expect(sanitizer.clean(html)).to include 'style'
62
+ expect(sanitizer.clean(html)).to include 'width: 523px;'
63
+ expect(sanitizer.clean(html)).to include 'height: 294px;'
64
+ expect(sanitizer.clean(html)).to include 'display: inline-block;'
65
+ end
57
66
  end
58
67
  end
@@ -10,6 +10,37 @@ describe Qti::V1::Models::AssessmentItem do
10
10
  end
11
11
  end
12
12
 
13
+ context 'feedback', focus: true do
14
+ let(:file_path) { File.join('spec', 'fixtures', 'items_1.2', 'true_false.xml') }
15
+ let(:test_object) { Qti::V1::Models::Assessment.from_path!(file_path) }
16
+ let(:assessment_item_refs) { test_object.assessment_items }
17
+ let(:loaded_class) { described_class.new(assessment_item_refs) }
18
+
19
+ it 'sanitizes general neutral feedback' do
20
+ expect(loaded_class.feedback[:neutral]).to include '<p>Neutral'
21
+ expect(loaded_class.feedback[:neutral]).to include '<img'
22
+ expect(loaded_class.feedback[:neutral]).not_to include 'script="alert(\'bad\')"'
23
+ end
24
+
25
+ it 'sanitizes general correct feedback' do
26
+ expect(loaded_class.feedback[:correct]).to include '<p>Correct'
27
+ expect(loaded_class.feedback[:correct]).to include '<img'
28
+ expect(loaded_class.feedback[:correct]).not_to include 'script="alert(\'bad\')"'
29
+ end
30
+
31
+ it 'sanitizes general incorrect feedback' do
32
+ expect(loaded_class.feedback[:incorrect]).to include '<p>Incorrect'
33
+ expect(loaded_class.feedback[:incorrect]).to include '<img'
34
+ expect(loaded_class.feedback[:incorrect]).not_to include 'script="alert(\'bad\')"'
35
+ end
36
+
37
+ it 'sanitizes answer feedback' do
38
+ expect(loaded_class.answer_feedback.first[:feedback]).to include '<p>Answer was Correc'
39
+ expect(loaded_class.answer_feedback.first[:feedback]).to include '<img'
40
+ expect(loaded_class.answer_feedback.first[:feedback]).not_to include 'script="alert(\'bad\')"'
41
+ end
42
+ end
43
+
13
44
  context 'quiz.xml' do
14
45
  let(:file_path) { File.join('spec', 'fixtures', 'items_1.2', 'true_false.xml') }
15
46
  let(:test_object) { Qti::V1::Models::Assessment.from_path!(file_path) }
@@ -0,0 +1,22 @@
1
+ describe Qti::V1::Models::Base do
2
+ context 'specified as single content node matching helpers' do
3
+ let(:loaded_class) do
4
+ fixtures_path = File.join('spec', 'fixtures')
5
+ path = File.join(fixtures_path, 'test_qti_1.2', 'quiz.xml')
6
+ described_class.from_path!(path)
7
+ end
8
+
9
+ describe '.sanitize_attributes' do
10
+ it 'respects valid content' do
11
+ source = 'fill in the [blank]'
12
+ expect(loaded_class.sanitize_attributes(source)).to eq source
13
+ end
14
+
15
+ it 'translates invalid content' do
16
+ source = '<span title="[x]">fill in the [blank]</span>'
17
+ expected = '<span title="&amp;#91;x&amp;#93;">fill in the [blank]</span>'
18
+ expect(loaded_class.sanitize_attributes(source)).to eq expected
19
+ end
20
+ end
21
+ end
22
+ end
@@ -39,7 +39,7 @@ describe Qti::V1::Models::Interactions::ChoiceInteraction do
39
39
  let(:file_path) { File.join(fixtures_path, 'true_false.xml') }
40
40
  let(:shuffle_value) { true }
41
41
  let(:answer_choices_count) { 2 }
42
- let(:meta_type) { nil }
42
+ let(:meta_type) { 'true_false_question' }
43
43
 
44
44
  include_examples 'shuffled?'
45
45
  include_examples 'answers'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qti
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.13.1
4
+ version: 2.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Diaz
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2023-04-24 00:00:00.000000000 Z
15
+ date: 2023-05-16 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: actionview
@@ -665,6 +665,7 @@ files:
665
665
  - spec/lib/qti/sanitizer_spec.rb
666
666
  - spec/lib/qti/v1/models/assessment_item_spec.rb
667
667
  - spec/lib/qti/v1/models/assessment_spec.rb
668
+ - spec/lib/qti/v1/models/base_spec.rb
668
669
  - spec/lib/qti/v1/models/choices/fill_blank_choice_spec.rb
669
670
  - spec/lib/qti/v1/models/choices/logical_identifier_choice_spec.rb
670
671
  - spec/lib/qti/v1/models/interactions/base_fill_blank_interaction_spec.rb
@@ -1022,6 +1023,7 @@ test_files:
1022
1023
  - spec/lib/qti/sanitizer_spec.rb
1023
1024
  - spec/lib/qti/v1/models/assessment_item_spec.rb
1024
1025
  - spec/lib/qti/v1/models/assessment_spec.rb
1026
+ - spec/lib/qti/v1/models/base_spec.rb
1025
1027
  - spec/lib/qti/v1/models/choices/fill_blank_choice_spec.rb
1026
1028
  - spec/lib/qti/v1/models/choices/logical_identifier_choice_spec.rb
1027
1029
  - spec/lib/qti/v1/models/interactions/base_fill_blank_interaction_spec.rb