qti 0.7.8 → 0.7.9

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
  SHA1:
3
- metadata.gz: 1897287f2eb2b6af316a0f7c81112e50fe2e8d43
4
- data.tar.gz: 41cc16c9d6928833b10c963a17f181eb9d5f2361
3
+ metadata.gz: bd980a1e89fbf6817329ae29519f4e266e889906
4
+ data.tar.gz: f4a2d4935a246d9a07987240059f0c2ca9b03e16
5
5
  SHA512:
6
- metadata.gz: f3a3945bbe3401cb3cfc512bbcefc301a3cd4983e9b2d6da4a9b9ac4e851bce71fa76e5c0be1c45f416eed89c196802e62794c9af5923f8194a5d28a6c3f9f2a
7
- data.tar.gz: dca959a4115b19ab11172438045b8f973511b92b1fa7c19767887f004ff655bbba00b03b7ba4f22796bc123dd3aa5b45a8e2aa4133c0cb60698ba15ca9448e17
6
+ metadata.gz: 66cc18a1302da5dab058a48613df70bb52f6c4211a1ce37dde549844e1fe31931ef3228f8ef74a07044202728efe57f96ec537f8de878338ed72e2acdc7c98c7
7
+ data.tar.gz: 39b9f7df2da0f51188766863bfd3e2589f8cd907459689b1b797e7439af46d112eec5d58d2c9ae9270f450c9bcddb295bccfe1c88baf34935132bf5a8eaaae7b
data/lib/qti.rb CHANGED
@@ -2,10 +2,16 @@ require 'find'
2
2
 
3
3
  module Qti
4
4
  class Importer
5
+ attr_reader :package_root
6
+
5
7
  def initialize(path)
6
8
  Find.find(path) do |subdir|
7
- @path = subdir if subdir =~ /imsmanifest.xml/
9
+ if subdir =~ /imsmanifest.xml\z/
10
+ @path = subdir
11
+ break
12
+ end
8
13
  end
14
+ @package_root = File.dirname(@path)
9
15
  end
10
16
 
11
17
  def import_manifest
@@ -20,9 +26,9 @@ module Qti
20
26
 
21
27
  def version_agnostic_test_object(assessment_test_file)
22
28
  if @manifest.qti_1_href
23
- Qti::V1::Models::Assessment.from_path!(assessment_test_file)
29
+ Qti::V1::Models::Assessment.from_path!(assessment_test_file, @package_root)
24
30
  else
25
- Qti::V2::Models::AssessmentTest.from_path!(assessment_test_file)
31
+ Qti::V2::Models::AssessmentTest.from_path!(assessment_test_file, @package_root)
26
32
  end
27
33
  end
28
34
 
@@ -38,9 +44,9 @@ module Qti
38
44
 
39
45
  def create_assessment_item(assessment_item_ref)
40
46
  if @manifest.qti_1_href
41
- Qti::V1::Models::AssessmentItem.new(assessment_item_ref)
47
+ Qti::V1::Models::AssessmentItem.new(assessment_item_ref, @package_root)
42
48
  else
43
- Qti::V2::Models::AssessmentItem.from_path!(assessment_item_ref)
49
+ Qti::V2::Models::AssessmentItem.from_path!(assessment_item_ref, @package_root)
44
50
  end
45
51
  end
46
52
  end
@@ -1,5 +1,6 @@
1
1
  require 'nokogiri'
2
2
  require 'sanitize'
3
+ require 'pathname'
3
4
 
4
5
  module Qti
5
6
  class ParseError < StandardError; end
@@ -8,7 +9,7 @@ module Qti
8
9
 
9
10
  module Models
10
11
  class Base
11
- attr_reader :doc
12
+ attr_reader :doc, :path, :package_root
12
13
 
