mirror of
https://github.com/discourse/discourse.git
synced 2024-12-01 05:54:15 +08:00
366ff0e76b
# Context https://meta.discourse.org/t/missing-translate-in-review-page/262604 ![image](https://user-images.githubusercontent.com/50783505/234089049-72332040-e7d5-4081-824a-b0b36e37187a.png) An additional button was added as a result ofdd495a0e19
which was intended to grant access to deleting reviewable from the API. We were being too flexible by only checking if the user was an admin012aaf0ba3/lib/guardian.rb (L237)
where it should instead by scoped to check if the request was an API call. # Fix https://github.com/discourse/discourse/pull/21226/files#diff-0a2548be4b18bd4ef2dffb3ef8e44984d2fef7f037b53e98f67abea52ef75aa2R237 # Additions Added a new guard of `is_api?` https://github.com/discourse/discourse/pull/21226/files#diff-0a2548be4b18bd4ef2dffb3ef8e44984d2fef7f037b53e98f67abea52ef75aa2R657-R660 In `app/models/reviewable.rb` we check if the user has the permissions to the destroy action via the `Guardian`. To do this we were instantiating a new `Guardian` class which then caused us to lose the context of the request. The request is a necessary component in the guard of `is_api?` so we needed to pass the already defined Guardian from the `app/controllers/reviewables_controller.rb` to the `#perform` method to ensure the request is present.
314 lines
9.3 KiB
Ruby
314 lines
9.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class ReviewablesController < ApplicationController
|
|
requires_login
|
|
|
|
PER_PAGE = 10
|
|
|
|
before_action :version_required, only: %i[update perform]
|
|
before_action :ensure_can_see, except: [:destroy]
|
|
|
|
def index
|
|
offset = params[:offset].to_i
|
|
|
|
if params[:type].present?
|
|
raise Discourse::InvalidParameters.new(:type) unless Reviewable.valid_type?(params[:type])
|
|
end
|
|
|
|
status = (params[:status] || "pending").to_sym
|
|
raise Discourse::InvalidParameters.new(:status) unless allowed_statuses.include?(status)
|
|
|
|
topic_id = params[:topic_id] ? params[:topic_id].to_i : nil
|
|
category_id = params[:category_id] ? params[:category_id].to_i : nil
|
|
|
|
custom_keys = Reviewable.custom_filters.map(&:first)
|
|
additional_filters =
|
|
JSON.parse(params.fetch(:additional_filters, {}), symbolize_names: true).slice(*custom_keys)
|
|
filters = {
|
|
ids: params[:ids],
|
|
status: status,
|
|
category_id: category_id,
|
|
topic_id: topic_id,
|
|
additional_filters: additional_filters.reject { |_, v| v.blank? },
|
|
}
|
|
|
|
%i[priority username reviewed_by from_date to_date type sort_order].each do |filter_key|
|
|
filters[filter_key] = params[filter_key]
|
|
end
|
|
|
|
total_rows = Reviewable.list_for(current_user, **filters).count
|
|
reviewables =
|
|
Reviewable.list_for(current_user, **filters.merge(limit: PER_PAGE, offset: offset)).to_a
|
|
|
|
claimed_topics = ReviewableClaimedTopic.claimed_hash(reviewables.map { |r| r.topic_id }.uniq)
|
|
|
|
# This is a bit awkward, but ActiveModel serializers doesn't seem to serialize STI. Note `hash`
|
|
# is mutated by the serializer and contains the side loaded records which must be merged in the end.
|
|
hash = {}
|
|
json = {
|
|
reviewables:
|
|
reviewables.map! do |r|
|
|
result =
|
|
r
|
|
.serializer
|
|
.new(r, root: nil, hash: hash, scope: guardian, claimed_topics: claimed_topics)
|
|
.as_json
|
|
hash[:bundled_actions].uniq!
|
|
(hash["actions"] || []).uniq!
|
|
result
|
|
end,
|
|
meta:
|
|
filters.merge(
|
|
total_rows_reviewables: total_rows,
|
|
types: meta_types,
|
|
reviewable_types: Reviewable.types,
|
|
reviewable_count: current_user.reviewable_count,
|
|
unseen_reviewable_count: Reviewable.unseen_reviewable_count(current_user),
|
|
),
|
|
}
|
|
if (offset + PER_PAGE) < total_rows
|
|
json[:meta][:load_more_reviewables] = review_path(filters.merge(offset: offset + PER_PAGE))
|
|
end
|
|
json.merge!(hash)
|
|
|
|
render_json_dump(json, rest_serializer: true)
|
|
end
|
|
|
|
def user_menu_list
|
|
json = {
|
|
reviewables:
|
|
Reviewable.basic_serializers_for_list(
|
|
Reviewable.user_menu_list_for(current_user),
|
|
current_user,
|
|
).as_json,
|
|
reviewable_count: current_user.reviewable_count,
|
|
}
|
|
render_json_dump(json, rest_serializer: true)
|
|
end
|
|
|
|
def count
|
|
render_json_dump(count: Reviewable.pending_count(current_user))
|
|
end
|
|
|
|
def topics
|
|
topic_ids = Set.new
|
|
|
|
stats = {}
|
|
unique_users = {}
|
|
|
|
# topics isn't indexed on `reviewable_score` and doesn't know what the current user can see,
|
|
# so let's query from the inside out.
|
|
pending = Reviewable.viewable_by(current_user).pending
|
|
pending = pending.where("score >= ?", Reviewable.min_score_for_priority)
|
|
|
|
pending.each do |r|
|
|
topic_ids << r.topic_id
|
|
|
|
meta = stats[r.topic_id] ||= { count: 0, unique_users: 0 }
|
|
users = unique_users[r.topic_id] ||= Set.new
|
|
|
|
r.reviewable_scores.each do |rs|
|
|
users << rs.user_id
|
|
meta[:count] += 1
|
|
end
|
|
meta[:unique_users] = users.size
|
|
end
|
|
|
|
topics = Topic.where(id: topic_ids).order("reviewable_score DESC")
|
|
render_serialized(
|
|
topics,
|
|
ReviewableTopicSerializer,
|
|
root: "reviewable_topics",
|
|
stats: stats,
|
|
claimed_topics: ReviewableClaimedTopic.claimed_hash(topic_ids),
|
|
rest_serializer: true,
|
|
meta: {
|
|
types: meta_types,
|
|
},
|
|
)
|
|
end
|
|
|
|
def explain
|
|
reviewable = find_reviewable
|
|
|
|
render_serialized(
|
|
{ reviewable: reviewable, scores: reviewable.explain_score },
|
|
ReviewableExplanationSerializer,
|
|
rest_serializer: true,
|
|
root: "reviewable_explanation",
|
|
)
|
|
end
|
|
|
|
def show
|
|
reviewable = find_reviewable
|
|
|
|
render_serialized(
|
|
reviewable,
|
|
reviewable.serializer,
|
|
rest_serializer: true,
|
|
claimed_topics: ReviewableClaimedTopic.claimed_hash([reviewable.topic_id]),
|
|
root: "reviewable",
|
|
meta: {
|
|
types: meta_types,
|
|
},
|
|
)
|
|
end
|
|
|
|
def destroy
|
|
user =
|
|
if is_api?
|
|
if @guardian.is_admin?
|
|
fetch_user_from_params
|
|
else
|
|
raise Discourse::InvalidAccess
|
|
end
|
|
else
|
|
current_user
|
|
end
|
|
|
|
reviewable = Reviewable.find_by(id: params[:reviewable_id], created_by: user)
|
|
raise Discourse::NotFound.new if reviewable.blank?
|
|
|
|
reviewable.perform(current_user, :delete, { guardian: @guardian })
|
|
|
|
render json: success_json
|
|
end
|
|
|
|
def update
|
|
reviewable = find_reviewable
|
|
if error = claim_error?(reviewable)
|
|
return render_json_error(error)
|
|
end
|
|
|
|
editable = reviewable.editable_for(guardian)
|
|
raise Discourse::InvalidAccess.new unless editable.present?
|
|
|
|
# Validate parameters are all editable
|
|
edit_params = params[:reviewable] || {}
|
|
edit_params.each do |name, value|
|
|
if value.is_a?(ActionController::Parameters)
|
|
value.each do |pay_name, pay_value|
|
|
raise Discourse::InvalidAccess.new unless editable.has?("#{name}.#{pay_name}")
|
|
end
|
|
else
|
|
raise Discourse::InvalidAccess.new unless editable.has?(name)
|
|
end
|
|
end
|
|
|
|
begin
|
|
if reviewable.update_fields(edit_params, current_user, version: params[:version].to_i)
|
|
result = edit_params.merge(version: reviewable.version)
|
|
render json: result
|
|
else
|
|
render_json_error(reviewable.errors)
|
|
end
|
|
rescue Reviewable::UpdateConflict
|
|
render_json_error(I18n.t("reviewables.conflict"), status: 409)
|
|
end
|
|
end
|
|
|
|
def perform
|
|
args = { version: params[:version].to_i }
|
|
|
|
result = nil
|
|
begin
|
|
reviewable = find_reviewable
|
|
|
|
if error = claim_error?(reviewable)
|
|
return render_json_error(error)
|
|
end
|
|
|
|
if reviewable.type == "ReviewableUser"
|
|
args.merge!(
|
|
reject_reason: params[:reject_reason],
|
|
send_email: params[:send_email] != "false",
|
|
)
|
|
end
|
|
|
|
plugin_params =
|
|
DiscoursePluginRegistry.reviewable_params.select do |reviewable_param|
|
|
reviewable.type == reviewable_param[:type].to_s.classify
|
|
end
|
|
args.merge!(params.slice(*plugin_params.map { |pp| pp[:param] }).permit!)
|
|
|
|
result = reviewable.perform(current_user, params[:action_id].to_sym, args)
|
|
rescue Reviewable::InvalidAction => e
|
|
if reviewable.type == "ReviewableUser" && !reviewable.pending? && reviewable.target.blank?
|
|
raise Discourse::NotFound.new(
|
|
e.message,
|
|
custom_message: "reviewables.already_handled_and_user_not_exist",
|
|
)
|
|
else
|
|
# Consider InvalidAction an InvalidAccess
|
|
raise Discourse::InvalidAccess.new(e.message)
|
|
end
|
|
rescue Reviewable::UpdateConflict
|
|
return render_json_error(I18n.t("reviewables.conflict"), status: 409)
|
|
end
|
|
|
|
if result.success?
|
|
render_serialized(result, ReviewablePerformResultSerializer)
|
|
else
|
|
render_json_error(result)
|
|
end
|
|
end
|
|
|
|
def settings
|
|
raise Discourse::InvalidAccess.new unless current_user.admin?
|
|
|
|
post_action_types = PostActionType.where(id: PostActionType.flag_types.values).order("id")
|
|
|
|
if request.put?
|
|
params[:reviewable_priorities].each do |id, priority|
|
|
if !priority.nil? && Reviewable.priorities.has_value?(priority.to_i)
|
|
# For now, the score bonus is equal to the priority. In the future we might want
|
|
# to calculate it a different way.
|
|
PostActionType.where(id: id).update_all(
|
|
reviewable_priority: priority.to_i,
|
|
score_bonus: priority.to_f,
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
data = { reviewable_score_types: post_action_types }
|
|
render_serialized(data, ReviewableSettingsSerializer, rest_serializer: true)
|
|
end
|
|
|
|
protected
|
|
|
|
def claim_error?(reviewable)
|
|
return if SiteSetting.reviewable_claiming == "disabled" || reviewable.topic_id.blank?
|
|
|
|
claimed_by_id = ReviewableClaimedTopic.where(topic_id: reviewable.topic_id).pluck(:user_id)[0]
|
|
|
|
if SiteSetting.reviewable_claiming == "required" && claimed_by_id.blank?
|
|
I18n.t("reviewables.must_claim")
|
|
elsif claimed_by_id.present? && claimed_by_id != current_user.id
|
|
I18n.t("reviewables.user_claimed")
|
|
end
|
|
end
|
|
|
|
def find_reviewable
|
|
reviewable = Reviewable.viewable_by(current_user).where(id: params[:reviewable_id]).first
|
|
raise Discourse::NotFound.new if reviewable.blank?
|
|
reviewable
|
|
end
|
|
|
|
def allowed_statuses
|
|
@allowed_statuses ||= (%i[reviewed all] + Reviewable.statuses.symbolize_keys.keys)
|
|
end
|
|
|
|
def version_required
|
|
render_json_error(I18n.t("reviewables.missing_version"), status: 422) if params[:version].blank?
|
|
end
|
|
|
|
def meta_types
|
|
{ created_by: "user", target_created_by: "user", reviewed_by: "user", claimed_by: "user" }
|
|
end
|
|
|
|
def ensure_can_see
|
|
Guardian.new(current_user).ensure_can_see_review_queue!
|
|
end
|
|
end
|