# frozen_string_literal: true require 'fileutils' require 'booru/live_feed' # The heart of the app - ImagesController deals with uploading, viewing, editing, and deleting images. class PostsController < ApplicationController include RateLimitable include ImageUpload include SortParams before_action -> { ratelimit 1, 10.seconds, t('booru.errors.upload_flooding', seconds: 10) }, only: [:create], unless: -> { current_user&.staff? } before_action :filter_banned_users, only: [:new, :create] before_action :setup_pagination_and_tags, :set_image_filter before_action :load_post_by_id, only: [:show] skip_authorization_check only: [:index, :scrape_url, :featured] # GET /images # View an index of all the images def index if params[:format] == 'json' && params[:constraint] # Less intensive requirement. @include_deleted = params[:deleted] @posts = index_constraints @post_ids = @posts.map(&:id) else @search = frontpage_images @posts = @search.records(includes: [:tags, :user]) @post_ids = @posts.map(&:id) @interactions = PostQuery.interactions(@post_ids, current_user.id) if current_user end @title = t('posts.index.title') respond_to do |format| format.html format.json { render json: { images: @posts.map(&:as_json), interactions: (@interactions || []) }.to_json } end end # GET /:id # GET /images/:id # View a specific image - this is the page that most users are going to spend the most time on. def show authorize! :read, Post Notification.mark_all_read(@post, current_user) if current_user @dupe_reports = DuplicateReport.includes(:post, :duplicate_of_post).where('post_id = ? OR duplicate_of_post_id = ?', @post.id, @post.id) if @post.hidden_from_users && @post.duplicate_id && !@post.can_see_when_hidden?(current_user) respond_to do |format| format.html { redirect_to short_post_path(@post.duplicate_id), notice: t('posts.show.image_duplicate_redirect') } format.json { render json: @post.as_json } end end @search_query = params[:q] @interactions = PostQuery.interactions(@post.id, current_user.id) if current_user @title = "##{@post.id} - #{@post.tag_list}" respond_to do |format| format.html do @comments = CommentQuery.paginate(post: @post, current_user: current_user, page: '0') end format.json do render json: @post.as_json.merge(interactions: (@interactions || []), spoilered: @current_filter&.image_spoilered_tag_ids(@post).present?) end format.any { head :bad_request } end end # GET /images/new # View the form to upload a new image. def new @title = t('posts.new.title') authorize! :create, Post unless Flipflop.enabled?(:image_uploads) flash[:error] = t('posts.errors.upload_disabled') redirect_back && return end @post = Post.new @post.anonymous = current_user&.anonymous_by_default? || force_anonymous? end def scrape_url if params[:url].present? scraped = Booru::Scrapers.scrape(params[:url]) render(json: scraped) && return if scraped.images.present? || scraped.errors.any? end render json: { errors: [t('posts.errors.scraper_fetch_failed')] }, status: :unprocessable_entity end # POST /images # Actually do the upload of a brand new image. def create authorize! :create, Post unless Flipflop.enabled?(:image_uploads) flash[:error] = t('posts.errors.upload_disabled') redirect_to(root_path) && return end @post = Post.new(post_creation_params.merge(request_attributes)) unless captcha_verified? @post.errors[:base] << t('posts.create.errors.captcha_unverified') render 'new' return end # scraped_url = scraped_image_url do |metadata| # if metadata.errors.any? # @post.errors[:base].concat metadata.errors # else # # Keep manually input source over scraper source # @post.source_url = metadata.source_url if @post.source_url.blank? # if metadata.author_name.present? # artist_tag = "artist:#{metadata.author_name.downcase}" # @post.tag_input << ',' << artist_tag if @post.tag_input.downcase.exclude? artist_tag # end # end # end respond_to do |format| if @post.media.errors.blank? && @post.errors.blank? && @post.save SourceChange.add_to_img(@post, current_user, request, initial: true) Notification.watch(@post, current_user) if current_user&.watch_on_upload Booru::LiveFeed.push_upload(@post) # NewImageLogger.log(@post) inc_user_stat :uploads format.html { redirect_to short_post_path(@post), notice: t('posts.create.image_created') } format.json { render json: @post, status: :created, location: @post } else @post.errors.merge!(@post.media.errors) format.html { render :new } format.json { render json: @post.errors, status: :unprocessable_entity } end end end # DELETE /images/:id # This ACTUALLY destroys the image. Forever. Gone. Nuked. def destroy authorize! :destroy, Post @post = Post.find_by!(id: params[:id], hidden_from_users: true) # Update the image deleter. @post.deleted_by = current_user ImageDestroyer.new(@post).save respond_to do |format| format.html do redirect_to(posts_path, notice: t('posts.hard_destroy.success')) end format.json { head :ok } end end def navigate authorize! :read, Post if params[:do] == 'find' # do=find uses the current search scope; do=find_global defaults to the # index's lack of scope. # We're trying to find an image and direct the user to that page search = Post.fancy_search(default_image_filter_options) do |s| s.add_filter range: { id: { gt: params[:id].to_i } } end count_before = search.results.total_count page_num = if count_before != 0 ((count_before + 1.0) / @per_page).ceil.to_i.to_s else 1 end redirect_to posts_path(page: page_num) elsif %w[prev next].include?(params[:do]) id = params[:id].to_i options = { query: params[:q], sorts: parse_sort } img = if params[:do] == 'prev' PostLoader.new(default_image_filter_options.merge(options)).find_prev(id) else PostLoader.new(default_image_filter_options.merge(options)).find_next(id) end respond_to do |format| format.html do if img redirect_to short_post_path(img.id, scope_key) else redirect_to short_post_path(params[:id], scope_key) end end format.json do if img render json: img.as_json else render json: {} end end end else respond_to do |format| format.html { redirect_to '/' } format.json { render head: :ok } end end end def featured @featured_image = Post.featured respond_to do |format| format.json { render json: @featured_image.as_json } end end private # this method is really stupidly named - it actually uses the constraints provided and paginates based on the page # param and all kinds of other stuff. def index_constraints case params[:constraint] when 'id' constraint = :id when 'updated' constraint = :updated_at when 'created' constraint = :created_at when 'first_seen_at' constraint = :first_seen_at else return [] end begin constraints = {} constraints[:hidden_from_users] = false unless @include_deleted search = Post.where(constraints) search = search.where("#{constraint} > ?", params[:gt]) if params[:gt] search = search.where("#{constraint} >= ?", params[:gte]) if params[:gte] search = search.where("#{constraint} < ?", params[:lt]) if params[:lt] search = search.where("#{constraint} <= ?", params[:lte]) if params[:lte] if params[:order].blank? || params[:order] == 'a' search = search.order(constraint => :asc) elsif params[:order] == 'd' search = search.order(constraint => :desc) else return [] end search.includes(:user, :tags).limit(@per_page).page(@page) rescue StandardError [] end end def load_post_by_id @post = Post.find(params[:id]) # process images as we view them if the viewer is not a bot and we're not already currently processing it (race condition) # if !is_probably_bot? && @post.media_type == 'image' && !@post.processed && !$redis.sismember('processing_images', @post.id.to_s) # ProcessImageJob.perform_later(@post.id) # end end def load_non_destroyed_image @post = Post.find_by!(id: params[:id], destroyed_content: false) end def post_params params.require(:post).permit(:source_url, :tag_input, :description, :anonymous, :file, :file_cache, :file_type_cache, :paste_input) .merge(params.permit(:scraper_url, :scraper_cache, :file_type_cache)).tap do |p| p.merge!(anonymous: true) if force_anonymous? end end end