canvas_link_migrator 1.0.18 → 1.0.20

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: 949c23c7d951df591af05de644bc829ceada5889aed295bb8e8dee5b897054ee
4
- data.tar.gz: 8418a95af9eca811bf3e3848987bbd83bef233ca7aadaf33455f277cecb759b0
3
+ metadata.gz: ca1247f98a0eee8de26f1fa32da2ec683f374ea995a695bb4af201d87644795a
4
+ data.tar.gz: 122790ae2f969eee61c75118096d2e9b736fc328e9a27846f8b5b2e82ec7830b
5
5
  SHA512:
6
- metadata.gz: 0e886dbeaefd433185a7036739dba643c4c26add118b0dd737ee79db6e1dd7e384ba1a5389491023a857b72727055f494e2894746e5f55849ea13f11855c5b4b
7
- data.tar.gz: 97f130e22876d639a79faf0b062442252df9392b8e2338a6bf6b46793a83d815b954b43e34f4e1ff1fce97f7e4856b4ca34e5085e1b212c58f653dc05ff89c9d
6
+ metadata.gz: 37e6e2273e60f1e4428793295e8610e21c138ae16527d54d3a8885c9b5ed88e3660e521e63386ea15f7b7fdab8201f29b0732360e26dce640020c40ec9e88985
7
+ data.tar.gz: 1d7e969234f632637138adcffa681a30a2a0d5fb3391ef080010c2252fbcd2520ed9e726ab4267793d8c1402baa45b5b910a16cce74e421be740899daccc4b7c
@@ -37,11 +37,14 @@ module CanvasLinkMigrator
37
37
  LinkParser::REFERENCE_KEYWORDS.each { |ref| url.gsub!("%24#{ref}%24", "$#{ref}$") }
38
38
  # create the link map for a single link (parse)
39
39
  link_parsing_result = link_parser.parse_single_url(url, link_type)
40
- link_parser.handle_parsed_url(url, link_parsing_result, nil, nil, link_type, nil, nil)
41
- # resolve_link! on the single element link map
42
- link_resolver.resolve_link!(link_parsing_result, link_type)
43
- # return the new value
44
- link_parsing_result[:new_value]
40
+ if link_parsing_result[:resolved]
41
+ link_parser.perform_substitutions_and_make_relative_if_possible(url, link_parsing_result)
42
+ else
43
+ link_parser.handle_unresolved_link(url, link_parsing_result, nil, nil, link_type, nil, nil)
44
+ # resolve_link! on the single element link map
45
+ link_resolver.resolve_link!(link_parsing_result, link_type)
46
+ link_parsing_result[:new_value]
47
+ end
45
48
  end
46
49
 
47
50
  def convert_exported_html(input_html)
@@ -180,7 +180,7 @@ module CanvasLinkMigrator
180
180
  add_unresolved_link(result, item_type, mig_id, field)
181
181
  end
182
182
 
183
- def handle_resolved_link(url, result, node, attr)
183
+ def perform_substitutions_and_make_relative_if_possible(url, result)
184
184
  new_url = result[:new_url] || url
185
185
  unless CanvasLinkMigrator.relative_url?(new_url)
186
186
  # perform configured substitutions
@@ -200,7 +200,11 @@ module CanvasLinkMigrator
200
200
  nil
201
201
  end
202
202
  end
203
- node[attr] = new_url
203
+ new_url
204
+ end
205
+
206
+ def handle_resolved_link(url, result, node, attr)
207
+ node[attr] = perform_substitutions_and_make_relative_if_possible(url, result)
204
208
  end
205
209
 
206
210
  def unresolved(type, data = {})
@@ -19,16 +19,38 @@
19
19
 
20
20
  module CanvasLinkMigrator
21
21
  class LinkReplacer
22
+ HTML_ESCAPES = { '"' => "&quot;", "<" => "&lt;", ">" => "&gt;" }.freeze
23
+ HTML_ESCAPE_RE = Regexp.union(HTML_ESCAPES.keys).freeze
24
+
25
+ def self.escape_url_for_html(value)
26
+ value.to_s.gsub(HTML_ESCAPE_RE, HTML_ESCAPES)
27
+ end
28
+
22
29
  # returns false if no substitutions were made
