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 +4 -4
- data/lib/canvas_link_migrator/imported_html_converter.rb +8 -5
- data/lib/canvas_link_migrator/link_parser.rb +6 -2
- data/lib/canvas_link_migrator/link_replacer.rb +24 -2
- data/lib/canvas_link_migrator/link_resolver.rb +1 -1
- data/lib/canvas_link_migrator/version.rb +1 -1
- data/spec/canvas_link_migrator/imported_html_converter_spec.rb +329 -0
- data/spec/fixtures/canvas_resource_map.json +0 -1
- data/spec/fixtures/canvas_resource_map_pages.json +0 -1
- metadata +23 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ca1247f98a0eee8de26f1fa32da2ec683f374ea995a695bb4af201d87644795a
|
|
4
|
+
data.tar.gz: 122790ae2f969eee61c75118096d2e9b736fc328e9a27846f8b5b2e82ec7830b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
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 = { '"' => """, "<" => "<", ">" => ">" }.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.
|
|
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
|
-
|
|
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
|
|
|
@@ -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" onmouseover="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" onmouseover="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" onmouseover="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&=&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
|
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.
|
|
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:
|
|
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: '
|
|
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.
|
|
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: []
|