canvas_link_migrator 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/canvas_link_migrator/imported_html_converter.rb +68 -0
- data/lib/canvas_link_migrator/link_parser.rb +243 -0
- data/lib/canvas_link_migrator/link_replacer.rb +48 -0
- data/lib/canvas_link_migrator/link_resolver.rb +251 -0
- data/lib/canvas_link_migrator/resource_map_service.rb +88 -0
- data/lib/canvas_link_migrator/version.rb +3 -0
- data/lib/canvas_link_migrator.rb +33 -0
- data/spec/canvas_link_migrator/imported_html_converter_spec.rb +279 -0
- data/spec/canvas_link_migrator/link_resolver_spec.rb +66 -0
- data/spec/canvas_link_migrator_spec.rb +41 -0
- data/spec/fixtures/canvas_resource_map.json +73 -0
- data/spec/spec_helper.rb +21 -0
- data/test.sh +4 -0
- metadata +139 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 993c3428360b15aea7b4eca2ee43652342f4a700e23837794ffe913e1572dea1
|
4
|
+
data.tar.gz: 5081c3aa602c6cf78fc3bd1e152a4c8e12f419fc6853ab72a9d09f86ba42498e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d8d3068542e30d89ddffb809c95b59ba1a2665a515a65d8f6359ec3c1f7fd204560794a3d016fc861490561215e82aad126fdb1ff0ef0f9cd8d20c6ebc12c7bb
|
7
|
+
data.tar.gz: b18abcf3c0b4e325cca73e10a905fea2d09c0f444230766eae955f842f8bde394765254cc9c9626ee7d6510251069e0aeed9061c81eb2c93df6b52e485aa5212
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (C) 2023 - present Instructure, Inc.
|
5
|
+
#
|
6
|
+
# This file is part of Canvas.
|
7
|
+
#
|
8
|
+
# Canvas is free software: you can redistribute it and/or modify it under
|
9
|
+
# the terms of the GNU Affero General Public License as published by the Free
|
10
|
+
# Software Foundation, version 3 of the License.
|
11
|
+
#
|
12
|
+
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
13
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
14
|
+
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
15
|
+
# details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Affero General Public License along
|
18
|
+
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
|
20
|
+
require "active_support/core_ext/module"
|
21
|
+
|
22
|
+
module CanvasLinkMigrator
|
23
|
+
class ImportedHtmlConverter
|
24
|
+
attr_reader :link_parser, :link_resolver, :migration_id_converter
|
25
|
+
|
26
|
+
def initialize(resource_map: nil, migration_id_converter: nil)
|
27
|
+
@migration_id_converter = migration_id_converter || ResourceMapService.new(resource_map)
|
28
|
+
@link_parser = LinkParser.new(@migration_id_converter)
|
29
|
+
@link_resolver = LinkResolver.new(@migration_id_converter)
|
30
|
+
end
|
31
|
+
|
32
|
+
delegate :convert, to: :link_parser
|
33
|
+
delegate :resolver_links!, to: :link_resolver
|
34
|
+
|
35
|
+
def convert_exported_html(input_html)
|
36
|
+
new_html = link_parser.convert(input_html, "type", "lookup_id", "field")
|
37
|
+
replace!(new_html)
|
38
|
+
|
39
|
+
# missing links comes back as a list for all types and fields, but if the user's only
|
40
|
+
# sending one piece of html at a time, we only need the first set of missing links,
|
41
|
+
# and only the actual missing links, not the look up information we set in this method
|
42
|
+
bad_links = missing_links&.first&.dig(:missing_links)
|
43
|
+
link_parser.reset!
|
44
|
+
[new_html, bad_links]
|
45
|
+
end
|
46
|
+
|
47
|
+
def replace!(placeholder_html)
|
48
|
+
link_map = link_parser.unresolved_link_map
|
49
|
+
return unless link_map.present?
|
50
|
+
|
51
|
+
link_resolver.resolve_links!(link_map)
|
52
|
+
LinkReplacer.sub_placeholders!(placeholder_html, link_map.values.map(&:values).flatten)
|
53
|
+
placeholder_html
|
54
|
+
end
|
55
|
+
|
56
|
+
def missing_links
|
57
|
+
link_parser.unresolved_link_map.each_with_object([]) do |(item_key, field_links), bad_links|
|
58
|
+
field_links.each do |field, links|
|
59
|
+
unresolved_links = links.select { |link| link[:replaced] && (link[:missing_url] || !link[:new_value]) }
|
60
|
+
unresolved_links = unresolved_links.map { |link| link.slice(:link_type, :missing_url) }
|
61
|
+
next unless unresolved_links.any?
|
62
|
+
|
63
|
+
bad_links << { object_lookup_id: item_key[:migration_id], object_type: item_key[:type], object_field: field, missing_links: unresolved_links }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,243 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (C) 2015 - present Instructure, Inc.
|
5
|
+
#
|
6
|
+
# This file is part of Canvas.
|
7
|
+
#
|
8
|
+
# Canvas is free software: you can redistribute it and/or modify it under
|
9
|
+
# the terms of the GNU Affero General Public License as published by the Free
|
10
|
+
# Software Foundation, version 3 of the License.
|
11
|
+
#
|
12
|
+
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
13
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
14
|
+
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
15
|
+
# details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Affero General Public License along
|
18
|
+
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
|
20
|
+
require "nokogiri"
|
21
|
+
require "digest"
|
22
|
+
|
23
|
+
module CanvasLinkMigrator
|
24
|
+
class LinkParser
|
25
|
+
REFERENCE_KEYWORDS = %w[CANVAS_COURSE_REFERENCE CANVAS_OBJECT_REFERENCE WIKI_REFERENCE IMS_CC_FILEBASE IMS-CC-FILEBASE].freeze
|
26
|
+
LINK_PLACEHOLDER = "LINK.PLACEHOLDER"
|
27
|
+
KNOWN_REFERENCE_TYPES = %w[
|
28
|
+
announcements
|
29
|
+
appointment_participants
|
30
|
+
assignment_groups
|
31
|
+
assignments
|
32
|
+
attachments
|
33
|
+
calendar_events
|
34
|
+
context_external_tools
|
35
|
+
context_module_tags
|
36
|
+
context_modules
|
37
|
+
course_paces
|
38
|
+
created_learning_outcomes
|
39
|
+
discussion_entries
|
40
|
+
discussion_topics
|
41
|
+
external_feeds
|
42
|
+
grading_standards
|
43
|
+
groups
|
44
|
+
learning_outcome_groups
|
45
|
+
learning_outcome_links
|
46
|
+
learning_outcomes
|
47
|
+
linked_learning_outcomes
|
48
|
+
media_attachments_iframe
|
49
|
+
modules
|
50
|
+
pages
|
51
|
+
quizzes
|
52
|
+
rubrics
|
53
|
+
wiki
|
54
|
+
wiki_pages
|
55
|
+
].freeze
|
56
|
+
CONTAINER_TYPES = %w[div p body].freeze
|
57
|
+
LINK_ATTRS = %w[rel href src srcset data value longdesc data-download-url].freeze
|
58
|
+
RCE_MEDIA_TYPES = %w[audio video].freeze
|
59
|
+
|
60
|
+
attr_reader :unresolved_link_map, :migration_query_service
|
61
|
+
|
62
|
+
def initialize(migration_query_service)
|
63
|
+
@migration_query_service = migration_query_service
|
64
|
+
reset!
|
65
|
+
end
|
66
|
+
|
67
|
+
def reset!
|
68
|
+
@unresolved_link_map = {}
|
69
|
+
end
|
70
|
+
|
71
|
+
def add_unresolved_link(link, item_type, mig_id, field)
|
72
|
+
key = { type: item_type, migration_id: mig_id }
|
73
|
+
@unresolved_link_map[key] ||= {}
|
74
|
+
@unresolved_link_map[key][field] ||= []
|
75
|
+
@unresolved_link_map[key][field] << link
|
76
|
+
end
|
77
|
+
|
78
|
+
def placeholder(old_value)
|
79
|
+
"#{LINK_PLACEHOLDER}_#{Digest::MD5.hexdigest(old_value)}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def convert(html, item_type, mig_id, field, remove_outer_nodes_if_one_child: nil)
|
83
|
+
mig_id = mig_id.to_s
|
84
|
+
doc = Nokogiri::HTML5(html || "")
|
85
|
+
|
86
|
+
# Replace source tags with iframes
|
87
|
+
doc.search("source[data-media-id]").each do |source|
|
88
|
+
next unless RCE_MEDIA_TYPES.include?(source.parent.name)
|
89
|
+
|
90
|
+
media_node = source.parent
|
91
|
+
media_node.name = "iframe"
|
92
|
+
media_node["src"] = source["src"]
|
93
|
+
source.remove
|
94
|
+
end
|
95
|
+
|
96
|
+
doc.search("*").each do |node|
|
97
|
+
LINK_ATTRS.each do |attr|
|
98
|
+
convert_link(node, attr, item_type, mig_id, field)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
node = doc.at_css("body")
|
103
|
+
return "" unless node
|
104
|
+
|
105
|
+
if remove_outer_nodes_if_one_child
|
106
|
+
while node.children.size == 1 && node.child.child
|
107
|
+
break unless CONTAINER_TYPES.member?(node.child.name) && node.child.attributes.blank?
|
108
|
+
|
109
|
+
node = node.child
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
node.inner_html
|
114
|
+
rescue Nokogiri::SyntaxError
|
115
|
+
""
|
116
|
+
end
|
117
|
+
|
118
|
+
def convert_link(node, attr, item_type, mig_id, field)
|
119
|
+
return unless node[attr].present?
|
120
|
+
|
121
|
+
if attr == "value" &&
|
122
|
+
!(node[attr] =~ /IMS(?:-|_)CC(?:-|_)FILEBASE/ || node[attr].include?("CANVAS_COURSE_REFERENCE"))
|
123
|
+
return
|
124
|
+
end
|
125
|
+
|
126
|
+
url = node[attr].dup
|
127
|
+
REFERENCE_KEYWORDS.each do |ref|
|
128
|
+
url.gsub!("%24#{ref}%24", "$#{ref}$")
|
129
|
+
end
|
130
|
+
|
131
|
+
result = parse_url(url, node, attr)
|
132
|
+
if result[:resolved]
|
133
|
+
# resolved, just replace and carry on
|
134
|
+
new_url = result[:new_url] || url
|
135
|
+
unless CanvasLinkMigrator.relative_url?(new_url)
|
136
|
+
# perform configured substitutions
|
137
|
+
if (processed_url = @migration_query_service.process_domain_substitutions(new_url))
|
138
|
+
new_url = processed_url
|
139
|
+
end
|
140
|
+
# relative-ize absolute links outside the course but inside our domain
|
141
|
+
# (analogous to what is done in Api#process_incoming_html_content)
|
142
|
+
begin
|
143
|
+
uri = URI.parse(new_url)
|
144
|
+
account_hosts = @migration_query_service.context_hosts.map { |h| h.split(":").first }
|
145
|
+
if account_hosts.include?(uri.host)
|
146
|
+
uri.scheme = uri.host = uri.port = nil
|
147
|
+
new_url = uri.to_s
|
148
|
+
end
|
149
|
+
rescue URI::InvalidURIError, URI::InvalidComponentError
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
end
|
153
|
+
node[attr] = new_url
|
154
|
+
else
|
155
|
+
result.delete(:resolved)
|
156
|
+
if result[:link_type] == :media_object
|
157
|
+
# because we may actually change the media comment node itself
|
158
|
+
# (rather than just replacing a value), we're going to
|
159
|
+
# replace the entire node with a placeholder
|
160
|
+
result[:old_value] = node.to_xml
|
161
|
+
result[:placeholder] = placeholder(result[:old_value])
|
162
|
+
placeholder_node = Nokogiri::HTML5.fragment(result[:placeholder])
|
163
|
+
|
164
|
+
node.replace(placeholder_node)
|
165
|
+
else
|
166
|
+
result[:old_value] = node[attr]
|
167
|
+
result[:placeholder] = placeholder(result[:old_value])
|
168
|
+
node[attr] = result[:placeholder]
|
169
|
+
end
|
170
|
+
add_unresolved_link(result, item_type, mig_id, field)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def unresolved(type, data = {})
|
175
|
+
{ resolved: false, link_type: type }.merge(data)
|
176
|
+
end
|
177
|
+
|
178
|
+
def resolved(new_url = nil)
|
179
|
+
{ resolved: true, new_url: new_url }
|
180
|
+
end
|
181
|
+
|
182
|
+
# returns a hash with resolution status and data to hold onto if unresolved
|
183
|
+
def parse_url(url, node, attr)
|
184
|
+
if url =~ /wiki_page_migration_id=(.*)/
|
185
|
+
unresolved(:wiki_page, migration_id: $1)
|
186
|
+
elsif url =~ /discussion_topic_migration_id=(.*)/
|
187
|
+
unresolved(:discussion_topic, migration_id: $1)
|
188
|
+
elsif url =~ %r{\$CANVAS_COURSE_REFERENCE\$/modules/items/([^?]*)(\?.*)?}
|
189
|
+
unresolved(:module_item, migration_id: $1, query: $2)
|
190
|
+
elsif url =~ %r{\$CANVAS_COURSE_REFERENCE\$/file_ref/([^/?#]+)(.*)}
|
191
|
+
unresolved(:file_ref,
|
192
|
+
migration_id: $1,
|
193
|
+
rest: $2,
|
194
|
+
in_media_iframe: attr == "src" && ["iframe", "source"].include?(node.name) && node["data-media-id"])
|
195
|
+
elsif url =~ %r{(?:\$CANVAS_OBJECT_REFERENCE\$|\$WIKI_REFERENCE\$)/([^/]*)/([^?]*)(\?.*)?}
|
196
|
+
if KNOWN_REFERENCE_TYPES.include?($1)
|
197
|
+
unresolved(:object, type: $1, migration_id: $2, query: $3)
|
198
|
+
else
|
199
|
+
# If the `type` is not known, there's something amiss...
|
200
|
+
@migration_query_service.report_link_parse_warning($1)
|
201
|
+
resolved(url)
|
202
|
+
end
|
203
|
+
elsif url =~ %r{\$CANVAS_COURSE_REFERENCE\$/(.*)}
|
204
|
+
resolved("#{@migration_query_service.context_path}/#{$1}")
|
205
|
+
|
206
|
+
elsif url =~ %r{\$IMS(?:-|_)CC(?:-|_)FILEBASE\$/(.*)}
|
207
|
+
rel_path = URI::DEFAULT_PARSER.unescape($1)
|
208
|
+
if (attr == "href" && node["class"]&.include?("instructure_inline_media_comment")) ||
|
209
|
+
(attr == "src" && ["iframe", "source"].include?(node.name) && node["data-media-id"])
|
210
|
+
unresolved(:media_object, rel_path: rel_path)
|
211
|
+
else
|
212
|
+
unresolved(:file, rel_path: rel_path)
|
213
|
+
end
|
214
|
+
elsif (attr == "href" && node["class"]&.include?("instructure_inline_media_comment")) ||
|
215
|
+
(attr == "src" && ["iframe", "source"].include?(node.name) && node["data-media-id"])
|
216
|
+
# Course copy media reference, leave it alone
|
217
|
+
resolved
|
218
|
+
elsif @migration_query_service.supports_embedded_images && attr == "src" && (info_match = url.match(%r{\Adata:(?<mime_type>[-\w]+/[-\w+.]+)?;base64,(?<image>.*)}m))
|
219
|
+
result = @migration_query_service.link_embedded_image(info_match)
|
220
|
+
if result[:resolved]
|
221
|
+
resolved(result[:url])
|
222
|
+
else
|
223
|
+
unresolved(:file, rel_path: result[:url])
|
224
|
+
end
|
225
|
+
elsif # rubocop:disable Lint/DuplicateBranch
|
226
|
+
# Equation image, leave it alone
|
227
|
+
(attr == "src" && node["class"] && node["class"].include?("equation_image")) || # rubocop:disable Layout/ConditionPosition
|
228
|
+
# The file is in the context of an AQ, leave the link alone
|
229
|
+
url =~ %r{\A/assessment_questions/\d+/files/\d+} ||
|
230
|
+
# This points to a specific file already, leave it alone
|
231
|
+
url =~ %r{\A/courses/\d+/files/\d+} ||
|
232
|
+
!@migration_query_service.fix_relative_urls? ||
|
233
|
+
# It's just a link to an anchor, leave it alone
|
234
|
+
url.start_with?("#")
|
235
|
+
resolved
|
236
|
+
elsif CanvasLinkMigrator.relative_url?(url)
|
237
|
+
unresolved(:file, rel_path: URI::DEFAULT_PARSER.unescape(url))
|
238
|
+
else # rubocop:disable Lint/DuplicateBranch
|
239
|
+
resolved
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (C) 2015 - present Instructure, Inc.
|
5
|
+
#
|
6
|
+
# This file is part of Canvas.
|
7
|
+
#
|
8
|
+
# Canvas is free software: you can redistribute it and/or modify it under
|
9
|
+
# the terms of the GNU Affero General Public License as published by the Free
|
10
|
+
# Software Foundation, version 3 of the License.
|
11
|
+
#
|
12
|
+
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
13
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
14
|
+
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
15
|
+
# details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Affero General Public License along
|
18
|
+
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
|
20
|
+
module CanvasLinkMigrator
|
21
|
+
class LinkReplacer
|
22
|
+
# returns false if no substitutions were made
|
23
|
+
def self.sub_placeholders!(html, links)
|
24
|
+
subbed = false
|
25
|
+
links.each do |link|
|
26
|
+
new_value = link[:new_value] || link[:old_value]
|
27
|
+
if html.gsub!(link[:placeholder], new_value)
|
28
|
+
link[:replaced] = true
|
29
|
+
subbed = true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
subbed
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.recursively_sub_placeholders!(object, links)
|
36
|
+
subbed = false
|
37
|
+
case object
|
38
|
+
when Hash
|
39
|
+
object.each_value { |o| subbed = true if recursively_sub_placeholders!(o, links) }
|
40
|
+
when Array
|
41
|
+
object.each { |o| subbed = true if recursively_sub_placeholders!(o, links) }
|
42
|
+
when String
|
43
|
+
subbed = sub_placeholders!(object, links)
|
44
|
+
end
|
45
|
+
subbed
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,251 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (C) 2015 - present Instructure, Inc.
|
5
|
+
#
|
6
|
+
# This file is part of Canvas.
|
7
|
+
#
|
8
|
+
# Canvas is free software: you can redistribute it and/or modify it under
|
9
|
+
# the terms of the GNU Affero General Public License as published by the Free
|
10
|
+
# Software Foundation, version 3 of the License.
|
11
|
+
#
|
12
|
+
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
13
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
14
|
+
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
15
|
+
# details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Affero General Public License along
|
18
|
+
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
|
20
|
+
require "active_support/core_ext/object"
|
21
|
+
|
22
|
+
module CanvasLinkMigrator
|
23
|
+
class LinkResolver
|
24
|
+
def initialize(migration_id_converter)
|
25
|
+
@migration_id_converter = migration_id_converter
|
26
|
+
end
|
27
|
+
|
28
|
+
def resolve_links!(link_map)
|
29
|
+
link_map.each_value do |field_links|
|
30
|
+
field_links.each_value do |links|
|
31
|
+
links.each do |link|
|
32
|
+
resolve_link!(link)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def context_path
|
39
|
+
@migration_id_converter.context_path
|
40
|
+
end
|
41
|
+
|
42
|
+
# finds the :new_value to use to replace the placeholder
|
43
|
+
def resolve_link!(link)
|
44
|
+
case link[:link_type]
|
45
|
+
when :wiki_page
|
46
|
+
if (linked_wiki_url = @migration_id_converter.convert_wiki_page_migration_id_to_slug(link[:migration_id]))
|
47
|
+
link[:new_value] = "#{context_path}/pages/#{linked_wiki_url}#{link[:query]}"
|
48
|
+
end
|
49
|
+
when :discussion_topic
|
50
|
+
if (linked_topic_id = @migration_id_converter.convert_discussion_topic_migration_id(link[:migration_id]))
|
51
|
+
link[:new_value] = "#{context_path}/discussion_topics/#{linked_topic_id}#{link[:query]}"
|
52
|
+
end
|
53
|
+
when :module_item
|
54
|
+
if (tag_id = @migration_id_converter.convert_context_module_tag_migration_id(link[:migration_id]))
|
55
|
+
link[:new_value] = "#{context_path}/modules/items/#{tag_id}#{link[:query]}"
|
56
|
+
end
|
57
|
+
when :object
|
58
|
+
type = link[:type]
|
59
|
+
migration_id = link[:migration_id]
|
60
|
+
|
61
|
+
type_for_url = type
|
62
|
+
type = "context_modules" if type == "modules"
|
63
|
+
type = "pages" if type == "wiki"
|
64
|
+
if type == "pages"
|
65
|
+
query = resolve_module_item_query(nil, link[:query])
|
66
|
+
link[:new_value] = "#{context_path}/pages/#{migration_id}#{query}"
|
67
|
+
elsif type == "attachments"
|
68
|
+
att_id = @migration_id_converter.convert_attachment_migration_id(migration_id)
|
69
|
+
if att_id
|
70
|
+
link[:new_value] = "#{context_path}/files/#{att_id}/preview"
|
71
|
+
end
|
72
|
+
elsif type == "media_attachments_iframe"
|
73
|
+
att_id = @migration_id_converter.convert_attachment_migration_id(migration_id)
|
74
|
+
link[:new_value] = att_id ? "/media_attachments_iframe/#{att_id}#{link[:query]}" : link[:old_value]
|
75
|
+
else
|
76
|
+
object_id = @migration_id_converter.convert_migration_id(type, migration_id)
|
77
|
+
if object_id
|
78
|
+
query = resolve_module_item_query(nil, link[:query])
|
79
|
+
link[:new_value] = "#{context_path}/#{type_for_url}/#{object_id}#{query}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
when :media_object
|
83
|
+
# because we actually might change the node itself
|
84
|
+
# this part is a little trickier
|
85
|
+
# tl;dr we've replaced the entire node with the placeholder
|
86
|
+
# see LinkParser for details
|
87
|
+
rel_path = link[:rel_path]
|
88
|
+
node = Nokogiri::HTML5.fragment(link[:old_value]).children.first
|
89
|
+
new_url = resolve_media_comment_data(node, rel_path)
|
90
|
+
new_url ||= resolve_relative_file_url(rel_path)
|
91
|
+
|
92
|
+
unless new_url
|
93
|
+
new_url ||= missing_relative_file_url(rel_path)
|
94
|
+
link[:missing_url] = new_url
|
95
|
+
end
|
96
|
+
if ["iframe", "source"].include?(node.name)
|
97
|
+
node["src"] = new_url
|
98
|
+
else
|
99
|
+
node["href"] = new_url
|
100
|
+
end
|
101
|
+
link[:new_value] = node.to_s
|
102
|
+
when :file
|
103
|
+
rel_path = link[:rel_path]
|
104
|
+
new_url = resolve_relative_file_url(rel_path)
|
105
|
+
unless new_url
|
106
|
+
new_url = missing_relative_file_url(rel_path)
|
107
|
+
link[:missing_url] = new_url
|
108
|
+
end
|
109
|
+
link[:new_value] = new_url
|
110
|
+
when :file_ref
|
111
|
+
file_id = @migration_id_converter.convert_attachment_migration_id(link[:migration_id])
|
112
|
+
if file_id
|
113
|
+
rest = link[:rest].presence || "/preview"
|
114
|
+
|
115
|
+
# Icon Maker files should not have the course
|
116
|
+
# context prepended to the URL. This prevents
|
117
|
+
# redirects to non cross-origin friendly urls
|
118
|
+
# during a file fetch
|
119
|
+
if rest.include?("icon_maker_icon=1")
|
120
|
+
link[:new_value] = "/files/#{file_id}#{rest}"
|
121
|
+
else
|
122
|
+
link[:new_value] = "#{context_path}/files/#{file_id}#{rest}"
|
123
|
+
link[:new_value] = "/media_objects_iframe?mediahref=#{link[:new_value]}" if link[:in_media_iframe]
|
124
|
+
end
|
125
|
+
end
|
126
|
+
else
|
127
|
+
raise "unrecognized link_type (#{link[:link_type]}) in unresolved link"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def resolve_module_item_query(_context, query)
|
132
|
+
return query unless query&.include?("module_item_id=")
|
133
|
+
|
134
|
+
original_param = query.sub("?", "").split("&").detect { |p| p.include?("module_item_id=") }
|
135
|
+
mig_id = original_param.split("=").last
|
136
|
+
tag_id = @migration_id_converter.convert_context_module_tag_migration_id(mig_id)
|
137
|
+
return query unless tag_id
|
138
|
+
|
139
|
+
new_param = "module_item_id=#{tag_id}"
|
140
|
+
query.sub(original_param, new_param)
|
141
|
+
end
|
142
|
+
|
143
|
+
def missing_relative_file_url(rel_path)
|
144
|
+
# the rel_path should already be escaped
|
145
|
+
File.join(URI::DEFAULT_PARSER.escape("#{context_path}/file_contents/#{@migration_id_converter.root_folder_name}"), rel_path.gsub(" ", "%20"))
|
146
|
+
end
|
147
|
+
|
148
|
+
def find_file_in_context(rel_path)
|
149
|
+
mig_id = nil
|
150
|
+
# This is for backward-compatibility: canvas attachment filenames are escaped
|
151
|
+
# with '+' for spaces and older exports have files with that instead of %20
|
152
|
+
alt_rel_path = rel_path.tr("+", " ")
|
153
|
+
if @migration_id_converter.attachment_path_id_lookup
|
154
|
+
mig_id ||= @migration_id_converter.attachment_path_id_lookup[rel_path]
|
155
|
+
mig_id ||= @migration_id_converter.attachment_path_id_lookup[alt_rel_path]
|
156
|
+
end
|
157
|
+
if !mig_id && @migration_id_converter.attachment_path_id_lookup_lower
|
158
|
+
mig_id ||= @migration_id_converter.attachment_path_id_lookup_lower[rel_path.downcase]
|
159
|
+
mig_id ||= @migration_id_converter.attachment_path_id_lookup_lower[alt_rel_path.downcase]
|
160
|
+
end
|
161
|
+
|
162
|
+
# This md5 comparison is here to handle faulty cartridges with the migration_id equivalent of an empty string
|
163
|
+
mig_id && mig_id != "gd41d8cd98f00b204e9800998ecf8427e" && @migration_id_converter.lookup_attachment_by_migration_id(mig_id)
|
164
|
+
end
|
165
|
+
|
166
|
+
def resolve_relative_file_url(rel_path)
|
167
|
+
split = rel_path.split("?")
|
168
|
+
qs = split.pop if split.length > 1
|
169
|
+
path = split.join("?")
|
170
|
+
|
171
|
+
# since we can't be sure whether a ? is part of a filename or query string, try it both ways
|
172
|
+
new_url = resolve_relative_file_url_with_qs(path, qs)
|
173
|
+
new_url ||= resolve_relative_file_url_with_qs(rel_path, "") if qs.present?
|
174
|
+
new_url
|
175
|
+
end
|
176
|
+
|
177
|
+
def resolve_relative_file_url_with_qs(rel_path, qs)
|
178
|
+
new_url = nil
|
179
|
+
rel_path_parts = Pathname.new(rel_path).each_filename.to_a
|
180
|
+
|
181
|
+
# e.g. start with "a/b/c.txt" then try "b/c.txt" then try "c.txt"
|
182
|
+
while new_url.nil? && !rel_path_parts.empty?
|
183
|
+
sub_path = File.join(rel_path_parts)
|
184
|
+
if (file = find_file_in_context(sub_path))
|
185
|
+
new_url = "#{context_path}/files/#{file.id}"
|
186
|
+
# support other params in the query string, that were exported from the
|
187
|
+
# original path components and query string. see
|
188
|
+
# CCHelper::file_query_string
|
189
|
+
params = Rack::Utils.parse_nested_query(qs.presence || "")
|
190
|
+
qs = []
|
191
|
+
new_action = ""
|
192
|
+
params.each do |k, v|
|
193
|
+
case k
|
194
|
+
when /canvas_qs_(.*)/
|
195
|
+
qs << "#{Rack::Utils.escape($1)}=#{Rack::Utils.escape(v)}"
|
196
|
+
when /canvas_(.*)/
|
197
|
+
new_action += "/#{$1}"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
new_url += new_action.presence || "/preview"
|
201
|
+
new_url += "?#{qs.join("&")}" if qs.present?
|
202
|
+
end
|
203
|
+
rel_path_parts.shift
|
204
|
+
end
|
205
|
+
new_url
|
206
|
+
end
|
207
|
+
|
208
|
+
def media_iframe_url(media_id, media_type = nil)
|
209
|
+
url = "/media_objects_iframe/#{media_id}"
|
210
|
+
url += "?type=#{media_type}" if media_type.present?
|
211
|
+
url
|
212
|
+
end
|
213
|
+
|
214
|
+
def media_attachment_iframe_url(file_id, media_type = nil)
|
215
|
+
url = "/media_attachments_iframe/#{file_id}"
|
216
|
+
url += "?type=#{media_type}" if media_type.present?
|
217
|
+
url
|
218
|
+
end
|
219
|
+
|
220
|
+
def resolve_media_comment_data(node, rel_path)
|
221
|
+
if (file = find_file_in_context(rel_path[/^[^?]+/])) # strip query string for this search
|
222
|
+
media_id = (file.media_object&.media_id || file.media_entry_id)
|
223
|
+
if media_id && media_id != "maybe"
|
224
|
+
if ["iframe", "source"].include?(node.name)
|
225
|
+
node["data-media-id"] = media_id
|
226
|
+
if node["data-is-media-attachment"]
|
227
|
+
node.delete("data-is-media-attachment")
|
228
|
+
return media_attachment_iframe_url(file.id, node["data-media-type"])
|
229
|
+
else
|
230
|
+
return media_iframe_url(media_id, node["data-media-type"])
|
231
|
+
end
|
232
|
+
else
|
233
|
+
node["id"] = "media_comment_#{media_id}"
|
234
|
+
return "/media_objects/#{media_id}"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
if node["id"] && node["id"] =~ /\Amedia_comment_(.+)\z/
|
240
|
+
"/media_objects/#{$1}"
|
241
|
+
elsif node["data-media-id"].present?
|
242
|
+
media_iframe_url(node["data-media-id"], node["data-media-type"])
|
243
|
+
else
|
244
|
+
node.delete("class")
|
245
|
+
node.delete("id")
|
246
|
+
node.delete("style")
|
247
|
+
nil
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|