23
30
  def self.sub_placeholders!(html, links)
24
31
  subbed = false
25
- links.each do |link|
32
+ media_links, other_links = links.partition { |l| l[:link_type] == :media_object }
33
+
34
+ if media_links.any?
35
+ by_placeholder = media_links.to_h { |l| [l[:placeholder], l] }
36
+ if html.gsub!(Regexp.union(by_placeholder.keys)) { |m|
37
+ link = by_placeholder[m]
38
+ link[:replaced] = true
39
+ link[:new_value] || link[:old_value]
40
+ }
41
+ subbed = true
42
+ end
43
+ end
44
+
45
+ other_links.each do |link|
26
46
  new_value = link[:new_value] || link[:old_value]
27
- if html.gsub!(link[:placeholder], new_value)
47
+ new_value = escape_url_for_html(new_value)
48
+ if html.gsub!(link[:placeholder]) { new_value }
28
49
  link[:replaced] = true
29
50
  subbed = true
30
51
  end
31
52
  end
53
+
32
54
  subbed
33
55
  end
34
56
 
@@ -222,7 +222,7 @@ module CanvasLinkMigrator
222
222
  case k
223
223
  when /canvas_qs_(.*)/
224
224
  qs << "#{Rack::Utils.escape($1)}=#{Rack::Utils.escape(v)}"
225
- when /canvas_(.*)/
225
+ when /\Acanvas_(\w*)\z/
226
226
  new_action += "/#{$1}"
227
227
  end
228
228
  end
@@ -1,3 +1,3 @@
1
1
  module CanvasLinkMigrator
2
- VERSION = "1.0.18"
2
+ VERSION = "1.0.20"
3
3
  end
@@ -140,6 +140,35 @@ describe CanvasLinkMigrator::ImportedHtmlConverter do
140
140
  test_string = %(<img src="%24IMS_CC_FILEBASE%24/test.png?notarelevantparam" alt="nope" />)
141
141
  expect(@converter.convert_exported_html(test_string)).to eq([%(<img src="#{@path}files/5/preview?verifier=u5" alt="nope">), nil])
142
142
  end
