canvas_link_migrator 0.1.0
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 +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
|