discourse/app/controllers/posts_controller.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1011 lines
29 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
2013-02-06 03:16:51 +08:00
class PostsController < ApplicationController
# Bug with Rails 7+
# see https://github.com/rails/rails/issues/44867
self._flash_types -= [:notice]
2013-02-06 03:16:51 +08:00
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 = nil if last_post_id <= 0
if params[:id] == "private_posts"
raise Discourse::NotFound if current_user.nil?
allowed_private_topics = TopicAllowedUser.where(user_id: current_user.id).select(:topic_id)
allowed_groups = GroupUser.where(user_id: current_user.id).select(:group_id)
allowed_private_topics_by_group =
TopicAllowedGroup.where(group_id: allowed_groups).select(:topic_id)
all_allowed =
Topic
.where(id: allowed_private_topics)
.or(Topic.where(id: allowed_private_topics_by_group))
.select(:id)
posts =
Post
.private_posts
.order(id: :desc)
.includes(topic: :category)
.includes(user: %i[primary_group flair_group])
.includes(:reply_to_user)
.limit(50)
rss_description = I18n.t("rss_description.private_posts")
posts = posts.where(topic_id: all_allowed) if !current_user.admin?
else
posts =
Post
.public_posts
.visible
.where(post_type: Post.types[:regular])
.order(id: :desc)
.includes(topic: :category)
.includes(user: %i[primary_group flair_group])
.includes(:reply_to_user)
.where("categories.id" => Category.secured(guardian).select(:id))
.limit(50)
rss_description = I18n.t("rss_description.posts")
@use_canonical = true
end
posts = posts.where("posts.id <= ?", last_post_id) if last_post_id
posts = posts.to_a
counts = PostAction.counts_for(posts, current_user)
2015-06-09 19:39:03 +08:00
respond_to do |format|
format.rss do
@posts = posts
@title = "#{SiteSetting.title} - #{rss_description}"
2015-06-09 19:39:03 +08:00
@link = Discourse.base_url
@description = rss_description
2015-06-09 19:39:03 +08:00
render "posts/latest", formats: [:rss]
end
format.json do
render_json_dump(
serialize_data(
posts,
PostSerializer,
scope: guardian,
root: params[:id],
2015-06-09 19:39:03 +08:00
add_raw: true,
add_title: true,
2015-06-09 19:39:03 +08:00
all_post_actions: counts,
),
2015-06-09 19:39:03 +08:00
)
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
2014-10-18 03:18:29 +08:00
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 }
2014-10-18 03:18:29 +08:00
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
2013-02-06 03:16:51 +08:00
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
2013-02-06 03:16:51 +08:00
def update
params.require(:post)
2013-02-07 23:45:24 +08:00
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?
2019-05-07 09:27:05 +08:00
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)
2013-02-06 03:16:51 +08:00
return render_json_error(post) if post.errors.present?
return render_json_error(topic) if topic.errors.present?
2013-02-06 03:16:51 +08:00
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)
2013-02-06 03:16:51 +08:00
end
2013-12-12 10:41:34 +08:00
def show
2014-02-25 01:03:29 +08:00
post = find_post_from_params
display_post(post)
2013-12-12 10:41:34 +08:00
end
2013-02-06 03:16:51 +08:00
def by_number
2014-02-25 01:03:29 +08:00
post = find_post_from_params_by_number
display_post(post)
2013-02-06 03:16:51 +08:00
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
2013-02-06 03:16:51 +08:00
def destroy
2013-02-09 06:49:15 +08:00
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
2014-04-02 05:45:16 +08:00
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")
2014-04-02 05:45:16 +08:00
end
def recover
2013-02-09 06:49:15 +08:00
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)
2013-02-06 03:16:51 +08:00
end
def destroy_many
params.require(:post_ids)
agree_with_first_reply_flag = (params[:agree_with_first_reply_flag] || true).to_s == "true"
2013-02-06 03:16:51 +08:00
posts = Post.where(id: post_ids_including_replies).order(:id)
2013-02-06 03:16:51 +08:00
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
2013-02-06 03:16:51 +08:00
end
render body: nil
2013-02-06 03:16:51 +08:00
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
2013-02-06 03:16:51 +08:00
# Direct replies to this post
def replies
2013-02-09 06:49:15 +08:00
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)
2013-02-06 03:16:51 +08:00
end
2013-12-12 10:41:34 +08:00
def revisions
post = find_post_from_params
raise Discourse::NotFound if post.hidden && !guardian.can_view_hidden_post_revisions?
2013-12-12 10:41:34 +08:00
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
FEATURE: Allow admins to permanently delete revisions (#19913) # Context This PR introduces the ability to permanently delete revisions from a post while maintaining the changes implemented by the revisions. Additional Context: /t/90301 # Functionality In the case a staff member wants to _remove the visual cue_ that a post has been edited eg. <img width="86" alt="Screenshot 2023-01-18 at 2 59 12 PM" src="https://user-images.githubusercontent.com/50783505/213293333-9c881229-ab18-4591-b39b-e3419a67907d.png"> while maintaining the changes made in the edits, they can enable the (hidden) site setting of `can_permanently_delete`. When this is enabled, after _hiding_ the revisions <img width="149" alt="Screenshot 2023-01-19 at 1 53 35 PM" src="https://user-images.githubusercontent.com/50783505/213546080-2a9e9c55-b3ef-428e-a93d-1b6ba287dfae.png"> there will be an additional button in the history modal to <kbd>Delete revisions</kbd> on a post. <img width="997" alt="Screenshot 2023-01-19 at 1 49 51 PM" src="https://user-images.githubusercontent.com/50783505/213546333-49042558-50ab-4724-9da7-08bacc68d38d.png"> Since this action is permanent, we display a confirmation dialog prior to triggering the destroy call <img width="722" alt="Screenshot 2023-01-19 at 1 55 59 PM" src="https://user-images.githubusercontent.com/50783505/213546487-96ea6e89-ac49-4892-b4b0-28996e3c867f.png"> Once confirmed the history modal will close and the post will `rebake` to display an _unedited_ post. <img width="868" alt="Screenshot 2023-01-19 at 1 56 35 PM" src="https://user-images.githubusercontent.com/50783505/213546608-d6436717-8484-4132-a1a8-b7a348d92728.png"> see that there is not a visual que for _revision have been made on this post_ for a post that **HAS** been edited. In addition to this, a user history log for `purge_post_revisions` will be added for each action completed. # Limits - Admins are rate limited to 20 posts per minute
2023-01-20 05:09:01 +08:00
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 if post.revisions.blank?
FEATURE: Allow admins to permanently delete revisions (#19913) # Context This PR introduces the ability to permanently delete revisions from a post while maintaining the changes implemented by the revisions. Additional Context: /t/90301 # Functionality In the case a staff member wants to _remove the visual cue_ that a post has been edited eg. <img width="86" alt="Screenshot 2023-01-18 at 2 59 12 PM" src="https://user-images.githubusercontent.com/50783505/213293333-9c881229-ab18-4591-b39b-e3419a67907d.png"> while maintaining the changes made in the edits, they can enable the (hidden) site setting of `can_permanently_delete`. When this is enabled, after _hiding_ the revisions <img width="149" alt="Screenshot 2023-01-19 at 1 53 35 PM" src="https://user-images.githubusercontent.com/50783505/213546080-2a9e9c55-b3ef-428e-a93d-1b6ba287dfae.png"> there will be an additional button in the history modal to <kbd>Delete revisions</kbd> on a post. <img width="997" alt="Screenshot 2023-01-19 at 1 49 51 PM" src="https://user-images.githubusercontent.com/50783505/213546333-49042558-50ab-4724-9da7-08bacc68d38d.png"> Since this action is permanent, we display a confirmation dialog prior to triggering the destroy call <img width="722" alt="Screenshot 2023-01-19 at 1 55 59 PM" src="https://user-images.githubusercontent.com/50783505/213546487-96ea6e89-ac49-4892-b4b0-28996e3c867f.png"> Once confirmed the history modal will close and the post will `rebake` to display an _unedited_ post. <img width="868" alt="Screenshot 2023-01-19 at 1 56 35 PM" src="https://user-images.githubusercontent.com/50783505/213546608-d6436717-8484-4132-a1a8-b7a348d92728.png"> see that there is not a visual que for _revision have been made on this post_ for a post that **HAS** been edited. In addition to this, a user history log for `purge_post_revisions` will be added for each action completed. # Limits - Admins are rate limited to 20 posts per minute
2023-01-20 05:09:01 +08:00
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
Improving bookmarks part 1 (#8466) Note: All of this functionality is hidden behind a hidden, default false, site setting called `enable_bookmarks_with_reminders`. Also, any feedback on Ember code would be greatly appreciated! This is part 1 of the bookmark improvements. The next PR will address the backend logic to send reminder notifications for bookmarked posts to users. This PR adds the following functionality: * We are adding a new `bookmarks` table and `Bookmark` model to make the bookmarks a first-class citizen and to allow attaching reminders to them. * Posts now have a new button in their actions menu that has the icon of an actual book * Clicking the button opens the new bookmark modal. * Both name and the reminder type are optional. * If you close the modal without doing anything, the bookmark is saved with no reminder. * If you click the Cancel button, no bookmark is saved at all. * All of the reminder type tiles are dynamic and the times they show will be based on your user timezone set in your profile (this should already be set for you). * If for some reason a user does not have their timezone set they will not be able to set a reminder, but they will still be able to create a bookmark. * A bookmark can be deleted by clicking on the book icon again which will be red if the post is bookmarked. This PR does NOT do anything to migrate or change existing bookmarks in the form of `PostActions`, the two features live side-by-side here. Also this does nothing to the topic bookmarking.
2019-12-11 12:04:02 +08:00
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))
Improving bookmarks part 1 (#8466) Note: All of this functionality is hidden behind a hidden, default false, site setting called `enable_bookmarks_with_reminders`. Also, any feedback on Ember code would be greatly appreciated! This is part 1 of the bookmark improvements. The next PR will address the backend logic to send reminder notifications for bookmarked posts to users. This PR adds the following functionality: * We are adding a new `bookmarks` table and `Bookmark` model to make the bookmarks a first-class citizen and to allow attaching reminders to them. * Posts now have a new button in their actions menu that has the icon of an actual book * Clicking the button opens the new bookmark modal. * Both name and the reminder type are optional. * If you close the modal without doing anything, the bookmark is saved with no reminder. * If you click the Cancel button, no bookmark is saved at all. * All of the reminder type tiles are dynamic and the times they show will be based on your user timezone set in your profile (this should already be set for you). * If for some reason a user does not have their timezone set they will not be able to set a reminder, but they will still be able to create a bookmark. * A bookmark can be deleted by clicking on the book icon again which will be red if the post is bookmarked. This PR does NOT do anything to migrate or change existing bookmarks in the form of `PostActions`, the two features live side-by-side here. Also this does nothing to the topic bookmarking.
2019-12-11 12:04:02 +08:00
end
2014-05-13 20:53:11 +08:00
def wiki
post = find_post_from_params
params.require(:wiki)
guardian.ensure_can_wiki!(post)
post.revise(current_user, wiki: params[:wiki])
2014-05-13 20:53:11 +08:00
render body: nil
2014-05-13 20:53:11 +08:00
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
FIX: Secure upload post processing race condition (#23968) * FIX: Secure upload post processing race condition This commit fixes a couple of issues. A little background -- when uploads are created in the composer for posts, regardless of whether the upload will eventually be marked secure or not, if secure_uploads is enabled we always mark the upload secure at first. This is so the upload is by default protected, regardless of post type (regular or PM) or category. This was causing issues in some rare occasions though because of the order of operations of our post creation and processing pipeline. When creating a post, we enqueue a sidekiq job to post-process the post which does various things including converting images to lightboxes. We were also enqueuing a job to update the secure status for all uploads in that post. Sometimes the secure status job would run before the post process job, marking uploads as _not secure_ in the background and changing their ACL before the post processor ran, which meant the users would see a broken image in their posts. This commit fixes that issue by always running the upload security changes inline _within_ the cooked_post_processor job. The other issue was that the lightbox wrapper link for images in the post would end up with a URL like this: ``` href="/secure-uploads/original/2X/4/4e1f00a40b6c952198bbdacae383ba77932fc542.jpeg" ``` Since we weren't actually using the `upload.url` to pass to `UrlHelper.cook_url` here, we weren't converting this href to the CDN URL if the post was not in a secure context (the UrlHelper does not know how to convert a secure-uploads URL to a CDN one). Now we always end up with the correct lightbox href. This was less of an issue than the other one, since the secure-uploads URL works even when the upload has become non-secure, but it was a good inconsistency to fix anyway.
2023-10-19 07:48:01 +08:00
post.rebake!(invalidate_oneboxes: true, invalidate_broken_images: true)
render body: nil
end
2014-09-23 00:55:13 +08:00
def unhide
post = find_post_from_params
guardian.ensure_can_unhide!(post)
post.unhide!
render body: nil
2014-09-23 00:55:13 +08:00
end
DELETED_POSTS_MAX_LIMIT = 100
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 = fetch_limit_from_params(default: 60, max: DELETED_POSTS_MAX_LIMIT)
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
2013-02-09 06:49:15 +08:00
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
2013-12-12 10:41:34 +08:00
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
2013-12-12 10:41:34 +08:00
guardian.ensure_can_see!(post_revision)
2013-12-12 10:41:34 +08:00
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
2013-12-12 10:41:34 +08:00
def create_params
permitted = %i[
raw
topic_id
archetype
category
target_recipients
reply_to_post_number
auto_track
typing_duration_msecs
composer_open_duration_msecs
visible
draft_key
2013-12-12 10:41:34 +08:00
]
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
2013-12-12 10:41:34 +08:00
# 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]
2013-12-12 10:41:34 +08:00
2016-08-26 08:58:34 +08:00
if is_api?
2013-12-12 10:41:34 +08:00
# 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
2013-12-12 10:41:34 +08:00
end
result =
params
.permit(*permitted)
.tap do |allowed|
allowed[:image_sizes] = params[:image_sizes]
if params.has_key?(:meta_data)
Discourse.deprecate(
"the :meta_data param is deprecated, use the :topic_custom_fields param instead",
since: "3.2",
drop_from: "3.3",
)
end
topic_custom_fields = {}
topic_custom_fields.merge!(editable_topic_custom_fields(:meta_data))
topic_custom_fields.merge!(editable_topic_custom_fields(:topic_custom_fields))
if topic_custom_fields.present?
allowed[:topic_opts] = { custom_fields: topic_custom_fields }
end
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
2015-09-11 04:01:23 +08:00
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"]
recipients = result[:target_recipients]
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
2013-12-12 10:41:34 +08:00
end
def editable_topic_custom_fields(params_key)
if (topic_custom_fields = params[params_key]).present?
editable_topic_custom_fields = Topic.editable_custom_fields(guardian)
if (
unpermitted_topic_custom_fields =
topic_custom_fields.except(*editable_topic_custom_fields)
).present?
raise Discourse::InvalidParameters.new(
"The following keys in :#{params_key} are not permitted: #{unpermitted_topic_custom_fields.keys.join(", ")}",
)
end
topic_custom_fields.permit(*editable_topic_custom_fields).to_h
else
{}
end
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
2014-02-22 00:12:43 +08:00
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
2014-02-22 00:12:43 +08:00
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
2014-02-22 00:12:43 +08:00
guardian.ensure_can_see!(post)
post
end
2013-02-06 03:16:51 +08:00
end