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 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