Fix media_object source and link usage

fixes LF-1335
flag=none

Test plan
- Set up various content types with media object links
- Run the migration and check all of those were updated
  with proper attachments

Change-Id: Ia0c980400f9e824444480d6559fadec0d69a27e3
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/345629
Reviewed-by: Mysti Lilla <mysti@instructure.com>
QA-Review: Luis Oliveira <luis.oliveira@instructure.com>
Product-Review: Luis Oliveira <luis.oliveira@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
This commit is contained in:
Matheus 2024-04-18 16:58:46 -03:00 committed by Luis Oliveira
parent 635fb829b4
commit c43f578a61
2 changed files with 399 additions and 0 deletions

View File

@ -0,0 +1,197 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require "nokogiri"
module DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks
CONTENT_MAP = [
{ AssessmentQuestion => :question_data },
{ Assignment => :description },
{ Course => :syllabus_body },
{ DiscussionTopic => :message },
{ DiscussionEntry => :message },
{ Quizzes::Quiz => nil },
{ Quizzes::QuizQuestion => :question_data },
{ Submission => :body },
{ WikiPage => :body }
].freeze
ATTRIBUTES = %w[href data src].freeze
def self.update_active_records(model, field, where_clause, start_at, end_at)
model.where(id: start_at..end_at).where(*where_clause).find_each(strategy: :pluck_ids) do |active_record|
next unless (field && active_record[field]) || active_record.is_a?(Quizzes::Quiz)
if active_record.is_a?(AssessmentQuestion) || active_record.is_a?(Quizzes::QuizQuestion)
question_data = active_record.question_data
question_data["question_text"] = fix_html(active_record, question_data["question_text"])
if question_data && question_data["answers"]
question_data["answers"] = active_record["question_data"]["answers"].map do |a|
a.merge({ "text" => fix_html(active_record, a["text"]) })
end
end
active_record.update! question_data:
elsif active_record.is_a?(Quizzes::Quiz)
active_record.description = fix_html(active_record, active_record.description)
active_record.quiz_data = active_record.quiz_data.map do |question|
question = question.merge({ "question_text" => fix_html(active_record, question["question_text"]) })
question["answers"] = question["answers"].map do |a|
a.merge({ "text" => fix_html(active_record, a["text"]) })
end
question
end
active_record.save
else
active_record.update! field => fix_html(active_record, active_record[field])
end
end
end
def self.set_iframe_width_and_height(element, media_id)
preexisting_style = element["style"] || ""
return if preexisting_style.include?("height:") || preexisting_style.include?("width:")
mo = MediaObject.by_media_id(media_id)
mo = mo.first
return unless mo
mo_keys = mo.data[:extensions].keys
ext_data = mo.data[:extensions][mo_keys.first]
return unless ext_data
if !preexisting_style.include?("height:") && !preexisting_style.include?("width:")
element.set_attribute("style", "width:#{ext_data[:width]}px; height:#{ext_data[:height]}px; #{preexisting_style}")
end
end
def self.fix_html(active_record, html)
doc = Nokogiri::HTML5::DocumentFragment.parse(html, nil, { max_tree_depth: 10_000 })
# media comments
doc.css("a.instructure_inline_media_comment").each do |e|
next unless e.attributes["id"]&.value&.match?("media_comment_m-")
media_id = e.attributes["id"].value.gsub("media_comment_", "")
attachment = get_attachment(active_record, media_id)
new_src = "/media_attachments_iframe/#{attachment.id}"
new_src = add_verifier_to_link(new_src, attachment) if attachment.context_type == "User"
iframe = doc.document.create_element "iframe", { "src" => new_src }
set_iframe_width_and_height(iframe, media_id)
e.replace iframe
end
# media object iframes
doc.css("iframe").select do |e|
next unless e.get_attribute("src")&.match?('(.*\/)?media_objects_iframe\/([^\/\?]*)(.*)')
source_parts = e.get_attribute("src").match('(.*\/)?media_objects_iframe\/([^\/\?]*)(.*)')
media_id = source_parts[2]
attachment = get_attachment(active_record, media_id)
new_src = "#{source_parts[1]}media_attachments_iframe/#{attachment.id}#{source_parts[3]}"
new_src = add_verifier_to_link(new_src, attachment) if attachment.context_type == "User"
e.set_attribute("src", new_src)
set_iframe_width_and_height(e, media_id)
end
# misc...
# doc.css("a, video, iframe, object, embed").select do |e|
# ATTRIBUTES.each do |attr|
# next unless e.get_attribute(attr)&.match?('(.*\/)?media_objects\/([^\/\?]*)(.*)')
#
# source_parts = e.get_attribute(attr).match('(.*\/)?media_objects\/([^\/\?]*)(.*)')
# media_id = source_parts[2]
# attachment = get_attachment(active_record, media_id)
# new_src = "#{source_parts[1]}media_attachments/#{attachment.id}#{source_parts[3]}"
# new_src = add_verifier_to_link(new_src, attachment) if attachment.context_type == "User"
# iframe = doc.document.create_element "iframe", { "src" => new_src }
# set_iframe_width_and_height(iframe, media_id)
# e.replace iframe
# end
# end
doc.to_s
end
def self.add_verifier_to_link(link, attachment)
parts = link.split("?")
verified = parts[0] + "?verifier=#{attachment.uuid}"
verified += "&#{parts[1]}" if parts[1]
verified
end
def self.get_preferred_contexts(active_record)
return [active_record, active_record.assessment_question_bank] if active_record.is_a?(AssessmentQuestion)
return [active_record.user, active_record.discussion_topic] if active_record.is_a?(DiscussionEntry)
return [active_record.context] if active_record.is_a?(Assignment) || active_record.is_a?(DiscussionTopic) || active_record.is_a?(Quizzes::Quiz)
return [active_record] if active_record.is_a?(Course)
return [active_record.user] if active_record.is_a?(Submission)
return [active_record.quiz.context, active_record.quiz] if active_record.is_a?(Quizzes::QuizQuestion)
return [active_record.context] if active_record.is_a?(WikiPage)
[]
end
def self.create_attachment(active_record, media_id)
chosen_context = get_preferred_contexts(active_record).compact[0]
return unless chosen_context
Attachment.create!(context: chosen_context, media_entry_id: media_id, filename: media_id, content_type: "unknown/unknown")
end
def self.get_valid_candidate(candidates, active_record)
selected_candidates = candidates.where(context: get_preferred_contexts(active_record).compact)
return selected_candidates.first if selected_candidates.present?
nil
end
def self.get_attachment(active_record, media_id)
candidates = Attachment.where(media_entry_id: media_id)
return create_attachment(active_record, media_id) if candidates.empty?
(chosen_attachment = get_valid_candidate(candidates, active_record)) ? chosen_attachment : create_attachment(active_record, media_id)
end
def self.get_dataset(model, where_clause)
model.where(*where_clause)
end
def self.run
patterns = ["%media_objects_iframe%", "%media_comment_m-%", "%/media_objects/%"]
quiz_patterns = ["%media_objects_iframe%", "%media_comment_m-%", "%/media_objects/%", "%media_objects_iframe%", "%media_comment_m-%", "%/media_objects/%"]
CONTENT_MAP.each do |model_map|
model_map.each do |model, field|
field_search = ["#{field} LIKE ? OR #{field} LIKE ? OR #{field} LIKE ?"]
quiz_field_search = ["description LIKE ? OR description LIKE ? OR description LIKE ? OR quiz_data LIKE ? OR quiz_data LIKE ? OR quiz_data LIKE ?"]
where_clause = (model == Quizzes::Quiz) ? quiz_field_search.concat(quiz_patterns) : field_search.concat(patterns)
delay_if_production(
priority: Delayed::LOW_PRIORITY,
n_strand: ["DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks", Shard.current.database_server.id]
).get_dataset(model, where_clause).find_ids_in_ranges(batch_size: 100_000) do |start_at, end_at|
delay_if_production(
priority: Delayed::LOW_PRIORITY,
n_strand: ["DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks", Shard.current.database_server.id]
).update_active_records(model, field, where_clause, start_at, end_at)
end
end
end
end
end