13
14
  ELEMENTS_REMAP = {
14
15
  'prompt' => 'div',
@@ -40,12 +41,13 @@ module Qti
40
41
  Sanitize::Config::RELAXED.merge transformers: remap_unknown_tags_transformer
41
42
  end
42
43
 
43
- def self.from_path!(path)
44
- new(path: path)
44
+ def self.from_path!(path, package_root = nil)
45
+ new(path: path, package_root: package_root)
45
46
  end
46
47
 
47
- def initialize(path: nil)
48
+ def initialize(path:, package_root: nil)
48
49
  @path = path
50
+ set_package_root(package_root || File.dirname(path))
49
51
  @doc = parse_xml(File.read(path))
50
52
  raise ArgumentError unless @doc
51
53
  end
@@ -63,19 +65,26 @@ module Qti
63
65
  end
64
66
 
65
67
  def parse_xml(xml_string)
66
- Nokogiri.XML(xml_string, &:noblanks)
68
+ Nokogiri.XML(xml_string, @path.to_s, &:noblanks)
67
69
  end
68
70
 
69
- def remap_href_path(href, source_path)
70
- return href unless href
71
- # Attempts to map to a file path relative href if href doesn't exist
72
- # Returns original href if that file doesn't exist
73
- if File.exist?(href)
74
- href
71
+ def remap_href_path(href)
72
+ return nil unless href
73
+ path = File.join(File.dirname(@path), href)
74
+ if @package_root.nil?
75
+ raise Qti::ParseError, "Potentially unsafe href '#{href}'" if href.split('/').include?('..')
75
76
  else
76
- new_path = File.join(File.dirname(source_path), href)
77
- File.exist?(new_path) ? new_path : href
77
+ raise Qti::ParseError, "Unsafe href '#{href}'" unless Pathname.new(path).cleanpath.to_s.start_with?(@package_root)
78
78
  end
79
+ path
80
+ end
81
+
82
+ protected
83
+
84
+ def set_package_root(package_root)
85
+ @package_root = package_root
86
+ return unless @package_root
87
+ @package_root = Pathname.new(@package_root).cleanpath.to_s + '/'
79
88
  end
80
89
  end
81
90
  end
@@ -5,7 +5,7 @@ module Qti
5
5
  class Manifest < Qti::Models::Base
6
6
  def assessment_test_href
7
7
  href = qti_1_href.nil? ? qti_2_x_href : qti_1_href
8
- remap_href_path(href, @path)
8
+ remap_href_path(href)
9
9
  end
10
10
 
11
11
  def qti_1_href
@@ -7,8 +7,10 @@ module Qti
7
7
  class AssessmentItem < Qti::V1::Models::Base
8
8
  attr_reader :doc
9
9
 
10
- def initialize(item)
10
+ def initialize(item, package_root = nil)
11
11
  @doc = item
12
+ @path = item.document.url
13
+ set_package_root(package_root)
12
14
  end
13
15
 
14
16
  def item_body
@@ -17,7 +17,7 @@ module Qti
17
17
  def item_body
18
18
  @item_body ||= begin
19
19
  node = @node.dup
20
- node.content.squish
20
+ sanitize_content!(node.content.squish)
21
21
  end
22
22
  end
23
23
  end
@@ -16,7 +16,7 @@ module Qti
16
16
  def item_body
17
17
  @item_body ||= begin
18
18
  node = @node.dup
19
- node.content.strip.gsub(/\s+/, ' ')
19
+ sanitize_content!(node.content.strip.gsub(/\s+/, ' '))
20
20
  end
21
21
  end
22
22
  end
@@ -17,7 +17,7 @@ module Qti
17
17
 
18
18
  def questions
19
19
  node.xpath('.//xmlns:response_lid').map do |lid_node|
20
- item_body = lid_node.at_xpath('.//xmlns:mattext').text
20
+ item_body = sanitize_content!(lid_node.at_xpath('.//xmlns:mattext').text)
21
21
  { id: lid_node.attributes['ident'].value, itemBody: item_body }
22
22
  end
23
23
  end
@@ -12,7 +12,7 @@ module Qti
12
12
  # Return the xml files we should be parsing
13
13
  @assessment_item_reference_hrefs ||= begin
14
14
  @doc.xpath('//xmlns:assessmentItemRef/@href').map(&:content).map do |href|
15
- remap_href_path(href, @path)
15
+ remap_href_path(href)
16
16
  end
17
17
  end
18
18
  end
@@ -18,7 +18,7 @@ module Qti
18
18
  @item_body ||= begin
19
19
  node = @node.dup
20
20
  node.children.filter(PROHIBITED_NODE_NAMES).each(&:unlink)
21
- node.content.strip.gsub(/\s+/, ' ')
21
+ sanitize_content!(node.content.strip.gsub(/\s+/, ' '))
22
22
  end
23
23
  end
24
24
  end
@@ -19,7 +19,7 @@ module Qti
19
19
  @item_body ||= begin
20
20
  node = @node.dup
21
21
  node.children.filter(PROHIBITED_NODE_NAMES).map(&:unlink)
22
- node.content.strip.gsub(/\s+/, ' ')
22
+ sanitize_content!(node.content.strip.gsub(/\s+/, ' '))
23
23
  end
24
24
  end
25
25
  end
@@ -45,26 +45,47 @@ describe Qti::Models::Base do
45
45
  end
46
46
 
47
47
  describe '#remap_href_path' do
48
- let(:href) { 'hi.xml' }
49
- let(:base_path) { 'hello/bob.xml' }
50
- let(:remapped_path) { File.join(File.dirname(base_path), href) }
51
- let(:subject) { loaded_class.remap_href_path(href, base_path) }
52
-
53
- it 'passes the original path if it exists' do
54
- allow(File).to receive(:exist?).with(href).and_return(true)
55
- expect(subject).to eq href
48
+ it 'constructs a path relative to the basename of the source' do
49
+ expect(loaded_class.remap_href_path('hi.xml')).to eq 'spec/fixtures/test_qti_2.2/hi.xml'
56
50
  end
57
51
 
58
- it 'passes the original path when the remapped path doesn\'t exist' do
59
- allow(File).to receive(:exist?).with(href).and_return(false)
60
- allow(File).to receive(:exist?).with(remapped_path).and_return(false)
61
- expect(subject).to eq href
52
+ it 'is not fooled by an absolute href' do
53
+ expect(loaded_class.remap_href_path('/etc/shadow')).to eq 'spec/fixtures/test_qti_2.2/etc/shadow'
62
54
  end
63
55
 
64
- it 'passes the remapped path if it exists' do
65
- allow(File).to receive(:exist?).with(href).and_return(false)
66
- allow(File).to receive(:exist?).with(remapped_path).and_return(true)
67
- expect(subject).to eq remapped_path
56
+ it 'uses an implicit package root' do
57
+ expect {
58
+ loaded_class.remap_href_path('../sneaky.txt')
59
+ }.to raise_error(Qti::ParseError)
60
+ end
61
+
62
+ context "with explicit package root" do
63
+ let(:package_root) { File.join('spec', 'fixtures', 'test_qti_2.2') }
64
+ let(:item_path) { File.join(package_root, 'true-false', 'true-false.xml') }
65
+ let(:item) { described_class.from_path!(item_path, package_root) }
66
+
67
+ it 'allows safe .. hrefs' do
68
+ expect {
69
+ item.remap_href_path('../okay.txt')
70
+ }.not_to raise_error
71
+ end
72
+
73
+ it 'rejects attempts to escape the package' do
74
+ expect {
75
+ item.remap_href_path('../../bad.txt')
76
+ }.to raise_error(Qti::ParseError)
77
+ end
78
+ end
79
+
80
+ context "with nil package root" do
81
+ let(:item_path) { File.join('spec', 'fixtures', 'test_qti_2.2', 'true-false', 'true-false.xml') }
82
+ let(:item) { described_class.from_path!(item_path, nil) }
83
+
84
+ it 'rejects .. hrefs' do
85
+ expect {
86
+ item.remap_href_path('../no_longer_okay.txt')
87
+ }.to raise_error(Qti::ParseError)
88
+ end
68
89
  end
69
90
  end
70
91
  end
@@ -9,6 +9,10 @@ describe Qti::Importer do
9
9
  it 'loads an xml file' do
10
10
  expect { importer }.to_not raise_error
11
11
  end
12
+
13
+ it 'sets the package root properly' do
14
+ expect(importer.package_root).to eq file_path
15
+ end
12
16
  end
13
17
 
14
18
  shared_examples_for 'unsupported QTI version' do
@@ -33,6 +37,13 @@ describe Qti::Importer do
33
37
  answer_arity = assessment_items.map { |item| item.scoring_data_structs.count }
34
38
  expect(answer_arity).to eq [1, 1, 4, 1, 1]
35
39
  end
40
+
41
+ it 'sets the path and package root properly' do
42
+ importer.test_object
43
+ item = importer.create_assessment_item(importer.assessment_item_refs.first)
44
+ expect(item.path).to eq file_path + '/quiz.xml'
45
+ expect(item.package_root).to eq file_path + '/'
46
+ end
36
47
  end
37
48
 
38
49
  context 'canvas generated' do
@@ -51,5 +62,15 @@ describe Qti::Importer do
51
62
 
52
63
  include_examples 'initialize'
53
64
  include_examples 'unsupported QTI version'
65
+
66
+ describe '#create_assessment_item' do
67
+ it 'sets the path and package root properly' do
68
+ importer.test_object
69
+ ref = importer.assessment_item_refs.first
70
+ item = importer.create_assessment_item(ref)
71
+ expect(item.path).to eq ref
72
+ expect(item.package_root).to eq file_path + '/'
73
+ end
74
+ end
54
75
  end
55
76
  end
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: 0.7.8
4
+ version: 0.7.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hannah Bottalla
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-08-09 00:00:00.000000000 Z
12
+ date: 2017-08-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -488,7 +488,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
488
488
  version: '0'
489
489
  requirements: []
490
490
  rubyforge_project:
491
- rubygems_version: 2.6.11
491
+ rubygems_version: 2.5.1
492
492
  signing_key:
493
493
  specification_version: 4
494
494
  summary: QTI 1.2 and 2.1 import and export models