mirror of
https://github.com/discourse/discourse.git
synced 2026-08-04 10:39:43 +08:00
## Summary Prevent the disclosure of inaccessible reply targets in post revision history. The serializer now correctly omits metadata for target posts, such as whispers or deleted posts, if the current user does not have permission to view them. ## Source - Patch Triage: https://patch.discourse.org/patch-triage/1359 Co-authored-by: discourse-patch-triage <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
396 lines
9.7 KiB
Ruby
Vendored
396 lines
9.7 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
class PostRevisionSerializer < ApplicationSerializer
|
|
attributes :created_at,
|
|
:post_id,
|
|
# which revision is hidden
|
|
:previous_hidden,
|
|
:current_hidden,
|
|
# dynamic & based on the current scope
|
|
:first_revision,
|
|
:previous_revision,
|
|
:current_revision,
|
|
:next_revision,
|
|
:last_revision,
|
|
# used for display
|
|
:current_version,
|
|
:version_count,
|
|
# from the user
|
|
:username,
|
|
:display_username,
|
|
:acting_user_name,
|
|
:avatar_template,
|
|
# all the changes
|
|
:edit_reason,
|
|
:body_changes,
|
|
:title_changes,
|
|
:user_changes,
|
|
:reply_to_post_number_changes,
|
|
:tags_changes,
|
|
:category_id_changes,
|
|
:can_edit,
|
|
:diff_error
|
|
|
|
# Creates a field called field_name_changes with previous and
|
|
# current members if a field has changed in this revision
|
|
def self.add_compared_field(field)
|
|
changes_name = :"#{field}_changes"
|
|
|
|
attributes changes_name
|
|
define_method(changes_name) do
|
|
return if suppress_hidden_diffs?
|
|
|
|
{ previous: previous[field], current: current[field] }
|
|
end
|
|
|
|
define_method("include_#{changes_name}?") do
|
|
!suppress_hidden_diffs? && previous[field] != current[field]
|
|
end
|
|
end
|
|
|
|
add_compared_field :wiki
|
|
add_compared_field :post_type
|
|
add_compared_field :locale
|
|
|
|
def previous_hidden
|
|
return previous["hidden"] if scope.can_view_hidden_post_revisions?
|
|
|
|
previous_side_hidden?
|
|
end
|
|
|
|
def current_hidden
|
|
!!(adjacent_current && adjacent_current["hidden"])
|
|
end
|
|
|
|
def first_revision
|
|
revisions.first["revision"]
|
|
end
|
|
|
|
def previous_revision
|
|
@previous_revision ||=
|
|
revisions
|
|
.select { |r| r["revision"] >= first_revision }
|
|
.select { |r| r["revision"] < current_revision }
|
|
.last
|
|
.try(:[], "revision")
|
|
end
|
|
|
|
def current_revision
|
|
object.number
|
|
end
|
|
|
|
def next_revision
|
|
@next_revision ||=
|
|
revisions
|
|
.select { |r| r["revision"] <= last_revision }
|
|
.select { |r| r["revision"] > current_revision }
|
|
.first
|
|
.try(:[], "revision")
|
|
end
|
|
|
|
def last_revision
|
|
@last_revision ||= revisions.select { |r| r["revision"] <= post.version }.last["revision"]
|
|
end
|
|
|
|
def current_version
|
|
@current_version ||= revisions.select { |r| r["revision"] <= current_revision }.count + 1
|
|
end
|
|
|
|
def version_count
|
|
revisions.count
|
|
end
|
|
|
|
def username
|
|
user.username_lower
|
|
end
|
|
|
|
def display_username
|
|
user.username
|
|
end
|
|
|
|
def acting_user_name
|
|
user.name
|
|
end
|
|
|
|
def include_acting_user_name?
|
|
SiteSetting.enable_names?
|
|
end
|
|
|
|
def avatar_template
|
|
user.avatar_template
|
|
end
|
|
|
|
def can_edit
|
|
scope.can_edit?(object.post)
|
|
end
|
|
|
|
def edit_reason
|
|
current["edit_reason"]
|
|
end
|
|
|
|
def include_edit_reason?
|
|
scope.can_view_hidden_post_revisions? || current["revision"] == previous["revision"] + 1
|
|
end
|
|
|
|
def body_changes
|
|
return if suppress_hidden_diffs?
|
|
|
|
cooked_diff = DiscourseDiff.new(previous["cooked"], current["cooked"])
|
|
raw_diff = DiscourseDiff.new(previous["raw"], current["raw"])
|
|
|
|
{
|
|
inline: cooked_diff.inline_html,
|
|
side_by_side: cooked_diff.side_by_side_html,
|
|
side_by_side_markdown: raw_diff.side_by_side_markdown,
|
|
}
|
|
rescue ONPDiff::DiffLimitExceeded
|
|
@diff_error = true
|
|
nil
|
|
end
|
|
|
|
def title_changes
|
|
return if suppress_hidden_diffs?
|
|
|
|
prev = "<div>#{previous["title"] && CGI.escapeHTML(previous["title"])}</div>"
|
|
cur = "<div>#{current["title"] && CGI.escapeHTML(current["title"])}</div>"
|
|
|
|
# always show the title for post_number == 1
|
|
return if object.post.post_number > 1 && prev == cur
|
|
|
|
diff = DiscourseDiff.new(prev, cur)
|
|
|
|
{ inline: diff.inline_html, side_by_side: diff.side_by_side_html }
|
|
rescue ONPDiff::DiffLimitExceeded
|
|
@diff_error = true
|
|
nil
|
|
end
|
|
|
|
def diff_error
|
|
@diff_error || false
|
|
end
|
|
|
|
def include_diff_error?
|
|
@diff_error
|
|
end
|
|
|
|
def include_title_changes?
|
|
object.post.post_number == 1 && !suppress_hidden_diffs?
|
|
end
|
|
|
|
def user_changes
|
|
return if suppress_hidden_diffs?
|
|
|
|
prev = previous["user_id"]
|
|
cur = current["user_id"]
|
|
|
|
# if stuff is messed up, default to system
|
|
previous = User.find_by(id: prev) || Discourse.system_user
|
|
current = User.find_by(id: cur) || Discourse.system_user
|
|
|
|
{
|
|
previous: {
|
|
username: previous.username_lower,
|
|
display_username: previous.username,
|
|
avatar_template: previous.avatar_template,
|
|
},
|
|
current: {
|
|
username: current.username_lower,
|
|
display_username: current.username,
|
|
avatar_template: current.avatar_template,
|
|
},
|
|
}
|
|
end
|
|
|
|
def include_user_changes?
|
|
!suppress_hidden_diffs? && previous["user_id"] != current["user_id"]
|
|
end
|
|
|
|
def reply_to_post_number_changes
|
|
return if suppress_hidden_diffs?
|
|
|
|
{
|
|
previous: reply_to_info(previous["reply_to_post_number"]),
|
|
current: reply_to_info(current["reply_to_post_number"]),
|
|
}
|
|
end
|
|
|
|
def include_reply_to_post_number_changes?
|
|
!suppress_hidden_diffs? && previous["reply_to_post_number"] != current["reply_to_post_number"]
|
|
end
|
|
|
|
def tags_changes
|
|
return if suppress_hidden_diffs?
|
|
|
|
pre = filter_tags previous["tags"]
|
|
cur = filter_tags current["tags"]
|
|
|
|
pre == cur ? nil : { previous: pre, current: cur }
|
|
end
|
|
|
|
def include_tags_changes?
|
|
!suppress_hidden_diffs? && previous["tags"] != current["tags"] && scope.can_see_tags?(topic)
|
|
end
|
|
|
|
def category_id_changes
|
|
return if suppress_hidden_diffs?
|
|
|
|
pre = filter_category_id previous["category_id"]
|
|
cur = filter_category_id current["category_id"]
|
|
|
|
pre == cur ? nil : { previous: pre, current: cur }
|
|
end
|
|
|
|
def include_category_id_changes?
|
|
!suppress_hidden_diffs? && previous["category_id"] != current["category_id"]
|
|
end
|
|
|
|
def locale_changes
|
|
return if suppress_hidden_diffs?
|
|
|
|
prev = previous["locale"].presence
|
|
cur = current["locale"].presence
|
|
{ previous: prev, current: cur }
|
|
end
|
|
|
|
protected
|
|
|
|
def post
|
|
@post ||= object.post
|
|
end
|
|
|
|
def topic
|
|
@topic ||= object.post.topic
|
|
end
|
|
|
|
def revisions
|
|
@revisions ||=
|
|
all_revisions.select { |r| scope.can_view_hidden_post_revisions? || !r["hidden"] }
|
|
end
|
|
|
|
def all_revisions
|
|
return @all_revisions if @all_revisions
|
|
|
|
post_revisions =
|
|
PostRevision.where(post_id: object.post_id).order(number: :desc).limit(99).to_a.reverse
|
|
|
|
latest_modifications = {
|
|
"raw" => [post.raw],
|
|
"cooked" => [post.cooked],
|
|
"edit_reason" => [post.edit_reason],
|
|
"wiki" => [post.wiki],
|
|
"post_type" => [post.post_type],
|
|
"user_id" => [post.user_id],
|
|
"locale" => [post.locale],
|
|
"reply_to_post_number" => [post.reply_to_post_number],
|
|
}
|
|
|
|
# Retrieve any `tracked_topic_fields`
|
|
PostRevisor.tracked_topic_fields.each_key do |field|
|
|
next unless topic.respond_to?(field)
|
|
topic
|
|
.public_send(field)
|
|
.then do |value|
|
|
next if value.try(:proxy_association)
|
|
latest_modifications[field.to_s] = [value]
|
|
end
|
|
end
|
|
|
|
latest_modifications["featured_link"] = [
|
|
topic.featured_link,
|
|
] if SiteSetting.topic_featured_link_enabled
|
|
|
|
latest_modifications["tags"] = [topic.tags.map(&:name).sort]
|
|
|
|
post_revisions << PostRevision.new(
|
|
number: post_revisions.last.number + 1,
|
|
hidden: post.hidden,
|
|
modifications: latest_modifications,
|
|
)
|
|
|
|
@all_revisions = []
|
|
|
|
# backtrack
|
|
post_revisions.each do |pr|
|
|
revision = ActiveSupport::HashWithIndifferentAccess.new
|
|
revision[:revision] = pr.number
|
|
revision[:hidden] = pr.hidden
|
|
|
|
pr.modifications.each { |field, (value, _)| revision[field] = value }
|
|
|
|
@all_revisions << revision
|
|
end
|
|
|
|
# waterfall
|
|
(@all_revisions.count - 1)
|
|
.downto(1)
|
|
.each do |r|
|
|
cur = @all_revisions[r]
|
|
prev = @all_revisions[r - 1]
|
|
|
|
cur.each_key { |field| prev[field] = prev.has_key?(field) ? prev[field] : cur[field] }
|
|
end
|
|
|
|
@all_revisions
|
|
end
|
|
|
|
def adjacent_previous
|
|
@adjacent_previous ||=
|
|
all_revisions.select { |revision| revision["revision"] < current_revision }.last
|
|
end
|
|
|
|
def adjacent_current
|
|
@adjacent_current ||=
|
|
all_revisions.select { |revision| revision["revision"] > current_revision }.first
|
|
end
|
|
|
|
def previous_side_hidden?
|
|
!!(previous["hidden"] || (adjacent_previous && adjacent_previous["hidden"]))
|
|
end
|
|
|
|
def suppress_hidden_diffs?
|
|
!scope.can_view_hidden_post_revisions? && (previous_side_hidden? || current_hidden)
|
|
end
|
|
|
|
def previous
|
|
@previous ||= revisions.select { |r| r["revision"] <= current_revision }.last
|
|
end
|
|
|
|
def current
|
|
@current ||= revisions.select { |r| r["revision"] > current_revision }.first
|
|
end
|
|
|
|
def user
|
|
# if stuff goes pear shape attribute to system
|
|
object.user || Discourse.system_user
|
|
end
|
|
|
|
def hidden_tags
|
|
@hidden_tags ||= DiscourseTagging.hidden_tag_names(scope)
|
|
end
|
|
|
|
def filter_tags(tags)
|
|
tags.is_a?(Array) && tags.any? ? tags - hidden_tags : tags
|
|
end
|
|
|
|
def filter_category_id(category_id)
|
|
return if category_id.blank?
|
|
Category.secured(scope).find_by(id: category_id)&.id
|
|
end
|
|
|
|
def reply_to_info(post_number)
|
|
return nil if post_number.blank?
|
|
|
|
target = Post.with_deleted.where(topic_id: topic.id, post_number: post_number).first
|
|
return nil if target.blank? || !scope.can_see?(target)
|
|
|
|
info = { post_number: target.post_number }
|
|
|
|
if target.user
|
|
info[:username] = target.user.username_lower
|
|
info[:display_username] = target.user.username
|
|
info[:avatar_template] = target.user.avatar_template
|
|
end
|
|
|
|
info
|
|
end
|
|
end
|