143
+
144
+ describe "path-segment injection via canvas_* parameter names" do
145
+ it "does not allow `..` path traversal via canvas_* keys" do
146
+ test_string = %(<img src="%24IMS_CC_FILEBASE%24/test.png?canvas_..%2F..%2Faccounts%2F1%2Fadmin=x" alt="nope" />)
147
+ html, _ = @converter.convert_exported_html(test_string)
148
+ src = Nokogiri::HTML5.fragment(html).at_css("img")["src"]
149
+ expect(src).not_to include(".."),
150
+ "path traversal: src contains `..` segments -- output was: #{src.inspect}"
151
+ end
152
+
153
+ it "does not allow a single `..` segment via canvas_* keys" do
154
+ test_string = %(<a href="%24IMS_CC_FILEBASE%24/test.png?canvas_..=x">x</a>)
155
+ html, _ = @converter.convert_exported_html(test_string)
156
+ href = Nokogiri::HTML5.fragment(html).at_css("a")["href"]
157
+ expect(href).not_to match(%r{/\.\.(?:/|\?|#|\z)}),
158
+ "path traversal: href ends in a `..` segment -- output was: #{href.inspect}"
159
+ end
160
+
161
+ it "does not allow `#` fragment injection that hides the verifier" do
162
+ test_string = %(<a href="%24IMS_CC_FILEBASE%24/test.png?canvas_a%23evil=x">x</a>)
163
+ html, _ = @converter.convert_exported_html(test_string)
164
+ href = Nokogiri::HTML5.fragment(html).at_css("a")["href"]
165
+ parsed = URI.parse(href)
166
+ expect(parsed.fragment).to be_nil,
167
+ "fragment injection: produced fragment #{parsed.fragment.inspect} -- output was: #{href.inspect}"
168
+ expect(parsed.query.to_s).to include("verifier="),
169
+ "verifier was pushed into the fragment by `#` injection -- output was: #{href.inspect}"
170
+ end
171
+ end
143
172
  end
144
173
 
145
174
  it "converts picture source srcsets" do
@@ -190,6 +219,113 @@ describe CanvasLinkMigrator::ImportedHtmlConverter do
190
219
  expect(@converter.convert_exported_html(test_string)).to eq([%(<a href="/courses/123">Mine</a><br><a href="/courses/456">Vain</a><br><a href="http://other-canvas.example.com/">Other Instance</a>), nil])
191
220
  end
192
221
 
222
+ describe "percent-encoded HTML in unresolved file references (CSAN-045)" do
223
+ it "does not allow attribute breakout via $IMS-CC-FILEBASE$ references" do
224
+ test_string = %(<img src="$IMS-CC-FILEBASE$/foo%22%09onerror=%22alert(1).jpg">)
225
+ html, _bad_links = @converter.convert_exported_html(test_string)
226
+
227
+ img = Nokogiri::HTML5.fragment(html).at_css("img")
228
+ expect(img).not_to be_nil
229
+ expect(img["onerror"]).to be_nil,
230
+ "attribute breakout: produced live onerror -- output was: #{html.inspect}"
231
+ expect(img.attributes.keys).to contain_exactly("src"),
232
+ "expected only `src`, got #{img.attributes.keys.inspect} -- output was: #{html.inspect}"
233
+ end
234
+
235
+ it "does not allow attribute breakout via plain relative URLs" do
236
+ test_string = %(<a href="relative/foo%22%09onmouseover=%22alert(1).html">x</a>)
237
+ html, _bad_links = @converter.convert_exported_html(test_string)
238
+
239
+ anchor = Nokogiri::HTML5.fragment(html).at_css("a")
240
+ expect(anchor).not_to be_nil
241
+ expect(anchor["onmouseover"]).to be_nil,
242
+ "attribute breakout: produced live onmouseover -- output was: #{html.inspect}"
243
+ expect(anchor.attributes.keys).to contain_exactly("href"),
244
+ "expected only `href`, got #{anchor.attributes.keys.inspect} -- output was: #{html.inspect}"
245
+ end
246
+
247
+ it "does not allow attribute breakout via $CANVAS_COURSE_REFERENCE$/file_ref/ rest" do
248
+ test_string = %(<a href="$CANVAS_COURSE_REFERENCE$/file_ref/E/preview&quot; onmouseover=&quot;alert(1)">x</a>)
249
+ html, _bad_links = @converter.convert_exported_html(test_string)
250
+
251
+ anchor = Nokogiri::HTML5.fragment(html).at_css("a")
252
+ expect(anchor).not_to be_nil
253
+ expect(anchor["onmouseover"]).to be_nil,
254
+ "attribute breakout via file_ref rest -- output was: #{html.inspect}"
255
+ expect(anchor.attributes.keys).to contain_exactly("href"),
256
+ "expected only `href`, got #{anchor.attributes.keys.inspect} -- output was: #{html.inspect}"
257
+ end
258
+
259
+ it "does not allow attribute breakout via $CANVAS_COURSE_REFERENCE$/modules/items/ query" do
260
+ test_string = %(<a href="$CANVAS_COURSE_REFERENCE$/modules/items/C?foo=bar&quot; onmouseover=&quot;alert(1)">x</a>)
261
+ html, _bad_links = @converter.convert_exported_html(test_string)
262
+
263
+ anchor = Nokogiri::HTML5.fragment(html).at_css("a")
264
+ expect(anchor).not_to be_nil
265
+ expect(anchor["onmouseover"]).to be_nil,
266
+ "attribute breakout via module_item query -- output was: #{html.inspect}"
267
+ expect(anchor.attributes.keys).to contain_exactly("href"),
268
+ "expected only `href`, got #{anchor.attributes.keys.inspect} -- output was: #{html.inspect}"
269
+ end
270
+
271
+ it "does not allow attribute breakout via wiki_page_migration_id fallback" do
272
+ test_string = %(<a href="wiki_page_migration_id=nope&quot; onmouseover=&quot;alert(1)">x</a>)
273
+ html, _bad_links = @converter.convert_exported_html(test_string)
274
+
275
+ anchor = Nokogiri::HTML5.fragment(html).at_css("a")
276
+ expect(anchor).not_to be_nil
277
+ expect(anchor["onmouseover"]).to be_nil,
278
+ "attribute breakout via wiki_page fallback -- output was: #{html.inspect}"
279
+ expect(anchor.attributes.keys).to contain_exactly("href"),
280
+ "expected only `href`, got #{anchor.attributes.keys.inspect} -- output was: #{html.inspect}"
281
+ end
282
+
283
+ it "does not allow text-node injection when placeholder lives in inner_html" do
284
+ payload = "relative/foo%3Cimg%09src=x%09onerror=alert(1)%3E.html"
285
+ test_string = %(<a href="#{payload}">#{payload}</a>)
286
+ html, _bad_links = @converter.convert_exported_html(test_string)
287
+
288
+ anchor = Nokogiri::HTML5.fragment(html).at_css("a")
289
+ expect(anchor).not_to be_nil
290
+ expect(anchor.css("img")).to be_empty,
291
+ "text-node injection: produced live <img> child -- output was: #{html.inspect}"
292
+ end
293
+
294
+ it "does not allow attribute injection via gsub! pre-match back-reference" do
295
+ test_string = %q(<img src="wiki_page_migration_id=NONEXIST\`onerror=alert(1) ">)
296
+ html, _bad_links = @converter.convert_exported_html(test_string)
297
+
298
+ img = Nokogiri::HTML5.fragment(html).at_css("img")
299
+ expect(img).not_to be_nil
300
+ expect(img["onerror"]).to be_nil,
301
+ "attribute injection via gsub backref: produced live onerror -- output was: #{html.inspect}"
302
+ expect(img.attributes.keys).to contain_exactly("src"),
303
+ "expected only `src`, got #{img.attributes.keys.inspect} -- output was: #{html.inspect}"
304
+ end
305
+
306
+ it "does not allow media_object cross-substitution into attribute context" do
307
+ attacker_video = %q(<video data-media-id="m-stuff" data-media-type="video"><source src="$IMS-CC-FILEBASE$/test.png" data-media-id="m-stuff" data-media-type="video"></video>)
308
+ preview = Nokogiri::HTML5.fragment(attacker_video, max_tree_depth: 10_000)
309
+ preview.search("source[data-media-type],source[data-media-id]").each do |source|
310
+ next unless %w[audio video].include?(source.parent.name)
311
+ media_node = source.parent
312
+ media_node.name = "iframe"
313
+ media_node["src"] = source["src"]
314
+ source.remove
315
+ end
316
+ predicted_placeholder = "LINK.PLACEHOLDER_#{Digest::MD5.hexdigest(preview.at_css("iframe").to_xml)}"
317
+
318
+ attack_html = %Q(<a href="evil/#{predicted_placeholder}">link</a>#{attacker_video})
319
+ html, _ = @converter.convert_exported_html(attack_html)
320
+
321
+ anchor = Nokogiri::HTML5.fragment(html).at_css("a")
322
+ expect(anchor).not_to be_nil
323
+ expect(anchor.attributes.keys).to contain_exactly("href"),
324
+ "media_object cross-substitution injected attributes onto anchor: " \
325
+ "#{anchor.attributes.keys.inspect} -- output was: #{html.inspect}"
326
+ end
327
+ end
328
+
193
329
  it "prepends course files for unrecognized relative urls" do
194
330
  test_string = %(<a href="/relative/path/to/file">Linkage</a>)
195
331
  html, bad_links = @converter.convert_exported_html(test_string)
@@ -438,5 +574,198 @@ describe CanvasLinkMigrator::ImportedHtmlConverter do
438
574
  link = @converter.convert_single_link(test_string)
439
575
  expect(link).to eq "/courses/2/files/9?verifier=u9&type=video&amp=&embedded=true"
440
576
  end
577
+
578
+ it "handles $CANVAS_COURSE_REFERENCE$/ without crashing" do
579
+ link = @converter.convert_single_link("$CANVAS_COURSE_REFERENCE$/")
580
+ expect(link).to eq "#{@path}"
581
+ end
582
+
583
+ it "handles $CANVAS_COURSE_REFERENCE$/ with link_type: :media_object without crashing" do
584
+ # Not sure this is a realistic case, but we should at least not crash on it
585
+ link = @converter.convert_single_link("$CANVAS_COURSE_REFERENCE$/", link_type: :media_object)
586
+ expect(link).to eq "#{@path}"
587
+ end
588
+
589
+ it "handles external URLs without crashing" do
590
+ link = @converter.convert_single_link("http://example.com/")
591
+ expect(link).to eq "http://example.com/"
592
+ end
593
+
594
+ it "returns nil for failed assignment substitution" do
595
+ link = @converter.convert_single_link("$CANVAS_OBJECT_REFERENCE$/assignments/noexist")
596
+ # passing thru might also be acceptable
597
+ expect(link).to be_nil
598
+ end
599
+
600
+ it "doesn't care about existence of wiki page slugs" do
601
+ link = @converter.convert_single_link("$WIKI_REFERENCE$/pages/slug-no-exist")
602
+ expect(link).to eq "/courses/2/pages/slug-no-exist"
603
+ end
604
+
605
+ it "doesn't care about existence of wiki page slugs (with fragment)" do
606
+ link = @converter.convert_single_link("$WIKI_REFERENCE$/pages/slug-no-exist#hello-world")
607
+ expect(link).to eq "/courses/2/pages/slug-no-exist#hello-world"
608
+ end
609
+
610
+ it "passes through incorrectly used $CANVAS_OBJECT_REFERENCE$" do
611
+ link = @converter.convert_single_link("http://example.com/courses/123/assignments/$CANVAS_OBJECT_REFERENCE$")
612
+ # returning nil might also be acceptable
613
+ expect(link).to eq "http://example.com/courses/123/assignments/$CANVAS_OBJECT_REFERENCE$"
614
+ end
615
+
616
+ it "returns nil for failed assignment substitution (on a different domain)" do
617
+ link = @converter.convert_single_link("http://pineapple.edu/$CANVAS_OBJECT_REFERENCE$/assignments/g2fac96de3e3dc1270155dddedb5bb1ce")
618
+ # passing through might also be acceptable
619
+ expect(link).to be_nil
620
+ end
621
+
622
+ it "returns nil for failed assignment substitution (used instead of host)" do
623
+ link = @converter.convert_single_link("http://$CANVAS_OBJECT_REFERENCE$/assignments/g2fac96de3e3dc1270155dddedb5bb1ce")
624
+ expect(link).to be_nil
625
+ end
626
+
627
+ it "passes through $CANVAS_COURSE_REFERENCE$ used weirdly" do
628
+ link = @converter.convert_single_link("$CANVAS_COURSE_REFERENCE$<b>/foo")
629
+ # nil would probably also be acceptable
630
+ expect(link).to eq "$CANVAS_COURSE_REFERENCE$<b>/foo"
631
+ end
632
+
633
+ it "passes through $INVALID_KEYWORD$" do
634
+ link = @converter.convert_single_link("http://example.com/$INVALID_KEYWORD$/path")
635
+ expect(link).to eq "http://example.com/$INVALID_KEYWORD$/path"
636
+ end
637
+
638
+ it "handles %24CANVAS_OBJECT_REFERENCE%24 like $CANVAS_OBJECT_REFERENCE$" do
639
+ link = @converter.convert_single_link("%24CANVAS_OBJECT_REFERENCE%24/assignments/I")
640
+ expect(link).to eq "/courses/2/assignments/12"
641
+ end
642
+
643
+ it "preserves absolute URLs with query params and fragments" do
644
+ link = @converter.convert_single_link("https://example.com/123?456#789")
645
+ expect(link).to eq "https://example.com/123?456#789"
646
+ end
647
+
648
+ it "preserves absolute URLs to other courses" do
649
+ link = @converter.convert_single_link("https://example.com/courses/123/assignments/123")
650
+ expect(link).to eq "https://example.com/courses/123/assignments/123"
651
+ end
652
+
653
+ it "converts assignment reference by migration id" do
654
+ link = @converter.convert_single_link("$CANVAS_OBJECT_REFERENCE$/assignments/I")
655
+ expect(link).to eq "/courses/2/assignments/12"
656
+ end
657
+
658
+ it "relative-izes absolute URLs in course domains with fragments" do
659
+ # I'm not 100% sure how important this case is, but this follows the
660
+ # behavior of translated HTML
661
+ link = @converter.convert_single_link("http://apple.edu/courses/18/settings#tab-navigation")
662
+ expect(link).to eq "/courses/18/settings#tab-navigation"
663
+ end
664
+
665
+ it "converts course reference with fragment" do
666
+ link = @converter.convert_single_link("$CANVAS_COURSE_REFERENCE$/settings#tab-navigation")
667
+ expect(link).to eq "/courses/2/settings#tab-navigation"
668
+ end
669
+
670
+ it "converts wiki reference by slug" do
671
+ link = @converter.convert_single_link("$WIKI_REFERENCE$/pages/slug-a")
672
+ expect(link).to eq "/courses/2/pages/slug-a"
673
+ end
674
+
675
+ it "converts wiki reference with fragment" do
676
+ link = @converter.convert_single_link("$WIKI_REFERENCE$/pages/slug-a#hello-world")
677
+ expect(link).to eq "/courses/2/pages/slug-a#hello-world"
678
+ end
679
+
680
+ it "preserves absolute URLs with invalid keywords" do
681
+ link = @converter.convert_single_link("http://example.com/$WHATEVER$/foo")
682
+ expect(link).to eq "http://example.com/$WHATEVER$/foo"
683
+ end
684
+
685
+ it "preserves absolute URLs with malformed $123$ syntax" do
686
+ link = @converter.convert_single_link("http://example.com/$123$/foo")
687
+ expect(link).to eq "http://example.com/$123$/foo"
688
+ end
689
+
690
+ it "preserves absolute URLs with errant $ characters" do
691
+ link = @converter.convert_single_link("http://example.com/$/foo")
692
+ expect(link).to eq "http://example.com/$/foo"
693
+ end
694
+
695
+ it "doesn't crash on http://example.com/$CANVAS_COURSE_REFERENCE$/foo" do
696
+ link = @converter.convert_single_link("http://example.com/$CANVAS_COURSE_REFERENCE$/foo")
697
+ # I don't know if we actually care what happens here, as long as it doesn't crash.
698
+ # this seems to follow existing behavior for HTML translation
699
+ expect(link).to eq "/courses/2/foo"
700
+ end
701
+
702
+ it "passes through malformed reference with spaces" do
703
+ link = @converter.convert_single_link("$ $/path")
704
+ expect(link).to eq "$ $/path"
705
+ end
706
+
707
+ it "handles $CANVAS_COURSE_REFERENCE$ with other stuff" do
708
+ link = @converter.convert_single_link("$CANVAS_COURSE_REFERENCE$/assignments/")
709
+ expect(link).to eq "/courses/2/assignments/"
710
+ end
711
+
712
+ describe 'invalid references returning file_contents' do
713
+ # These are invalid or malformed reference syntax that get treated as relative file paths.
714
+ # They get prepended with the course file_contents path, matching the existing behavior
715
+ # for HTML content. These cause 'broken link' pages to show up in the UI.
716
+ # That said, I'm not 100% sure we care that much about the actual values as they are
717
+ # invalid, so feel free to change the specs if we decide to handle these
718
+ # differently (e.g. return nil)
719
+
720
+ it "passes through $CANVAS_COURSE_REFERENCE" do
721
+ link = @converter.convert_single_link("$CANVAS_COURSE_REFERENCE")
722
+ expect(link).to eq("/courses/2/file_contents/course%20files/$CANVAS_COURSE_REFERENCE")
723
+ end
724
+
725
+ it "passes through lone $123" do
726
+ link = @converter.convert_single_link("/$123")
727
+ expect(link).to eq "/courses/2/file_contents/course%20files/$123"
728
+ end
729
+
730
+ it "passes through errant $" do
731
+ link = @converter.convert_single_link("/$")
732
+ expect(link).to eq "/courses/2/file_contents/course%20files/$"
733
+ end
734
+
735
+ it "passes through errant lone $123" do
736
+ link = @converter.convert_single_link("$123")
737
+ expect(link).to eq "/courses/2/file_contents/course%20files/$123"
738
+ end
739
+
740
+ it "passes through errant lone $" do
741
+ link = @converter.convert_single_link("$")
742
+ expect(link).to eq "/courses/2/file_contents/course%20files/$"
743
+ end
744
+
745
+ it "passes through starting $INVALID_KEYWORD$" do
746
+ link = @converter.convert_single_link("$INVALID_KEYWORD$/path")
747
+ expect(link).to eq "/courses/2/file_contents/course%20files/$INVALID_KEYWORD$/path"
748
+ end
749
+
750
+ it "passes through errant $123$" do
751
+ link = @converter.convert_single_link("$123$/path")
752
+ expect(link).to eq "/courses/2/file_contents/course%20files/$123$/path"
753
+ end
754
+
755
+ it "doesn't crash when given $CANVAS_COURSE_REFERENCE$" do
756
+ link = @converter.convert_single_link("$CANVAS_COURSE_REFERENCE$")
757
+ expect(link).to eq "/courses/2/file_contents/course%20files/$CANVAS_COURSE_REFERENCE$"
758
+ end
759
+
760
+ it "doesn't crash on /courses/123/assignments/$CANVAS_OBJECT_REFERENCE$" do
761
+ link = @converter.convert_single_link("/courses/123/assignments/$CANVAS_OBJECT_REFERENCE$")
762
+ expect(link).to eq "/courses/2/file_contents/course%20files/courses/123/assignments/$CANVAS_OBJECT_REFERENCE$"
763
+ end
764
+
765
+ it "handles $$/path" do
766
+ link = @converter.convert_single_link("$$/path")
767
+ expect(link).to eq "/courses/2/file_contents/course%20files/$$/path"
768
+ end
769
+ end
441
770
  end
442
771
  end
@@ -8,7 +8,6 @@
8
8
  "test.png": "E",
9
9
  "media_objects/0_bq09qam2": "F"
10
10
  },
11
- "contains_migration_ids": true,
12
11
  "destination_course": "2",
13
12
  "destination_hosts": [
14
13
  "apple.edu",
@@ -7,7 +7,6 @@
7
7
  "lolcat.mp3": "H",
8
8
  "test.png": "E"
9
9
  },
10
- "contains_migration_ids": true,
11
10
  "destination_course": "2",
12
11
  "destination_hosts": [
13
12
  "apple.edu",
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canvas_link_migrator
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.18
4
+ version: 1.0.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mysti Lilla
8
8
  - James Logan
9
9
  - Sarah Gerard
10
10
  - Math Costa
11
- autorequire:
11
+ autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2025-07-15 00:00:00.000000000 Z
14
+ date: 2026-05-15 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activesupport
@@ -27,6 +27,20 @@ dependencies:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
29
  version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: bigdecimal
32
+ requirement: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
30
44
  - !ruby/object:Gem::Dependency
31
45
  name: nokogiri
32
46
  requirement: !ruby/object:Gem::Requirement
@@ -125,7 +139,7 @@ dependencies:
125
139
  - - ">="
126
140
  - !ruby/object:Gem::Version
127
141
  version: '0'
128
- description:
142
+ description:
129
143
  email:
130
144
  - mysti@instructure.com
131
145
  - james.logan@instructure.com
@@ -150,11 +164,11 @@ files:
150
164
  - spec/fixtures/canvas_resource_map_pages.json
151
165
  - spec/spec_helper.rb
152
166
  - test.sh
153
- homepage:
167
+ homepage:
154
168
  licenses: []
155
169
  metadata:
156
170
  source_code_uri: https://github.com/instructure/canvas_link_migrator
157
- post_install_message:
171
+ post_install_message:
158
172
  rdoc_options: []
159
173
  require_paths:
160
174
  - lib
@@ -162,15 +176,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
162
176
  requirements:
163
177
  - - ">="
164
178
  - !ruby/object:Gem::Version
165
- version: '2.7'
179
+ version: '3.1'
166
180
  required_rubygems_version: !ruby/object:Gem::Requirement
167
181
  requirements:
168
182
  - - ">="
169
183
  - !ruby/object:Gem::Version
170
184
  version: '0'
171
185
  requirements: []
172
- rubygems_version: 3.1.6
173
- signing_key:
186
+ rubygems_version: 3.3.27
187
+ signing_key:
174
188
  specification_version: 4
175
189
  summary: Instructure gem for migrating Canvas style rich content
176
190
  test_files: []