0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 12:09:27 +08:00
discourse/app/models/form_template.rb
Régis Hanol aec18e9f70
FIX: Consistently exclude synonyms and hidden tags from tag lists (#42364)
Every endpoint that lists tags for browsing hand-rolled its own filter
chain, and they had drifted apart.

The tags page filtered synonyms out when tags were listed flat but not
when they were listed by group, so a synonym of a tag belonging to no
tag group was shown next to its target — a duplicate entry whose page
only redirects back to the tag it duplicates. Synonyms of grouped tags
were hidden by accident rather than by design: creating a synonym copies
the target's tag group memberships onto it, which happens to satisfy the
"belongs to no tag group" condition the ungrouped list is built from.
Non-admins were shielded by a second accident, since a synonym normally
ends up with a zero topic count — so any synonym that kept its topics,
whether from an import, a plugin, or assigning `target_tag_id` directly,
was listed to everyone.

Tag visibility has two independent mechanisms: tag group permissions,
and the categories a tag is attached to. Only the second one can apply
to a tag that belongs to no tag group, and neither the grouped tags page
nor the tag group search endpoint applied it, so the names of tags
restricted to a category the viewer cannot read were listed to them.
`TagGroup.visible` gates which groups are returned, never the tags
inside them.

The navigation menu tag picker filtered out neither synonyms nor tags
only used in personal messages, and gated on staff where the tags page
gates on admin, so moderators were served the entire tag table. A
synonym picked there was saved as a sidebar link and rendered from then
on. Top tags and tag search had the same gap.

Paths that resolve a tag by name keep matching synonyms on purpose: the
composer offers them so that typing a retired name finds the tag that
replaced it, and hashtags in posts cooked before a rename have to keep
working. Serializers that echo configuration back are left alone,
because filtering a value the client posts straight back would silently
delete it.
2026-08-05 20:29:35 +02:00

82 lines
2.3 KiB
Ruby
Vendored

# frozen_string_literal: true
class FormTemplate < ActiveRecord::Base
validates :name,
presence: true,
uniqueness: true,
length: {
maximum: -> { SiteSetting.max_form_template_title_length },
}
validates :template,
presence: true,
length: {
maximum: -> { SiteSetting.max_form_template_content_length },
}
validates_with FormTemplateYamlValidator, if: ->(ft) { ft.template }
has_many :category_form_templates, dependent: :destroy
has_many :categories, through: :category_form_templates
class NotAllowed < StandardError
end
def process!(guardian)
parsed_template = YAML.safe_load(template)
tag_group_names = parsed_template.map { |f| f["tag_group"] }.compact.map(&:downcase).uniq
tag_groups =
TagGroup
.includes(:base_tags)
.visible(guardian)
.where("lower(name) IN (?)", tag_group_names)
.index_by { |tg| tg.name }
parsed_template.map! do |form_field|
next form_field unless form_field["tag_group"]
tag_group_name = form_field["tag_group"]
tags = tag_groups[tag_group_name].base_tags
ordered_field = {}
tags =
tags.sort_by do |t|
# Transform the name to the displayed value before ordering
display = t.description.presence || t.name.tr("-", " ").upcase
display
end
form_field.each do |key, value|
ordered_field[key] = value
ordered_field["choices"] = tags.map { |t| { "id" => t.id, "name" => t.name } } if key ==
"id"
end
ordered_field["attributes"] ||= {}
ordered_field["attributes"]["tag_group"] = tag_group_name
translated_tags = tags.select { |t| t.description }.to_h { |t| [t.name, t.description] }
ordered_field["attributes"]["tag_choices"] = translated_tags
ordered_field
end
self.template = YAML.dump(parsed_template)
self
end
end
# == Schema Information
#
# Table name: form_templates
#
# id :bigint not null, primary key
# name :string not null
# template :text not null
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_form_templates_on_name (name) UNIQUE
#