discourse/app/controllers/admin/watched_words_controller.rb
Isaac Janzen dd982bf67c
DEV: Deferred upload accesses tempfile after request completes (#38337)
## Summary

The `upload` action in `Admin::WatchedWordsController` reads the
uploaded file's tempfile inside a `Scheduler::Defer.later` block, but
Rails/Rack automatically deletes tempfiles after the request completes,
causing `Errno::ENOENT` when the deferred block executes asynchronously
in production.
2026-03-10 07:53:29 -07:00

160 lines
4.8 KiB
Ruby
Vendored

# frozen_string_literal: true
require "csv"
class Admin::WatchedWordsController < Admin::StaffController
skip_before_action :check_xhr, only: [:download]
def index
watched_words = WatchedWord.order(:action, :word)
watched_words =
watched_words.where.not(action: WatchedWord.actions[:tag]) if !SiteSetting.tagging_enabled
render_json_dump WatchedWordListSerializer.new(watched_words, scope: guardian, root: false)
end
def create
opts = watched_words_params
action = WatchedWord.actions[opts[:action_key].to_sym]
words = opts.delete(:words)
if action == WatchedWord.actions[:tag] && watched_words_params[:replacement_tags].present?
opts = opts.merge(replacement: resolve_replacement_tags)
end
watched_word_group = WatchedWordGroup.new(action: action)
watched_word_group.create_or_update_members(words, opts)
if watched_word_group.valid?
StaffActionLogger.new(current_user).log_watched_words_creation(watched_word_group)
render_json_dump WatchedWordListSerializer.new(
watched_word_group.watched_words,
scope: guardian,
root: false,
)
else
render_json_error(watched_word_group)
end
end
def destroy
watched_word = WatchedWord.find_by(id: params[:id])
raise Discourse::InvalidParameters.new(:id) unless watched_word
watched_word_group = watched_word.watched_word_group
if watched_word_group&.watched_words&.count == 1
watched_word_group.destroy!
StaffActionLogger.new(current_user).log_watched_words_deletion(watched_word_group)
else
watched_word.destroy!
StaffActionLogger.new(current_user).log_watched_words_deletion(watched_word)
end
render json: success_json
end
def upload
file = params[:file] || params[:files].first
action_key = params[:action_key].to_sym
has_replacement = WatchedWord.has_replacement?(action_key)
content = Encodings.to_utf8(File.read(file.tempfile, mode: "rb"))
Scheduler::Defer.later("Upload watched words") do
begin
words_updated = 0
CSV.parse(content) do |row|
if row[0].present? && (!has_replacement || row[1].present?)
watched_word =
WatchedWord.create_or_update_word(
word: row[0],
replacement: has_replacement ? row[1] : nil,
action_key: action_key,
case_sensitive: "true" == row[2]&.strip&.downcase,
)
if watched_word.valid?
words_updated += 1
StaffActionLogger.new(current_user).log_watched_words_creation(watched_word)
end
end
end
data = { result: "ok", words_updated: words_updated }
rescue => e
data = failed_json.merge(errors: [e.message], words_updated: words_updated)
end
MessageBus.publish("/watched_words/upload", data.as_json, client_ids: [params[:client_id]])
end
render json: success_json
end
def download
params.require(:id)
name = watched_words_params[:id].to_sym
action = WatchedWord.actions[name]
raise Discourse::NotFound if !action
content = WatchedWord.where(action: action)
if WatchedWord.has_replacement?(name)
content = content.pluck(:word, :replacement).map(&:to_csv).join
else
content = content.pluck(:word).join("\n")
end
headers["Content-Length"] = content.bytesize.to_s
send_data content,
filename: "#{Discourse.current_hostname}-watched-words-#{name}.csv",
content_type: "text/csv"
end
def clear_all
params.require(:id)
name = watched_words_params[:id].to_sym
action = WatchedWord.actions[name]
raise Discourse::NotFound if !action
WatchedWord
.where(action: action)
.find_each do |watched_word|
watched_word.destroy!
StaffActionLogger.new(current_user).log_watched_words_deletion(watched_word)
end
WordWatcher.clear_cache!
render json: success_json
end
private
def watched_words_params
@watched_words_params ||=
params.permit(
:id,
:replacement,
:action_key,
:case_sensitive,
:html,
words: [],
replacement_tags: %i[id name],
)
end
def resolve_replacement_tags
tags_param = watched_words_params[:replacement_tags]
tags_param = tags_param.values if tags_param.is_a?(ActionController::Parameters)
return if tags_param.blank?
tag_ids = tags_param.filter_map { |t| t[:id]&.to_i }
new_tag_names = tags_param.select { |t| t[:id].blank? }.filter_map { |t| t[:name] }
tag_names = Tag.where(id: tag_ids).pluck(:name)
if new_tag_names.present?
new_tags = DiscourseTagging.find_or_create_tags!(new_tag_names, guardian)
tag_names.concat(new_tags.map(&:name))
end
tag_names.join(",")
end
end