qti 0.7.8 → 0.7.9

Sign up to get free protection for your applications and to get access to all the features.
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