0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-07 13:19:19 +08:00
discourse/plugins/discourse-templates/lib/tasks/discourse-templates.rake
Régis Hanol 3e012af58e
FIX: Scope duplicate topic title check to what the user can see (#41871)
Previously, the duplicate topic title check compared new titles against
every topic on the site regardless of visibility: users were blocked by
titles in categories they couldn't see or unlisted topics they couldn't
find, the bare "Title has already been used" error gave no way to locate
the conflict (and doubled as an existence oracle for hidden titles), and
the behavior was controlled by two entangled boolean settings.

This change scopes the check to the destination category plus whatever
the acting user can actually see, links the conflicting topic in the
error — safe by construction, since being blocked now implies being able
to see it:

> This title has already been used by [another topic]().

It also consolidates the two booleans into a single
`duplicate_topic_titles` enum (`disallowed` /
`allowed_across_categories` / `allowed`), with existing values migrated
and the old names kept as hidden deprecated aliases that admin search
still resolves.

Reported in
https://meta.discourse.org/t/title-has-already-been-used-in-a-secure-category/123047

Note for self-hosters: env-provided settings can't be migrated —
`DISCOURSE_ALLOW_DUPLICATE_TOPIC_TITLES=true` configs need to switch to
`DISCOURSE_DUPLICATE_TOPIC_TITLES=allowed`.
2026-07-24 15:35:35 +02:00

205 lines
7.1 KiB
Ruby
Vendored

# frozen_string_literal: true
desc "Migrate data from discourse-canned-replies to discourse-templates"
task "discourse-templates:migrate-from-canned-replies" => :environment do
ENV["RAILS_DB"] ? migrate_data : migrate_data_all_sites
end
desc "Purge old data from canned replies"
task "discourse-templates:purge-old-canned-replies-data" => :environment do
ENV["RAILS_DB"] ? purge_old_data : purge_old_data_all_sites
end
def create_category
old_settings_canned_replies_groups =
SiteSetting.find_by(name: "canned_replies_groups")&.value || ""
old_settings_canned_replies_everyone_enabled =
SiteSetting.find_by(name: "canned_replies_everyone_enabled")&.value&.starts_with?("t") || false
old_settings_canned_replies_everyone_can_edit =
SiteSetting.find_by(name: "canned_replies_everyone_can_edit")&.value&.starts_with?("t") || false
category = nil
I18n.with_locale(SiteSetting.default_locale) do
category =
Category.new(
name: I18n.t("default_category_template.name")[0...50], # category names are limited to 50 chars in discourse
description: I18n.t("default_category_template.description"),
user: Discourse.system_user,
all_topics_wiki: true,
)
end
permissions = { staff: :full } # staff is always allowed to use and edit canned replies
# get the existing groups and compares with the groups allowed to used canned replies
# using the same algorithm that the plugin used to ensure that the same groups are authorized
# if they are available
groups = Group.all
granted_group_list = old_settings_canned_replies_groups.split("|").map(&:downcase)
groups
.select { |group| granted_group_list.include?(group.name.downcase) }
.each { |group| permissions.merge!({ group.name => :full }) }
# insert privileges (or not) for everyone to use and edit canned replies
# based on the two settings available
if old_settings_canned_replies_everyone_enabled
permissions.merge!(
{ everyone: old_settings_canned_replies_everyone_can_edit ? :full : :readonly },
)
end
category.set_permissions(permissions)
raise <<~ERROR unless category.save!
****************************
ERROR while creating the existing_category to store the templates: #{category.errors.full_messages}
If you can't fix the reason of the error, you can create a existing_category manually
to store the templates and define it in Settings.discourse_templates_categories.
Then proceed with this migration.
ERROR
puts "Created category #{category.name}(id: #{category.id}) to store the templates"
category
end
def create_topic_from_v1_reply(reply, category)
topic = Topic.new(title: reply[:title], user: Discourse.system_user, category_id: category.id)
topic.custom_fields = { DiscourseTemplates::PLUGIN_NAME => reply[:id] }
topic.skip_callbacks = true
unless topic.save!(validate: false)
raise "ERROR importing #{reply[:id]}: #{reply[:title]} - #{errors.full_messages}"
end
post = topic.posts.build(raw: reply[:content], user: Discourse.system_user, wiki: true)
unless post.save!(validate: false)
raise "ERROR importing #{reply[:id]}: #{reply[:title]} - #{errors.full_messages}"
end
usage_count =
DiscourseTemplates::UsageCount.new(topic_id: topic.id, usage_count: reply[:usages] || 0)
usage_count.save
topic
end
def migrate_data_all_sites
RailsMultisite::ConnectionManagement.each_connection do |db|
puts "Starting DB: #{db} >>>>>>>>>>>>>>>>>>>>>>>>>"
migrate_data
puts "Ended DB: #{db} >>>>>>>>>>>>>>>>>>>>>>>>>"
end
end
def migrate_data
puts "Migrating data from discourse-canned-replies to discourse-templates"
begin
ActiveRecord::Base.transaction do
category =
if SiteSetting.discourse_templates_categories.blank?
new_category = create_category
SiteSetting.discourse_templates_categories = new_category.id.to_s
new_category
else
existing_category =
Category.find_by(id: SiteSetting.discourse_templates_categories&.split("|")&.first.to_i)
if existing_category.blank?
raise "Category specified not found. Check Settings.discourse_templates_categories"
end
puts "",
"****************************",
"Using existing category #{existing_category.name}(id: #{existing_category.id}) defined in Settings.discourse_templates_categories",
"Please note that access to templates will follow this existing category security settings",
"****************************",
""
existing_category
end
canned_replies_plugin_name = "discourse-canned-replies"
canned_replies_store_name = "replies"
replies_v1 = PluginStore.get(canned_replies_plugin_name, canned_replies_store_name)
count = replies_v1&.size || 0
puts "no canned replies from v1 were located to be migrated to v2" if count == 0
# duplicate topic titles must be temporarily enabled to ensure that all
# canned replies can be imported since there is no guarantee that a previous
# topic does not exist with the same title
duplicate_topic_titles = SiteSetting.duplicate_topic_titles
SiteSetting.duplicate_topic_titles = "allowed"
(replies_v1 || {}).each_with_index do |(_, reply), index|
position = index + 1
# search if a previous topic was already imported from this canned reply
existing_topic =
TopicCustomField.find_by(name: DiscourseTemplates::PLUGIN_NAME, value: reply[:id])
if existing_topic.blank?
topic = create_topic_from_v1_reply(reply, category)
puts "[#{position}/#{count}] canned reply #{reply[:id]}: #{reply[:title]} imported to topic #{topic.id}"
else
puts "[#{position}/#{count}] skipping #{reply[:title]}. Topic previously imported found!"
end
end
# restores the setting to the previous value after importing the topics
SiteSetting.duplicate_topic_titles = duplicate_topic_titles
end
puts "", "Canned replies migration to templates finished!"
rescue StandardError => e
puts e
puts "Transaction aborted! All changes were rolled back!"
end
end
def purge_old_data_all_sites
RailsMultisite::ConnectionManagement.each_connection do |db|
puts "Starting DB: #{db} >>>>>>>>>>>>>>>>>>>>>>>>>"
purge_old_data
puts "Ended DB: #{db} >>>>>>>>>>>>>>>>>>>>>>>>>"
end
end
def purge_old_data
puts "Removing canned replies data"
ActiveRecord::Base.transaction do
DB.exec <<~SQL
DELETE FROM site_settings#{" "}
WHERE name IN (
'canned_replies_groups',#{" "}
'canned_replies_everyone_enabled',#{" "}
'canned_replies_everyone_can_edit'
)
SQL
canned_replies_plugin_name = "discourse-canned-replies"
canned_replies_store_name = "replies"
old_replies =
PluginStoreRow.find_by(
plugin_name: canned_replies_plugin_name,
key: canned_replies_store_name,
)
old_replies.presence&.destroy!
puts "Finished!"
rescue StandardError => e
puts e
puts "Transaction aborted! All changes were rolled back!"
end
end