require_dependency 'new_post_manager' require_dependency 'post_creator' require_dependency 'post_destroyer' require_dependency 'distributed_memoizer' require_dependency 'new_post_result_serializer' class PostsController < ApplicationController # Need to be logged in for all actions here before_filter :ensure_logged_in, except: [:show, :replies, :by_number, :short_link, :reply_history, :revisions, :latest_revision, :expand_embed, :markdown_id, :markdown_num, :cooked, :latest] skip_before_filter :check_xhr, only: [:markdown_id, :markdown_num, :short_link] def markdown_id markdown Post.find(params[:id].to_i) end def markdown_num markdown Post.find_by(topic_id: params[:topic_id].to_i, post_number: (params[:post_number] || 1).to_i) end def markdown(post) if post && guardian.can_see?(post) render text: post.raw, content_type: 'text/plain' else raise Discourse::NotFound 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 # last 50 post IDs only, to avoid counting deleted posts in security check posts = Post.order(created_at: :desc) .where('posts.id <= ?', last_post_id) .where('posts.id > ?', last_post_id - 50) .includes(topic: :category) .includes(:user) .limit(50) # 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) } counts = PostAction.counts_for(posts, current_user) render_json_dump(serialize_data(posts, PostSerializer, scope: guardian, root: 'latest_posts', add_raw: true, all_post_actions: counts) ) end def cooked post = find_post_from_params render json: {cooked: post.cooked} end def raw_email post = Post.find(params[:id].to_i) guardian.ensure_can_view_raw_email!(post) render json: {raw_email: post.raw_email} end def short_link post = Post.find(params[:post_id].to_i) # Stuff the user in the request object, because that's what IncomingLink wants if params[:user_id] user = User.find(params[:user_id].to_i) request['u'] = user.username_lower if user end guardian.ensure_can_see!(post) redirect_to post.url end def create @manager_params = create_params manager = NewPostManager.new(current_user, @manager_params) if is_api? memoized_payload = DistributedMemoizer.memoize(signature_for(@manager_params), 120) do result = manager.perform MultiJson.dump(serialize_data(result, NewPostResultSerializer, root: false)) end parsed_payload = JSON.parse(memoized_payload) backwards_compatible_json(parsed_payload, parsed_payload['success']) else result = manager.perform json = serialize_data(result, NewPostResultSerializer, root: false) backwards_compatible_json(json, result.success?) end end def update params.require(:post) post = Post.where(id: params[:id]) post = post.with_deleted if guardian.is_staff? post = post.first post.image_sizes = params[:image_sizes] if params[:image_sizes].present? if too_late_to(:edit, post) return render json: { errors: [I18n.t('too_late_to_edit')] }, status: 422 end guardian.ensure_can_edit!(post) changes = { raw: params[:post][:raw], edit_reason: params[:post][:edit_reason] } # to stay consistent with the create api, we allow for title & category changes here if post.post_number == 1 changes[:title] = params[:title] if params[:title] changes[:category_id] = params[:post][:category_id] if params[:post][:category_id] end revisor = PostRevisor.new(post) if revisor.revise!(current_user, changes) TopicLink.extract_from(post) QuotedPost.extract_from(post) end return render_json_error(post) if post.errors.present? return render_json_error(post.topic) if post.topic.errors.present? post_serializer = PostSerializer.new(post, scope: guardian, root: false) post_serializer.draft_sequence = DraftSequence.current(current_user, post.topic.draft_key) link_counts = TopicLink.counts_for(guardian,post.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 reply_history post = find_post_from_params render_serialized(post.reply_history(params[:max_replies].to_i), PostSerializer) end def destroy post = find_post_from_params if too_late_to(:delete_post, post) render json: {errors: [I18n.t('too_late_to_edit')]}, status: 422 return end guardian.ensure_can_delete!(post) destroyer = PostDestroyer.new(current_user, post, { context: params[:context] }) destroyer.destroy render nothing: true end def expand_embed render json: {cooked: TopicEmbed.expanded_for(find_post_from_params) } rescue render_json_error I18n.t('errors.embed.load_from_remote') end def recover post = find_post_from_params guardian.ensure_can_recover_post!(post) destroyer = PostDestroyer.new(current_user, post) destroyer.recover post.reload render_post_json(post) end def destroy_many params.require(:post_ids) posts = Post.where(id: post_ids_including_replies) 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 {|p| PostDestroyer.new(current_user, p).destroy } end render nothing: true end # Direct replies to this post def replies post = find_post_from_params render_serialized(post.replies, PostSerializer) end def 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_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 nothing: true 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 nothing: true end def bookmark post = find_post_from_params if params[:bookmarked] == "true" PostAction.act(current_user, post, PostActionType.types[:bookmark]) else PostAction.remove_act(current_user, post, PostActionType.types[:bookmark]) end tu = TopicUser.get(post.topic, current_user) render_json_dump(topic_bookmarked: tu.try(:bookmarked)) end def wiki guardian.ensure_can_wiki! post = find_post_from_params post.revise(current_user, { wiki: params[:wiki] }) render nothing: true end def post_type guardian.ensure_can_change_post_type! post = find_post_from_params post.revise(current_user, { post_type: params[:post_type].to_i }) render nothing: true end def rebake guardian.ensure_can_rebake! post = find_post_from_params post.rebake!(invalidate_oneboxes: true) render nothing: true end def unhide post = find_post_from_params guardian.ensure_can_unhide!(post) post.unhide! render nothing: true end def flagged_posts params.permit(:offset, :limit) guardian.ensure_can_see_flagged_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(id: PostAction.where(post_action_type_id: PostActionType.notify_flag_type_ids) .where(disagreed_at: nil) .select(:post_id)) render_serialized(posts, AdminPostSerializer) 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, AdminPostSerializer) end protected # 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, success) json_obj = json_obj[:post] || json_obj['post'] unless params[:nested_post] 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 private def user_posts(guardian, user_id, opts) posts = Post.includes(:user, :topic, :deleted_by, :user_actions) .where(user_id: user_id) .with_deleted .order(created_at: :desc) 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) secured_category_ids = guardian.secure_category_ids topics = Topic.where(id: topic_ids).with_deleted.where.not(archetype: 'private_message') topics = topics.secured(guardian) posts = posts.where(topic_id: topics.pluck(:id)) end posts.offset(opts[:offset]) .limit(opts[:limit]) end def create_params permitted = [ :raw, :topic_id, :archetype, :category, :target_usernames, :reply_to_post_number, :auto_track ] # param munging for WordPress params[:auto_track] = !(params[:auto_track].to_s == "false") if params[:auto_track] if api_key_valid? # php seems to be sending this incorrectly, don't fight with it params[:skip_validations] = params[:skip_validations].to_s == "true" permitted << :skip_validations # We allow `embed_url` via the API permitted << :embed_url end params.require(:raw) result = params.permit(*permitted).tap do |whitelisted| whitelisted[:image_sizes] = params[:image_sizes] # TODO this does not feel right, we should name what meta_data is allowed whitelisted[: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 PostRevisor.tracked_topic_fields.keys.each 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"] result end def signature_for(args) "post##" << Digest::SHA1.hexdigest(args .to_a .concat([["user", current_user.id]]) .sort{|x,y| x[0] <=> y[0]}.join do |x,y| "#{x}:#{y}" end) end def too_late_to(action, post) !guardian.send("can_#{action}?", post) && post.user_id == current_user.id && post.edit_time_limit_expired? 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_using(finder) # Include deleted posts if the user is staff finder = finder.with_deleted if current_user.try(:staff?) post = finder.first raise Discourse::NotFound unless post # load deleted topic post.topic = Topic.with_deleted.find(post.topic_id) if current_user.try(:staff?) guardian.ensure_can_see!(post) post end end