mirror of
https://github.com/discourse/discourse.git
synced 2026-08-07 13:19:19 +08:00
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`.
205 lines
7.1 KiB
Ruby
Vendored
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
|