mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 01:32:23 +08:00
821e9cb649
Calling pluck is instantly making a SELECT, while passing the relationship allows rails to build a correct query. Before (2 selects): ``` pry(main)> Post.where(topic_id: Topic.where(id: [1,3,4]).pluck(:id)).count (1.3ms) SELECT "topics"."id" FROM "topics" WHERE "topics"."deleted_at" IS NULL AND "topics"."id" IN (1, 3, 4) Post Count (0.5ms) SELECT COUNT(*) FROM "posts" WHERE "posts"."deleted_at" IS NULL AND "posts"."topic_id" IN (1, 3, 4) ``` After (1 select): ``` pry(main)> Post.where(topic_id: Topic.where(id: [1,3,4])).count Post Count (2.7ms) SELECT COUNT(*) FROM "posts" WHERE "posts"."deleted_at" IS NULL AND "posts"."topic_id" IN (SELECT "topics"."id" FROM "topics" WHERE "topics"."deleted_at" IS NULL AND "topics"."id" IN (1, 3, 4)) ```
979 lines
28 KiB
Ruby
979 lines
28 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class PostsController < ApplicationController
|
|
# Bug with Rails 7+
|
|
# see https://github.com/rails/rails/issues/44867
|
|
self._flash_types -= [:notice]
|
|
|
|
requires_login except: %i[
|
|
show
|
|
replies
|
|
by_number
|
|
by_date
|
|
short_link
|
|
reply_history
|
|
reply_ids
|
|
revisions
|
|
latest_revision
|
|
expand_embed
|
|
markdown_id
|
|
markdown_num
|
|
cooked
|
|
latest
|
|
user_posts_feed
|
|
]
|
|
|
|
skip_before_action :preload_json,
|
|
:check_xhr,
|
|
only: %i[markdown_id markdown_num short_link latest user_posts_feed]
|
|
|
|
MARKDOWN_TOPIC_PAGE_SIZE ||= 100
|
|
|
|
def markdown_id
|
|
markdown Post.find_by(id: params[:id].to_i)
|
|
end
|
|
|
|
def markdown_num
|
|
if params[:revision].present?
|
|
post_revision = find_post_revision_from_topic_id
|
|
render plain: post_revision.modifications[:raw].last
|
|
elsif params[:post_number].present?
|
|
markdown Post.find_by(
|
|
topic_id: params[:topic_id].to_i,
|
|
post_number: params[:post_number].to_i,
|
|
)
|
|
else
|
|
opts = params.slice(:page)
|
|
opts[:limit] = MARKDOWN_TOPIC_PAGE_SIZE
|
|
topic_view = TopicView.new(params[:topic_id], current_user, opts)
|
|
content = topic_view.posts.map { |p| <<~MD }
|
|
#{p.user.username} | #{p.updated_at} | ##{p.post_number}
|
|
|
|
#{p.raw}
|
|
|
|
-------------------------
|
|
|
|
MD
|
|
render plain: content.join
|
|
end
|
|
end
|
|
|
|
def latest
|
|
params.permit(:before)
|
|
last_post_id = params[:before].to_i
|
|
last_post_id = Post.last.id if last_post_id <= 0
|
|
|
|
if params[:id] == "private_posts"
|
|
raise Discourse::NotFound if current_user.nil?
|
|
posts =
|
|
Post
|
|
.private_posts
|
|
.order(created_at: :desc)
|
|
.where("posts.id <= ?", last_post_id)
|
|
.where("posts.id > ?", last_post_id - 50)
|
|
.includes(topic: :category)
|
|
.includes(user: %i[primary_group flair_group])
|
|
.includes(:reply_to_user)
|
|
.limit(50)
|
|
rss_description = I18n.t("rss_description.private_posts")
|
|
else
|
|
posts =
|
|
Post
|
|
.public_posts
|
|
.visible
|
|
.where(post_type: Post.types[:regular])
|
|
.order(created_at: :desc)
|
|
.where("posts.id <= ?", last_post_id)
|
|
.where("posts.id > ?", last_post_id - 50)
|
|
.includes(topic: :category)
|
|
.includes(user: %i[primary_group flair_group])
|
|
.includes(:reply_to_user)
|
|
.limit(50)
|
|
rss_description = I18n.t("rss_description.posts")
|
|
@use_canonical = true
|
|
end
|
|
|
|
# Remove posts the user doesn't have permission to see
|
|
# This isn't leaking any information we weren't already through the post ID numbers
|
|
posts = posts.reject { |post| !guardian.can_see?(post) || post.topic.blank? }
|
|
counts = PostAction.counts_for(posts, current_user)
|
|
|
|
respond_to do |format|
|
|
format.rss do
|
|
@posts = posts
|
|
@title = "#{SiteSetting.title} - #{rss_description}"
|
|
@link = Discourse.base_url
|
|
@description = rss_description
|
|
render "posts/latest", formats: [:rss]
|
|
end
|
|
format.json do
|
|
render_json_dump(
|
|
serialize_data(
|
|
posts,
|
|
PostSerializer,
|
|
scope: guardian,
|
|
root: params[:id],
|
|
add_raw: true,
|
|
add_title: true,
|
|
all_post_actions: counts,
|
|
),
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
def user_posts_feed
|
|
params.require(:username)
|
|
user = fetch_user_from_params
|
|
raise Discourse::NotFound unless guardian.can_see_profile?(user)
|
|
|
|
posts =
|
|
Post
|
|
.public_posts
|
|
.visible
|
|
.where(user_id: user.id)
|
|
.where(post_type: Post.types[:regular])
|
|
.order(created_at: :desc)
|
|
.includes(:user)
|
|
.includes(topic: :category)
|
|
.limit(50)
|
|
|
|
posts = posts.reject { |post| !guardian.can_see?(post) || post.topic.blank? }
|
|
|
|
respond_to do |format|
|
|
format.rss do
|
|
@posts = posts
|
|
@title =
|
|
"#{SiteSetting.title} - #{I18n.t("rss_description.user_posts", username: user.username)}"
|
|
@link = "#{user.full_url}/activity"
|
|
@description = I18n.t("rss_description.user_posts", username: user.username)
|
|
render "posts/latest", formats: [:rss]
|
|
end
|
|
|
|
format.json do
|
|
render_json_dump(serialize_data(posts, PostSerializer, scope: guardian, add_excerpt: true))
|
|
end
|
|
end
|
|
end
|
|
|
|
def cooked
|
|
render json: { cooked: find_post_from_params.cooked }
|
|
end
|
|
|
|
def raw_email
|
|
params.require(:id)
|
|
post = Post.unscoped.find(params[:id].to_i)
|
|
guardian.ensure_can_view_raw_email!(post)
|
|
text, html = Email.extract_parts(post.raw_email)
|
|
render json: { raw_email: post.raw_email, text_part: text, html_part: html }
|
|
end
|
|
|
|
def short_link
|
|
post = Post.find_by(id: params[:post_id].to_i)
|
|
raise Discourse::NotFound unless post
|
|
|
|
# Stuff the user in the request object, because that's what IncomingLink wants
|
|
if params[:user_id]
|
|
user = User.find_by(id: params[:user_id].to_i)
|
|
request["u"] = user.username_lower if user
|
|
end
|
|
|
|
guardian.ensure_can_see!(post)
|
|
redirect_to path(post.url)
|
|
end
|
|
|
|
def create
|
|
manager_params = create_params
|
|
manager_params[:first_post_checks] = !is_api?
|
|
manager_params[:advance_draft] = !is_api?
|
|
|
|
manager = NewPostManager.new(current_user, manager_params)
|
|
|
|
json =
|
|
if is_api?
|
|
memoized_payload =
|
|
DistributedMemoizer.memoize(signature_for(manager_params), 120) do
|
|
MultiJson.dump(serialize_data(manager.perform, NewPostResultSerializer, root: false))
|
|
end
|
|
|
|
JSON.parse(memoized_payload)
|
|
else
|
|
serialize_data(manager.perform, NewPostResultSerializer, root: false)
|
|
end
|
|
|
|
backwards_compatible_json(json)
|
|
end
|
|
|
|
def update
|
|
params.require(:post)
|
|
|
|
post = Post.where(id: params[:id])
|
|
post = post.with_deleted if guardian.is_staff?
|
|
post = post.first
|
|
|
|
raise Discourse::NotFound if post.blank?
|
|
|
|
post.image_sizes = params[:image_sizes] if params[:image_sizes].present?
|
|
|
|
if !guardian.public_send("can_edit?", post) && post.user_id == current_user.id &&
|
|
post.edit_time_limit_expired?(current_user)
|
|
return render_json_error(I18n.t("too_late_to_edit"))
|
|
end
|
|
|
|
guardian.ensure_can_edit!(post)
|
|
|
|
changes = { raw: params[:post][:raw], edit_reason: params[:post][:edit_reason] }
|
|
|
|
Post.plugin_permitted_update_params.keys.each { |param| changes[param] = params[:post][param] }
|
|
|
|
raw_old = params[:post][:raw_old]
|
|
if raw_old.present? && raw_old != post.raw
|
|
return render_json_error(I18n.t("edit_conflict"), status: 409)
|
|
end
|
|
|
|
# to stay consistent with the create api, we allow for title & category changes here
|
|
if post.is_first_post?
|
|
changes[:title] = params[:title] if params[:title]
|
|
changes[:category_id] = params[:post][:category_id] if params[:post][:category_id]
|
|
|
|
if changes[:category_id] && changes[:category_id].to_i != post.topic.category_id.to_i
|
|
category = Category.find_by(id: changes[:category_id])
|
|
if category || (changes[:category_id].to_i == 0)
|
|
guardian.ensure_can_move_topic_to_category!(category)
|
|
else
|
|
return render_json_error(I18n.t("category.errors.not_found"))
|
|
end
|
|
end
|
|
end
|
|
|
|
# We don't need to validate edits to small action posts by staff
|
|
opts = {}
|
|
if post.post_type == Post.types[:small_action] && current_user.staff?
|
|
opts[:skip_validations] = true
|
|
end
|
|
|
|
topic = post.topic
|
|
topic = Topic.with_deleted.find(post.topic_id) if guardian.is_staff?
|
|
|
|
revisor = PostRevisor.new(post, topic)
|
|
revisor.revise!(current_user, changes, opts)
|
|
|
|
return render_json_error(post) if post.errors.present?
|
|
return render_json_error(topic) if topic.errors.present?
|
|
|
|
post_serializer = PostSerializer.new(post, scope: guardian, root: false, add_raw: true)
|
|
post_serializer.draft_sequence = DraftSequence.current(current_user, topic.draft_key)
|
|
link_counts = TopicLink.counts_for(guardian, topic, [post])
|
|
post_serializer.single_post_link_counts = link_counts[post.id] if link_counts.present?
|
|
|
|
result = { post: post_serializer.as_json }
|
|
if revisor.category_changed.present?
|
|
result[:category] = BasicCategorySerializer.new(
|
|
revisor.category_changed,
|
|
scope: guardian,
|
|
root: false,
|
|
).as_json
|
|
end
|
|
|
|
render_json_dump(result)
|
|
end
|
|
|
|
def show
|
|
post = find_post_from_params
|
|
display_post(post)
|
|
end
|
|
|
|
def by_number
|
|
post = find_post_from_params_by_number
|
|
display_post(post)
|
|
end
|
|
|
|
def by_date
|
|
post = find_post_from_params_by_date
|
|
display_post(post)
|
|
end
|
|
|
|
def reply_history
|
|
post = find_post_from_params
|
|
|
|
reply_history = post.reply_history(params[:max_replies].to_i, guardian)
|
|
user_custom_fields = {}
|
|
if (added_fields = User.allowed_user_custom_fields(guardian)).present?
|
|
user_custom_fields = User.custom_fields_for_ids(reply_history.pluck(:user_id), added_fields)
|
|
end
|
|
|
|
render_serialized(reply_history, PostSerializer, user_custom_fields: user_custom_fields)
|
|
end
|
|
|
|
def reply_ids
|
|
post = find_post_from_params
|
|
render json: post.reply_ids(guardian).to_json
|
|
end
|
|
|
|
def all_reply_ids
|
|
Discourse.deprecate("/posts/:id/reply-ids/all is deprecated.", drop_from: "3.0")
|
|
|
|
post = find_post_from_params
|
|
render json: post.reply_ids(guardian, only_replies_to_single_post: false).to_json
|
|
end
|
|
|
|
def destroy
|
|
post = find_post_from_params
|
|
force_destroy = ActiveModel::Type::Boolean.new.cast(params[:force_destroy])
|
|
|
|
if force_destroy
|
|
if !guardian.can_permanently_delete?(post)
|
|
return render_json_error post.cannot_permanently_delete_reason(current_user), status: 403
|
|
end
|
|
else
|
|
guardian.ensure_can_delete!(post)
|
|
end
|
|
|
|
unless guardian.can_moderate_topic?(post.topic)
|
|
RateLimiter.new(
|
|
current_user,
|
|
"delete_post_per_min",
|
|
SiteSetting.max_post_deletions_per_minute,
|
|
1.minute,
|
|
).performed!
|
|
RateLimiter.new(
|
|
current_user,
|
|
"delete_post_per_day",
|
|
SiteSetting.max_post_deletions_per_day,
|
|
1.day,
|
|
).performed!
|
|
end
|
|
|
|
PostDestroyer.new(
|
|
current_user,
|
|
post,
|
|
context: params[:context],
|
|
force_destroy: force_destroy,
|
|
).destroy
|
|
|
|
render body: nil
|
|
end
|
|
|
|
def expand_embed
|
|
render json: { cooked: TopicEmbed.expanded_for(find_post_from_params) }
|
|
rescue StandardError
|
|
render_json_error I18n.t("errors.embed.load_from_remote")
|
|
end
|
|
|
|
def recover
|
|
post = find_post_from_params
|
|
guardian.ensure_can_recover_post!(post)
|
|
|
|
unless guardian.can_moderate_topic?(post.topic)
|
|
RateLimiter.new(
|
|
current_user,
|
|
"delete_post_per_min",
|
|
SiteSetting.max_post_deletions_per_minute,
|
|
1.minute,
|
|
).performed!
|
|
RateLimiter.new(
|
|
current_user,
|
|
"delete_post_per_day",
|
|
SiteSetting.max_post_deletions_per_day,
|
|
1.day,
|
|
).performed!
|
|
end
|
|
|
|
destroyer = PostDestroyer.new(current_user, post)
|
|
destroyer.recover
|
|
post.reload
|
|
|
|
render_post_json(post)
|
|
end
|
|
|
|
def destroy_many
|
|
params.require(:post_ids)
|
|
agree_with_first_reply_flag = (params[:agree_with_first_reply_flag] || true).to_s == "true"
|
|
|
|
posts = Post.where(id: post_ids_including_replies).order(:id)
|
|
raise Discourse::InvalidParameters.new(:post_ids) if posts.blank?
|
|
|
|
# Make sure we can delete the posts
|
|
posts.each { |p| guardian.ensure_can_delete!(p) }
|
|
|
|
Post.transaction do
|
|
posts.each_with_index do |p, i|
|
|
PostDestroyer.new(
|
|
current_user,
|
|
p,
|
|
defer_flags: !(agree_with_first_reply_flag && i == 0),
|
|
).destroy
|
|
end
|
|
end
|
|
|
|
render body: nil
|
|
end
|
|
|
|
def merge_posts
|
|
params.require(:post_ids)
|
|
posts = Post.where(id: params[:post_ids]).order(:id)
|
|
raise Discourse::InvalidParameters.new(:post_ids) if posts.pluck(:id) == params[:post_ids]
|
|
PostMerger.new(current_user, posts).merge
|
|
render body: nil
|
|
rescue PostMerger::CannotMergeError => e
|
|
render_json_error(e.message)
|
|
end
|
|
|
|
# Direct replies to this post
|
|
def replies
|
|
post = find_post_from_params
|
|
replies = post.replies.secured(guardian)
|
|
|
|
user_custom_fields = {}
|
|
if (added_fields = User.allowed_user_custom_fields(guardian)).present?
|
|
user_custom_fields = User.custom_fields_for_ids(replies.pluck(:user_id), added_fields)
|
|
end
|
|
|
|
render_serialized(replies, PostSerializer, user_custom_fields: user_custom_fields)
|
|
end
|
|
|
|
def revisions
|
|
post = find_post_from_params
|
|
raise Discourse::NotFound if post.hidden && !guardian.can_view_hidden_post_revisions?
|
|
|
|
post_revision = find_post_revision_from_params
|
|
post_revision_serializer =
|
|
PostRevisionSerializer.new(post_revision, scope: guardian, root: false)
|
|
render_json_dump(post_revision_serializer)
|
|
end
|
|
|
|
def latest_revision
|
|
post = find_post_from_params
|
|
raise Discourse::NotFound if post.hidden && !guardian.can_view_hidden_post_revisions?
|
|
|
|
post_revision = find_latest_post_revision_from_params
|
|
post_revision_serializer =
|
|
PostRevisionSerializer.new(post_revision, scope: guardian, root: false)
|
|
render_json_dump(post_revision_serializer)
|
|
end
|
|
|
|
def hide_revision
|
|
post_revision = find_post_revision_from_params
|
|
guardian.ensure_can_hide_post_revision!(post_revision)
|
|
|
|
post_revision.hide!
|
|
|
|
post = find_post_from_params
|
|
post.public_version -= 1
|
|
post.save
|
|
|
|
render body: nil
|
|
end
|
|
|
|
def permanently_delete_revisions
|
|
guardian.ensure_can_permanently_delete_post_revisions!
|
|
|
|
post = find_post_from_params
|
|
raise Discourse::InvalidParameters.new(:post) if post.blank?
|
|
raise Discourse::NotFound unless post.revisions.present?
|
|
|
|
RateLimiter.new(
|
|
current_user,
|
|
"admin_permanently_delete_post_revisions",
|
|
20,
|
|
1.minute,
|
|
apply_limit_to_staff: true,
|
|
).performed!
|
|
|
|
ActiveRecord::Base.transaction do
|
|
updated_at = Time.zone.now
|
|
post.revisions.destroy_all
|
|
post.update(version: 1, public_version: 1, last_version_at: updated_at)
|
|
StaffActionLogger.new(current_user).log_permanently_delete_post_revisions(post)
|
|
end
|
|
|
|
post.rebake!
|
|
|
|
render body: nil
|
|
end
|
|
|
|
def show_revision
|
|
post_revision = find_post_revision_from_params
|
|
guardian.ensure_can_show_post_revision!(post_revision)
|
|
|
|
post_revision.show!
|
|
|
|
post = find_post_from_params
|
|
post.public_version += 1
|
|
post.save
|
|
|
|
render body: nil
|
|
end
|
|
|
|
def revert
|
|
raise Discourse::NotFound unless guardian.is_staff?
|
|
|
|
post_id = params[:id] || params[:post_id]
|
|
revision = params[:revision].to_i
|
|
raise Discourse::InvalidParameters.new(:revision) if revision < 2
|
|
|
|
post_revision = PostRevision.find_by(post_id: post_id, number: revision)
|
|
raise Discourse::NotFound unless post_revision
|
|
|
|
post = find_post_from_params
|
|
raise Discourse::NotFound if post.blank?
|
|
|
|
post_revision.post = post
|
|
guardian.ensure_can_see!(post_revision)
|
|
guardian.ensure_can_edit!(post)
|
|
if post_revision.modifications["raw"].blank? && post_revision.modifications["title"].blank? &&
|
|
post_revision.modifications["category_id"].blank?
|
|
return render_json_error(I18n.t("revert_version_same"))
|
|
end
|
|
|
|
topic = Topic.with_deleted.find(post.topic_id)
|
|
|
|
changes = {}
|
|
changes[:raw] = post_revision.modifications["raw"][0] if post_revision.modifications[
|
|
"raw"
|
|
].present? && post_revision.modifications["raw"][0] != post.raw
|
|
if post.is_first_post?
|
|
changes[:title] = post_revision.modifications["title"][0] if post_revision.modifications[
|
|
"title"
|
|
].present? && post_revision.modifications["title"][0] != topic.title
|
|
changes[:category_id] = post_revision.modifications["category_id"][
|
|
0
|
|
] if post_revision.modifications["category_id"].present? &&
|
|
post_revision.modifications["category_id"][0] != topic.category.id
|
|
end
|
|
return render_json_error(I18n.t("revert_version_same")) if changes.length <= 0
|
|
changes[:edit_reason] = I18n.with_locale(SiteSetting.default_locale) do
|
|
I18n.t("reverted_to_version", version: post_revision.number.to_i - 1)
|
|
end
|
|
|
|
revisor = PostRevisor.new(post, topic)
|
|
revisor.revise!(current_user, changes)
|
|
|
|
return render_json_error(post) if post.errors.present?
|
|
return render_json_error(topic) if topic.errors.present?
|
|
|
|
post_serializer = PostSerializer.new(post, scope: guardian, root: false)
|
|
post_serializer.draft_sequence = DraftSequence.current(current_user, topic.draft_key)
|
|
|
|
link_counts = TopicLink.counts_for(guardian, topic, [post])
|
|
post_serializer.single_post_link_counts = link_counts[post.id] if link_counts.present?
|
|
|
|
result = { post: post_serializer.as_json }
|
|
if post.is_first_post?
|
|
result[:topic] = BasicTopicSerializer.new(
|
|
topic,
|
|
scope: guardian,
|
|
root: false,
|
|
).as_json if post_revision.modifications["title"].present?
|
|
result[:category_id] = post_revision.modifications["category_id"][
|
|
0
|
|
] if post_revision.modifications["category_id"].present?
|
|
end
|
|
|
|
render_json_dump(result)
|
|
end
|
|
|
|
def locked
|
|
post = find_post_from_params
|
|
locker = PostLocker.new(post, current_user)
|
|
params[:locked] === "true" ? locker.lock : locker.unlock
|
|
render_json_dump(locked: post.locked?)
|
|
end
|
|
|
|
def notice
|
|
post = find_post_from_params
|
|
raise Discourse::NotFound unless guardian.can_edit_staff_notes?(post.topic)
|
|
|
|
old_notice = post.custom_fields[Post::NOTICE]
|
|
|
|
if params[:notice].present?
|
|
post.custom_fields[Post::NOTICE] = {
|
|
type: Post.notices[:custom],
|
|
raw: params[:notice],
|
|
cooked: PrettyText.cook(params[:notice], features: { onebox: false }),
|
|
}
|
|
else
|
|
post.custom_fields.delete(Post::NOTICE)
|
|
end
|
|
|
|
post.save_custom_fields
|
|
|
|
StaffActionLogger.new(current_user).log_post_staff_note(
|
|
post,
|
|
old_value: old_notice&.[]("raw"),
|
|
new_value: params[:notice],
|
|
)
|
|
|
|
render body: nil
|
|
end
|
|
|
|
def destroy_bookmark
|
|
params.require(:post_id)
|
|
|
|
bookmark_id =
|
|
Bookmark.where(
|
|
bookmarkable_id: params[:post_id],
|
|
bookmarkable_type: "Post",
|
|
user_id: current_user.id,
|
|
).pick(:id)
|
|
destroyed_bookmark = BookmarkManager.new(current_user).destroy(bookmark_id)
|
|
|
|
render json:
|
|
success_json.merge(BookmarkManager.bookmark_metadata(destroyed_bookmark, current_user))
|
|
end
|
|
|
|
def wiki
|
|
post = find_post_from_params
|
|
params.require(:wiki)
|
|
guardian.ensure_can_wiki!(post)
|
|
|
|
post.revise(current_user, wiki: params[:wiki])
|
|
|
|
render body: nil
|
|
end
|
|
|
|
def post_type
|
|
guardian.ensure_can_change_post_type!
|
|
post = find_post_from_params
|
|
params.require(:post_type)
|
|
raise Discourse::InvalidParameters.new(:post_type) if Post.types[params[:post_type].to_i].blank?
|
|
|
|
post.revise(current_user, post_type: params[:post_type].to_i)
|
|
|
|
render body: nil
|
|
end
|
|
|
|
def rebake
|
|
guardian.ensure_can_rebake!
|
|
|
|
post = find_post_from_params
|
|
post.rebake!(invalidate_oneboxes: true, invalidate_broken_images: true)
|
|
|
|
render body: nil
|
|
end
|
|
|
|
def unhide
|
|
post = find_post_from_params
|
|
|
|
guardian.ensure_can_unhide!(post)
|
|
|
|
post.unhide!
|
|
|
|
render body: nil
|
|
end
|
|
|
|
def deleted_posts
|
|
params.permit(:offset, :limit)
|
|
guardian.ensure_can_see_deleted_posts!
|
|
|
|
user = fetch_user_from_params
|
|
offset = [params[:offset].to_i, 0].max
|
|
limit = [(params[:limit] || 60).to_i, 100].min
|
|
|
|
posts = user_posts(guardian, user.id, offset: offset, limit: limit).where.not(deleted_at: nil)
|
|
|
|
render_serialized(posts, AdminUserActionSerializer)
|
|
end
|
|
|
|
def pending
|
|
params.require(:username)
|
|
user = fetch_user_from_params
|
|
raise Discourse::NotFound unless guardian.can_edit_user?(user)
|
|
|
|
render_serialized(
|
|
user.pending_posts.order(created_at: :desc),
|
|
PendingPostSerializer,
|
|
root: :pending_posts,
|
|
)
|
|
end
|
|
|
|
protected
|
|
|
|
def markdown(post)
|
|
if post && guardian.can_see?(post)
|
|
render plain: post.raw
|
|
else
|
|
raise Discourse::NotFound
|
|
end
|
|
end
|
|
|
|
# We can't break the API for making posts. The new, queue supporting API
|
|
# doesn't return the post as the root JSON object, but as a nested object.
|
|
# If a param is present it uses that result structure.
|
|
def backwards_compatible_json(json_obj)
|
|
json_obj.symbolize_keys!
|
|
|
|
success = json_obj[:success]
|
|
|
|
if params[:nested_post].blank? && json_obj[:errors].blank? &&
|
|
json_obj[:action].to_s != "enqueued"
|
|
json_obj = json_obj[:post]
|
|
end
|
|
|
|
if !success && GlobalSetting.try(:verbose_api_logging) && (is_api? || is_user_api?)
|
|
Rails.logger.error "Error creating post via API:\n\n#{json_obj.inspect}"
|
|
end
|
|
|
|
render json: json_obj, status: (!!success) ? 200 : 422
|
|
end
|
|
|
|
def find_post_revision_from_params
|
|
post_id = params[:id] || params[:post_id]
|
|
revision = params[:revision].to_i
|
|
raise Discourse::InvalidParameters.new(:revision) if revision < 2
|
|
|
|
post_revision = PostRevision.find_by(post_id: post_id, number: revision)
|
|
raise Discourse::NotFound unless post_revision
|
|
|
|
post_revision.post = find_post_from_params
|
|
guardian.ensure_can_see!(post_revision)
|
|
|
|
post_revision
|
|
end
|
|
|
|
def find_latest_post_revision_from_params
|
|
post_id = params[:id] || params[:post_id]
|
|
|
|
finder = PostRevision.where(post_id: post_id).order(:number)
|
|
finder = finder.where(hidden: false) unless guardian.is_staff?
|
|
post_revision = finder.last
|
|
|
|
raise Discourse::NotFound unless post_revision
|
|
|
|
post_revision.post = find_post_from_params
|
|
guardian.ensure_can_see!(post_revision)
|
|
|
|
post_revision
|
|
end
|
|
|
|
def find_post_revision_from_topic_id
|
|
post =
|
|
Post.find_by(topic_id: params[:topic_id].to_i, post_number: (params[:post_number] || 1).to_i)
|
|
raise Discourse::NotFound unless guardian.can_see?(post)
|
|
|
|
revision = params[:revision].to_i
|
|
raise Discourse::NotFound if revision < 2
|
|
|
|
post_revision = PostRevision.find_by(post_id: post.id, number: revision)
|
|
raise Discourse::NotFound unless post_revision
|
|
|
|
post_revision.post = post
|
|
guardian.ensure_can_see!(post_revision)
|
|
|
|
post_revision
|
|
end
|
|
|
|
private
|
|
|
|
def user_posts(guardian, user_id, opts)
|
|
# Topic.unscoped is necessary to remove the default deleted_at: nil scope
|
|
posts =
|
|
Topic.unscoped do
|
|
Post
|
|
.includes(:user, :topic, :deleted_by, :user_actions)
|
|
.where(user_id: user_id)
|
|
.with_deleted
|
|
.order(created_at: :desc)
|
|
end
|
|
|
|
if guardian.user.moderator?
|
|
# Awful hack, but you can't seem to remove the `default_scope` when joining
|
|
# So instead I grab the topics separately
|
|
topic_ids = posts.dup.pluck(:topic_id)
|
|
topics = Topic.where(id: topic_ids).with_deleted.where.not(archetype: "private_message")
|
|
topics = topics.secured(guardian)
|
|
|
|
posts = posts.where(topic_id: topics)
|
|
end
|
|
|
|
posts.offset(opts[:offset]).limit(opts[:limit])
|
|
end
|
|
|
|
def create_params
|
|
permitted = [
|
|
:raw,
|
|
:topic_id,
|
|
:archetype,
|
|
:category,
|
|
# TODO remove together with 'targetUsername' deprecations
|
|
:target_usernames,
|
|
:target_recipients,
|
|
:reply_to_post_number,
|
|
:auto_track,
|
|
:typing_duration_msecs,
|
|
:composer_open_duration_msecs,
|
|
:visible,
|
|
:draft_key,
|
|
]
|
|
|
|
Post.plugin_permitted_create_params.each do |key, value|
|
|
if value[:plugin].enabled?
|
|
permitted << case value[:type]
|
|
when :string
|
|
key.to_sym
|
|
when :array
|
|
{ key => [] }
|
|
when :hash
|
|
{ key => {} }
|
|
end
|
|
end
|
|
end
|
|
|
|
# param munging for WordPress
|
|
params[:auto_track] = !(params[:auto_track].to_s == "false") if params[:auto_track]
|
|
params[:visible] = (params[:unlist_topic].to_s == "false") if params[:unlist_topic]
|
|
|
|
if is_api?
|
|
# php seems to be sending this incorrectly, don't fight with it
|
|
params[:skip_validations] = params[:skip_validations].to_s == "true"
|
|
permitted << :skip_validations
|
|
|
|
params[:import_mode] = params[:import_mode].to_s == "true"
|
|
permitted << :import_mode
|
|
|
|
# We allow `embed_url` via the API
|
|
permitted << :embed_url
|
|
|
|
# We allow `created_at` via the API
|
|
permitted << :created_at
|
|
|
|
# We allow `external_id` via the API
|
|
permitted << :external_id
|
|
end
|
|
|
|
result =
|
|
params
|
|
.permit(*permitted)
|
|
.tap do |allowed|
|
|
allowed[:image_sizes] = params[:image_sizes]
|
|
# TODO this does not feel right, we should name what meta_data is allowed
|
|
allowed[:meta_data] = params[:meta_data]
|
|
end
|
|
|
|
# Staff are allowed to pass `is_warning`
|
|
if current_user.staff?
|
|
params.permit(:is_warning)
|
|
result[:is_warning] = (params[:is_warning] == "true")
|
|
else
|
|
result[:is_warning] = false
|
|
end
|
|
|
|
if params[:no_bump] == "true"
|
|
raise Discourse::InvalidParameters.new(:no_bump) unless guardian.can_skip_bump?
|
|
result[:no_bump] = true
|
|
end
|
|
|
|
if params[:shared_draft] == "true"
|
|
raise Discourse::InvalidParameters.new(:shared_draft) unless guardian.can_create_shared_draft?
|
|
|
|
result[:shared_draft] = true
|
|
end
|
|
|
|
if params[:whisper] == "true"
|
|
unless guardian.can_create_whisper?
|
|
raise Discourse::InvalidAccess.new(
|
|
"invalid_whisper_access",
|
|
nil,
|
|
custom_message: "invalid_whisper_access",
|
|
)
|
|
end
|
|
|
|
result[:post_type] = Post.types[:whisper]
|
|
end
|
|
|
|
PostRevisor.tracked_topic_fields.each_key do |f|
|
|
params.permit(f => [])
|
|
result[f] = params[f] if params.has_key?(f)
|
|
end
|
|
|
|
# Stuff we can use in spam prevention plugins
|
|
result[:ip_address] = request.remote_ip
|
|
result[:user_agent] = request.user_agent
|
|
result[:referrer] = request.env["HTTP_REFERER"]
|
|
|
|
if recipients = result[:target_usernames]
|
|
Discourse.deprecate(
|
|
"`target_usernames` is deprecated, use `target_recipients` instead.",
|
|
output_in_test: true,
|
|
drop_from: "2.9.0",
|
|
)
|
|
else
|
|
recipients = result[:target_recipients]
|
|
end
|
|
|
|
if recipients
|
|
recipients = recipients.split(",").map(&:downcase)
|
|
groups =
|
|
Group.messageable(current_user).where("lower(name) in (?)", recipients).pluck("lower(name)")
|
|
recipients -= groups
|
|
emails = recipients.select { |user| user.match(/@/) }
|
|
recipients -= emails
|
|
result[:target_usernames] = recipients.join(",")
|
|
result[:target_emails] = emails.join(",")
|
|
result[:target_group_names] = groups.join(",")
|
|
end
|
|
|
|
result.permit!
|
|
result.to_h
|
|
end
|
|
|
|
def signature_for(args)
|
|
+"post##" << Digest::SHA1.hexdigest(
|
|
args
|
|
.to_h
|
|
.to_a
|
|
.concat([["user", current_user.id]])
|
|
.sort { |x, y| x[0] <=> y[0] }
|
|
.join { |x, y| "#{x}:#{y}" },
|
|
)
|
|
end
|
|
|
|
def display_post(post)
|
|
post.revert_to(params[:version].to_i) if params[:version].present?
|
|
render_post_json(post)
|
|
end
|
|
|
|
def find_post_from_params
|
|
by_id_finder = Post.where(id: params[:id] || params[:post_id])
|
|
find_post_using(by_id_finder)
|
|
end
|
|
|
|
def find_post_from_params_by_number
|
|
by_number_finder = Post.where(topic_id: params[:topic_id], post_number: params[:post_number])
|
|
find_post_using(by_number_finder)
|
|
end
|
|
|
|
def find_post_from_params_by_date
|
|
by_date_finder =
|
|
TopicView
|
|
.new(params[:topic_id], current_user)
|
|
.filtered_posts
|
|
.where("created_at >= ?", Time.zone.parse(params[:date]))
|
|
.order("created_at ASC")
|
|
.limit(1)
|
|
|
|
find_post_using(by_date_finder)
|
|
end
|
|
|
|
def find_post_using(finder)
|
|
# A deleted post can be seen by staff or a category group moderator for the topic.
|
|
# But we must find the deleted post to determine which category it belongs to, so
|
|
# we must find.with_deleted
|
|
post = finder.with_deleted.first
|
|
raise Discourse::NotFound unless post
|
|
|
|
post.topic = Topic.with_deleted.find_by(id: post.topic_id)
|
|
|
|
if !post.topic ||
|
|
(
|
|
(post.deleted_at.present? || post.topic.deleted_at.present?) &&
|
|
!guardian.can_moderate_topic?(post.topic)
|
|
)
|
|
raise Discourse::NotFound
|
|
end
|
|
|
|
guardian.ensure_can_see!(post)
|
|
post
|
|
end
|
|
end
|