From d979f3573d37c83b442a856d33a1a7d2dcb041f7 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Thu, 12 Feb 2026 23:20:54 +0100 Subject: [PATCH] FIX: Multiple bulk importer fixes (#37740) --- script/bulk_import/base.rb | 14 +++++++++++--- script/bulk_import/generic_bulk.rb | 13 ++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/script/bulk_import/base.rb b/script/bulk_import/base.rb index c6567777f12..0a3d1396b8a 100644 --- a/script/bulk_import/base.rb +++ b/script/bulk_import/base.rb @@ -1329,6 +1329,8 @@ class BulkImport::Base end @users[user[:imported_id].to_i] = user[:id] = @last_user_id += 1 + @emails[user[:email]] = user[:id] if user[:email].present? + @external_ids[user[:external_id]] = user[:id] if user[:external_id].present? imported_username = user[:original_username].presence || user[:username].dup @@ -2246,8 +2248,7 @@ class BulkImport::Base name.gsub!(/[^A-Za-z0-9]+$/, "") name.gsub!(/([-_.]{2,})/) { $1.first } name.strip! - name.truncate(60) - name + name.truncate(60, omission: "") end def random_username @@ -2362,7 +2363,14 @@ class BulkImport::Base def normalize_text(text) return nil if text.blank? - @html_entities.decode(normalize_charset(text.presence || "").scrub) + text = normalize_charset(text.presence || "").scrub + # Escape HTML-encoded UTF-16 surrogates (e.g. �) so they pass through + # as literal text instead of crashing the HTML entity decoder. + text.gsub!(/&#(x?)(\h+);/) do + cp = $1.empty? ? $2.to_i : $2.to_i(16) + (0xD800..0xDFFF).cover?(cp) ? "&##{$1}#{$2};" : $& + end + @html_entities.decode(text) end def normalize_charset(text) diff --git a/script/bulk_import/generic_bulk.rb b/script/bulk_import/generic_bulk.rb index e41768af0e0..4379c618e24 100644 --- a/script/bulk_import/generic_bulk.rb +++ b/script/bulk_import/generic_bulk.rb @@ -602,7 +602,7 @@ class BulkImport::Generic < BulkImport::Base create_user_emails(users) do |row| user_id = user_id_from_imported_id(row["id"]) - next if user_id && existing_user_ids.include?(user_id) + next unless user_id && existing_user_ids.add?(user_id) if row["anonymized"] == 1 username = username_from_id(user_id) @@ -628,7 +628,7 @@ class BulkImport::Generic < BulkImport::Base create_user_profiles(users) do |row| user_id = user_id_from_imported_id(row["id"]) - next if user_id && existing_user_ids.include?(user_id) + next unless user_id && existing_user_ids.add?(user_id) if row["anonymized"] == 1 row["bio"] = nil @@ -659,7 +659,7 @@ class BulkImport::Generic < BulkImport::Base create_user_options(users) do |row| user_id = user_id_from_imported_id(row["id"]) - next if user_id && existing_user_ids.include?(user_id) + next unless user_id && existing_user_ids.add?(user_id) { user_id: user_id, @@ -755,7 +755,7 @@ class BulkImport::Generic < BulkImport::Base create_single_sign_on_records(users) do |row| user_id = user_id_from_imported_id(row["id"]) - next if user_id && existing_user_ids.include?(user_id) + next unless user_id && existing_user_ids.add?(user_id) sso_record = JSON.parse(row["sso_record"], symbolize_names: true) sso_record[:user_id] = user_id @@ -949,12 +949,15 @@ class BulkImport::Generic < BulkImport::Base SQL poll_details.each do |poll| - if (placeholder = poll_mapping[poll["id"]]) + if (placeholder = poll_mapping.delete(poll["id"])) raw.gsub!(placeholder, poll_bbcode(poll)) end end poll_details.close + + # Remove placeholders for polls without options + poll_mapping.each_value { |placeholder| raw.gsub!(placeholder, "") } end if (mentions = placeholders&.fetch("mentions", nil))