View File

@ -0,0 +1,202 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require "spec_helper"
describe DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks do
let(:course) { course_model }
let(:assignment) { course.assignments.create!(submission_types: "online_text_entry", points_possible: 2) }
context "using media object dimensions in iframe" do
it "does not overwrite preexisting iframe dimensions" do
MediaObject.create! media_id: "m-3EtLMkFf9KBMneRZozuhGmYGTJSiqELW", data: { extensions: { mp4: { width: 640, height: 400 } } }
assignment.update! description: '<iframe width=0 height=0 src="/media_objects_iframe/m-3EtLMkFf9KBMneRZozuhGmYGTJSiqELW/?type=video&amp;embedded=true"></iframe>'
DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks.run
expect(assignment.reload.description).to eq "<iframe width=\"0\" height=\"0\" src=\"/media_attachments_iframe/#{Attachment.last.id}/?type=video&amp;embedded=true\" style=\"width:640px; height:400px; \"></iframe>"
end
it "does not lose preexisting style settings" do
MediaObject.create! media_id: "m-3EtLMkFf9KBMneRZozuhGmYGTJSiqELW", data: { extensions: { mp4: { width: 640, height: 400 } } }
assignment.update! description: '<iframe width=0 height=0 src="/media_objects_iframe/m-3EtLMkFf9KBMneRZozuhGmYGTJSiqELW/?type=video&amp;embedded=true" style="background:#ffffff"></iframe>'
DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks.run
expect(assignment.reload.description).to eq "<iframe width=\"0\" height=\"0\" src=\"/media_attachments_iframe/#{Attachment.last.id}/?type=video&amp;embedded=true\" style=\"width:640px; height:400px; background:#ffffff\"></iframe>"
end
it "does not overwrite preexisting dimension style settings" do
MediaObject.create! media_id: "m-3EtLMkFf9KBMneRZozuhGmYGTJSiqELW", data: { extensions: { mp4: { width: 640, height: 400 } } }
assignment.update! description: '<iframe width=0 height=0 src="/media_objects_iframe/m-3EtLMkFf9KBMneRZozuhGmYGTJSiqELW/?type=video&amp;embedded=true" style="width:0px; height:0px"></iframe>'
DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks.run
expect(assignment.reload.description).to eq "<iframe width=\"0\" height=\"0\" src=\"/media_attachments_iframe/#{Attachment.last.id}/?type=video&amp;embedded=true\" style=\"width:0px; height:0px\"></iframe>"
end
it "places the dimensions in their own attributes and styles properly" do
MediaObject.create! media_id: "m-3EtLMkFf9KBMneRZozuhGmYGTJSiqELW", data: { extensions: { mp4: { width: 640, height: 400 } } }
assignment.update! description: '<iframe src="/media_objects_iframe/m-3EtLMkFf9KBMneRZozuhGmYGTJSiqELW/?type=video&amp;embedded=true"></iframe>'
DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks.run
expect(assignment.reload.description).to eq "<iframe src=\"/media_attachments_iframe/#{Attachment.last.id}/?type=video&amp;embedded=true\" style=\"width:640px; height:400px; \"></iframe>"
end
end
context "having to create attachments" do
# it "replaces random links" do
# assignment.update! description: '<a href="/media_objects/media_id/info">clickme</a>'
# DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks.run
# expect(Assignment.last.description).to eq "<iframe src=\"/media_attachments/#{Attachment.last.id}/info\"></iframe>"
# expect(Attachment.last.context).to eq assignment.context
# end
it "replaces media object iframes" do
assignment.update! description: '<iframe src="/media_objects_iframe/m-3EtLMkFf9KBMneRZozuhGmYGTJSiqELW/?type=video&amp;embedded=true"></iframe>'
DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks.run
expect(Assignment.last.description).to eq "<iframe src=\"/media_attachments_iframe/#{Attachment.last.id}/?type=video&amp;embedded=true\"></iframe>"
expect(Attachment.last.context).to eq assignment.context
end
it "replaces media comments" do
assignment.update! description: '<a id="media_comment_m-4uoGqVdEqXhpqu2ZMytHSy9XMV73aQ7E" class="instructure_inline_media_comment video_comment" data-media_comment_type="video" data-alt=""></a>'
DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks.run
expect(Assignment.last.description).to eq "<iframe src=\"/media_attachments_iframe/#{Attachment.last.id}\"></iframe>"
expect(Attachment.last.context).to eq assignment.context
end
it "knows to ignore media attachments not context matched" do
non_matching_attachment = Attachment.create! context: Course.create!, media_entry_id: "m-4uoGqVdEqXhpqu2ZMytHSy9XMV73aQ7E", filename: "whatever", content_type: "unknown/unknown"
assignment.update! description: '<a id="media_comment_m-4uoGqVdEqXhpqu2ZMytHSy9XMV73aQ7E" class="instructure_inline_media_comment video_comment" data-media_comment_type="video" data-alt=""></a>'
DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks.run
expect(Assignment.last.description).not_to eq "<iframe src=\"/media_attachments_iframe/#{non_matching_attachment.id}\"></iframe>"
expect(Assignment.last.description).to eq "<iframe src=\"/media_attachments_iframe/#{Attachment.last.id}\"></iframe>"
expect(Attachment.last.context).to eq assignment.context
end
end
context "picking up preexisting attachments" do
# it "replaces random links" do
# matching_attachment = Attachment.create! context: course, media_entry_id: "media_id", filename: "whatever", content_type: "unknown/unknown"
# assignment.update! description: '<a href="/media_objects/media_id/info">clickme</a>'
# page = course.wiki_pages.create!(title: ".-.", body: '<a href="http://fullthing/media_objects/media_id/info">clickme</a>')
# DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks.run
# expect(assignment.reload.description).to eq "<iframe src=\"/media_attachments/#{matching_attachment.id}/info\"></iframe>"
# expect(page.reload.body).to eq "<iframe src=\"http://fullthing/media_attachments/#{matching_attachment.id}/info\"></iframe>"
# expect(Attachment.last.context).to eq assignment.context
# end
it "replaces media object iframes" do
matching_attachment = Attachment.create! context: course, media_entry_id: "m-3EtLMkFf9KBMneRZozuhGmYGTJSiqELW", filename: "whatever", content_type: "unknown/unknown"
assignment.update! description: '<iframe src="/media_objects_iframe/m-3EtLMkFf9KBMneRZozuhGmYGTJSiqELW/?type=video&amp;embedded=true"></iframe>'
DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks.run
expect(Assignment.last.description).to eq "<iframe src=\"/media_attachments_iframe/#{matching_attachment.id}/?type=video&amp;embedded=true\"></iframe>"
expect(matching_attachment.context).to eq assignment.context
end
it "replaces media comments" do
matching_attachment = Attachment.create! context: course, media_entry_id: "m-4uoGqVdEqXhpqu2ZMytHSy9XMV73aQ7E", filename: "whatever", content_type: "unknown/unknown"
assignment.update! description: '<a id="media_comment_m-4uoGqVdEqXhpqu2ZMytHSy9XMV73aQ7E" class="instructure_inline_media_comment video_comment" data-media_comment_type="video" data-alt=""></a>'
DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks.run
expect(Assignment.last.description).to eq "<iframe src=\"/media_attachments_iframe/#{matching_attachment.id}\"></iframe>"
expect(matching_attachment.context).to eq assignment.context
end
end
def expected_body(context)
att = context.attachments.last
return "<iframe src=\"/media_attachments_iframe/#{att.id}/?verifier=#{att.uuid}&amp;type=video&amp;embedded=true\"></iframe>" if att.context.is_a?(User)
"<iframe src=\"/media_attachments_iframe/#{att.id}/?type=video&amp;embedded=true\"></iframe>"
end
context "context matching to pre existing attachments" do
it "chooses the proper attachments and rejects unmatching context ones" do
media_id = "m-3EtLMkFf9KBMneRZozuhGmYGTJSiqELW"
record_body = "<iframe src=\"/media_objects_iframe/#{media_id}/?type=video&amp;embedded=true\"></iframe>"
another_course = course_model
another_course.update! syllabus_body: record_body
assignment = another_course.assignments.create!(description: record_body, submission_types: "online_text_entry", points_possible: 2)
assessment_question_bank = another_course.assessment_question_banks.create!
assessment_question = assessment_question_bank.assessment_questions.create! question_data: { "question_text" => record_body }
discussion_topic = another_course.discussion_topics.create! message: record_body
discussion_entry = discussion_topic.discussion_entries.create! message: record_body, user: User.create!
quiz = Quizzes::Quiz.create! context: another_course
quiz_question = quiz.quiz_questions.create! question_data: { "question_text" => record_body }
wiki_page = another_course.wiki_pages.create! title: "Whatevs", body: record_body
DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks.run
expect(another_course.reload.syllabus_body).to eq(expected_body(another_course))
expect(assignment.reload.description).to eq(expected_body(assignment.context))
expect(assessment_question.reload.question_data["question_text"]).to eq(expected_body(assessment_question))
expect(discussion_topic.reload.message).to eq(expected_body(another_course))
expect(discussion_entry.reload.message).to eq(expected_body(discussion_entry.user))
expect(quiz_question.reload.question_data["question_text"]).to eq(expected_body(another_course))
expect(wiki_page.reload.body).to eq(expected_body(wiki_page.context))
end
end
context "with multiple media uses in content quiz" do
it "replaces all the old media links throughout the data" do
first_matching_attachment = Attachment.create! context: course, media_entry_id: "m-media_1", filename: "whatever", content_type: "unknown/unknown"
second_matching_attachment = Attachment.create! context: course, media_entry_id: "m-media_2", filename: "whatever", content_type: "unknown/unknown"
third_matching_attachment = Attachment.create! context: course, media_entry_id: "m-media_3", filename: "whatever", content_type: "unknown/unknown"
quiz_description = "<iframe src=\"/media_objects_iframe/m-media_2/?type=video&amp;embedded=true\"></iframe>"
question_text_1 = "<a id=\"media_comment_m-media_3\" class=\"instructure_inline_media_comment video_comment\" data-media_comment_type=\"video\" data-alt=''></a>"
question_text_2 = "<a id=\"media_comment_m-media_2\" class=\"instructure_inline_media_comment video_comment\" data-media_comment_type=\"video\" data-alt=''></a>"
question_text_3 = "<a id=\"media_comment_m-media_1\" class=\"instructure_inline_media_comment video_comment\" data-media_comment_type=\"video\" data-alt=''></a>"
answer_text_1 = "<iframe src=\"/media_objects_iframe/m-media_1/?type=video&amp;embedded=true\"></iframe>"
answer_text_2 = "<iframe src=\"/media_objects_iframe/m-media_2/?type=video&amp;embedded=true\"></iframe>"
answer_text_3 = "<iframe src=\"/media_objects_iframe/m-media_3/?type=video&amp;embedded=true\"></iframe>"
answer_text_4 = "<iframe src=\"/media_objects_iframe/m-media_1/?type=video&amp;embedded=true\"></iframe>"
answer_text_5 = "<iframe src=\"/media_objects_iframe/m-media_2/?type=video&amp;embedded=true\"></iframe>"
answer_text_6 = "<iframe src=\"/media_objects_iframe/m-media_3/?type=video&amp;embedded=true\"></iframe>"
quiz_data = [
{
"question_text" => question_text_1,
"answers" => [
{ "id" => "7427", "text" => answer_text_1, "comments" => "", "comments_html" => "", "weight" => 100.0 },
{ "id" => "3893", "text" => answer_text_2, "comments" => "", "comments_html" => "", "weight" => 0.0 }
],
},
{
"question_text" => question_text_2,
"answers" => [
{ "id" => "7427", "text" => answer_text_3, "comments" => "", "comments_html" => "", "weight" => 100.0 },
{ "id" => "3893", "text" => answer_text_4, "comments" => "", "comments_html" => "", "weight" => 0.0 }
],
}
]
q = course.quizzes.create!(description: quiz_description, quiz_data:)
q.quiz_questions.create! question_data: {
"question_text" => question_text_3,
"answers" => [
{ "id" => "7427", "text" => answer_text_5, "comments" => "", "comments_html" => "", "weight" => 100.0 },
{ "id" => "3893", "text" => answer_text_6, "comments" => "", "comments_html" => "", "weight" => 0.0 }
],
}
DataFixup::ReplaceMediaObjectLinksForMediaAttachmentLinks.run
q.reload
expect(q.description).to eq("<iframe src=\"/media_attachments_iframe/#{second_matching_attachment.id}/?type=video&amp;embedded=true\"></iframe>")
expect(q.quiz_data[0]["question_text"]).to eq("<iframe src=\"/media_attachments_iframe/#{third_matching_attachment.id}\"></iframe>")
expect(q.quiz_data[0]["answers"][0]["text"]).to eq("<iframe src=\"/media_attachments_iframe/#{first_matching_attachment.id}/?type=video&amp;embedded=true\"></iframe>")
expect(q.quiz_data[0]["answers"][1]["text"]).to eq("<iframe src=\"/media_attachments_iframe/#{second_matching_attachment.id}/?type=video&amp;embedded=true\"></iframe>")
expect(q.quiz_data[1]["question_text"]).to eq("<iframe src=\"/media_attachments_iframe/#{second_matching_attachment.id}\"></iframe>")
expect(q.quiz_data[1]["answers"][0]["text"]).to eq("<iframe src=\"/media_attachments_iframe/#{third_matching_attachment.id}/?type=video&amp;embedded=true\"></iframe>")
expect(q.quiz_data[1]["answers"][1]["text"]).to eq("<iframe src=\"/media_attachments_iframe/#{first_matching_attachment.id}/?type=video&amp;embedded=true\"></iframe>")
expect(q.quiz_questions.first.question_data["question_text"]).to eq("<iframe src=\"/media_attachments_iframe/#{first_matching_attachment.id}\"></iframe>")
expect(q.quiz_questions.first.question_data["answers"][0]["text"]).to eq("<iframe src=\"/media_attachments_iframe/#{second_matching_attachment.id}/?type=video&amp;embedded=true\"></iframe>")
expect(q.quiz_questions.first.question_data["answers"][1]["text"]).to eq("<iframe src=\"/media_attachments_iframe/#{third_matching_attachment.id}/?type=video&amp;embedded=true\"></iframe>")
end
end
end