otwcode/otwarchive

View on GitHub
app/controllers/comments_controller.rb

Summary

Maintainability
F
3 days
Test Coverage
class CommentsController < ApplicationController
  skip_before_action :store_location, except: [:show, :index, :new]
  before_action :load_commentable,
                only: [:index, :new, :create, :edit, :update, :show_comments,
                       :hide_comments, :add_comment_reply,
                       :cancel_comment_reply, :delete_comment,
                       :cancel_comment_delete, :unreviewed, :review_all]
  before_action :check_user_status, only: [:new, :create, :edit, :update, :destroy]
  before_action :load_comment, only: [:show, :edit, :update, :delete_comment, :destroy, :cancel_comment_edit, :cancel_comment_delete, :review, :approve, :reject, :freeze, :unfreeze, :hide, :unhide]
  before_action :check_visibility, only: [:show]
  before_action :check_if_restricted
  before_action :check_tag_wrangler_access
  before_action :check_parent
  before_action :check_modify_parent,
                only: [:new, :create, :edit, :update, :add_comment_reply,
                       :cancel_comment_reply, :cancel_comment_edit]
  before_action :check_pseud_ownership, only: [:create, :update]
  before_action :check_ownership, only: [:edit, :update, :cancel_comment_edit]
  before_action :check_permission_to_edit, only: [:edit, :update ]
  before_action :check_permission_to_delete, only: [:delete_comment, :destroy]
  before_action :check_guest_comment_admin_setting, only: [:new, :create, :add_comment_reply]
  before_action :check_parent_comment_permissions, only: [:new, :create, :add_comment_reply]
  before_action :check_unreviewed, only: [:add_comment_reply]
  before_action :check_frozen, only: [:new, :create, :add_comment_reply]
  before_action :check_hidden_by_admin, only: [:new, :create, :add_comment_reply]
  before_action :check_not_replying_to_spam, only: [:new, :create, :add_comment_reply]
  before_action :check_guest_replies_preference, only: [:new, :create, :add_comment_reply]
  before_action :check_permission_to_review, only: [:unreviewed]
  before_action :check_permission_to_access_single_unreviewed, only: [:show]
  before_action :check_permission_to_moderate, only: [:approve, :reject]
  before_action :check_permission_to_modify_frozen_status, only: [:freeze, :unfreeze]
  before_action :check_permission_to_modify_hidden_status, only: [:hide, :unhide]
  before_action :admin_logout_required, only: [:new, :create, :add_comment_reply]

  include BlockHelper

  before_action :check_blocked, only: [:new, :create, :add_comment_reply, :edit, :update]
  def check_blocked
    parent = find_parent

    if blocked_by?(parent)
      flash[:comment_error] = t("comments.check_blocked.parent")
      redirect_to_all_comments(parent, show_comments: true)
    elsif @comment && blocked_by_comment?(@comment.commentable)
      # edit and update set @comment to the comment being edited
      flash[:comment_error] = t("comments.check_blocked.reply")
      redirect_to_all_comments(parent, show_comments: true)
    elsif @comment.nil? && blocked_by_comment?(@commentable)
      # new, create, and add_comment_reply don't set @comment, but do set @commentable
      flash[:comment_error] = t("comments.check_blocked.reply")
      redirect_to_all_comments(parent, show_comments: true)
    end
  end

  def check_pseud_ownership
    return unless params[:comment][:pseud_id]
    pseud = Pseud.find(params[:comment][:pseud_id])
    return if pseud && current_user && current_user.pseuds.include?(pseud)
    flash[:error] = ts("You can't comment with that pseud.")
    redirect_to root_path
  end

  def load_comment
    @comment = Comment.find(params[:id])
    @check_ownership_of = @comment
    @check_visibility_of = @comment
  end

  def check_parent
    parent = find_parent
    # Only admins and the owner can see comments on something hidden by an admin.
    if parent.respond_to?(:hidden_by_admin) && parent.hidden_by_admin
      logged_in_as_admin? || current_user_owns?(parent) || access_denied(redirect: root_path)
    end
    # Only admins and the owner can see comments on unrevealed works.
    if parent.respond_to?(:in_unrevealed_collection) && parent.in_unrevealed_collection
      logged_in_as_admin? || current_user_owns?(parent) || access_denied(redirect: root_path)
    end
  end

  def check_modify_parent
    parent = find_parent
    # No one can create or update comments on something hidden by an admin.
    if parent.respond_to?(:hidden_by_admin) && parent.hidden_by_admin
      flash[:error] = ts("Sorry, you can't add or edit comments on a hidden work.")
      redirect_to work_path(parent)
    end
    # No one can create or update comments on unrevealed works.
    if parent.respond_to?(:in_unrevealed_collection) && parent.in_unrevealed_collection
      flash[:error] = ts("Sorry, you can't add or edit comments on an unrevealed work.")
      redirect_to work_path(parent)
    end
  end

  def find_parent
    if @comment.present?
      @comment.ultimate_parent
    elsif @commentable.is_a?(Comment)
      @commentable.ultimate_parent
    elsif @commentable.present? && @commentable.respond_to?(:work)
      @commentable.work
    else
      @commentable
    end
  end

  # Check to see if the ultimate_parent is a Work, and if so, if it's restricted
  def check_if_restricted
    parent = find_parent

    return unless parent.respond_to?(:restricted) && parent.restricted? && !(logged_in? || logged_in_as_admin?)
    redirect_to new_user_session_path(restricted_commenting: true)
  end

  # Check to see if the ultimate_parent is a Work or AdminPost, and if so, if it allows
  # comments for the current user.
  def check_parent_comment_permissions
    parent = find_parent
    if parent.is_a?(Work)
      translation_key = "work"
    elsif parent.is_a?(AdminPost)
      translation_key = "admin_post"
    else
      return
    end

    if parent.disable_all_comments?
      flash[:error] = t("comments.commentable.permissions.#{translation_key}.disable_all")
      redirect_to parent
    elsif parent.disable_anon_comments? && !logged_in?
      flash[:error] = t("comments.commentable.permissions.#{translation_key}.disable_anon")
      redirect_to parent
    end
  end

  def check_guest_comment_admin_setting
    admin_settings = AdminSetting.current

    return unless admin_settings.guest_comments_off? && guest?
    
    flash[:error] = t("comments.commentable.guest_comments_disabled")
    redirect_back(fallback_location: root_path)
  end

  def check_guest_replies_preference
    return unless guest? && @commentable.respond_to?(:guest_replies_disallowed?) && @commentable.guest_replies_disallowed?

    flash[:error] = t("comments.check_guest_replies_preference.error")
    redirect_back(fallback_location: root_path)
  end

  def check_unreviewed
    return unless @commentable.respond_to?(:unreviewed?) && @commentable.unreviewed?

    flash[:error] = ts("Sorry, you cannot reply to an unapproved comment.")
    redirect_to logged_in? ? root_path : new_user_session_path
  end

  def check_frozen
    return unless @commentable.respond_to?(:iced?) && @commentable.iced?

    flash[:error] = t("comments.check_frozen.error")
    redirect_back(fallback_location: root_path)
  end

  def check_hidden_by_admin
    return unless @commentable.respond_to?(:hidden_by_admin?) && @commentable.hidden_by_admin?

    flash[:error] = t("comments.check_hidden_by_admin.error")
    redirect_back(fallback_location: root_path)
  end

  def check_not_replying_to_spam
    return unless @commentable.respond_to?(:approved?) && !@commentable.approved?

    flash[:error] = t("comments.check_not_replying_to_spam.error")
    redirect_back(fallback_location: root_path)
  end

  def check_permission_to_review
    parent = find_parent
    return if logged_in_as_admin? || current_user_owns?(parent)
    flash[:error] = ts("Sorry, you don't have permission to see those unreviewed comments.")
    redirect_to logged_in? ? root_path : new_user_session_path
  end

  def check_permission_to_access_single_unreviewed
    return unless @comment.unreviewed?
    parent = find_parent
    return if logged_in_as_admin? || current_user_owns?(parent) || current_user_owns?(@comment)
    flash[:error] = ts("Sorry, that comment is currently in moderation.")
    redirect_to logged_in? ? root_path : new_user_session_path
  end

  def check_permission_to_moderate
    parent = find_parent
    unless logged_in_as_admin? || current_user_owns?(parent)
      flash[:error] = ts("Sorry, you don't have permission to moderate that comment.")
      redirect_to(logged_in? ? root_path : new_user_session_path)
    end
  end

  def check_tag_wrangler_access
    if @commentable.is_a?(Tag) || (@comment&.parent&.is_a?(Tag))
      logged_in_as_admin? || permit?("tag_wrangler") || access_denied
    end
  end

  # Must be able to delete other people's comments on owned works, not just owned comments!
  def check_permission_to_delete
    access_denied(redirect: @comment) unless logged_in_as_admin? || current_user_owns?(@comment) || current_user_owns?(@comment.ultimate_parent)
  end

  # Comments cannot be edited after they've been replied to or if they are frozen.
  def check_permission_to_edit
    if @comment&.iced?
      flash[:error] = t("comments.check_permission_to_edit.error.frozen")
      redirect_back(fallback_location: root_path)
    elsif !@comment&.count_all_comments&.zero?
      flash[:error] = ts("Comments with replies cannot be edited")
      redirect_back(fallback_location: root_path)
    end
  end

  # Comments on works can be frozen or unfrozen by admins with proper
  # authorization or the work creator.
  # Comments on tags can be frozen or unfrozen by admins with proper
  # authorization.
  # Comments on admin posts can be frozen or unfrozen by any admin.
  def check_permission_to_modify_frozen_status
    return if permission_to_modify_frozen_status

    # i18n-tasks-use t('comments.freeze.permission_denied')
    # i18n-tasks-use t('comments.unfreeze.permission_denied')
    flash[:error] = t("comments.#{action_name}.permission_denied")
    redirect_back(fallback_location: root_path)
  end

  def check_permission_to_modify_hidden_status
    return if policy(@comment).can_hide_comment?

    # i18n-tasks-use t('comments.hide.permission_denied')
    # i18n-tasks-use t('comments.unhide.permission_denied')
    flash[:error] = t("comments.#{action_name}.permission_denied")
    redirect_back(fallback_location: root_path)
  end

  # Get the thing the user is trying to comment on
  def load_commentable
    @thread_view = false
    if params[:comment_id]
      @thread_view = true
      if params[:id]
        @commentable = Comment.find(params[:id])
        @thread_root = Comment.find(params[:comment_id])
      else
        @commentable = Comment.find(params[:comment_id])
        @thread_root = @commentable
      end
    elsif params[:chapter_id]
      @commentable = Chapter.find(params[:chapter_id])
    elsif params[:work_id]
      @commentable = Work.find(params[:work_id])
    elsif params[:admin_post_id]
      @commentable = AdminPost.find(params[:admin_post_id])
    elsif params[:tag_id]
      @commentable = Tag.find_by_name(params[:tag_id])
      @page_subtitle = @commentable.try(:name)
    end
  end

  def index
    return raise_not_found if @commentable.blank?

    return unless @commentable.class == Comment

    # we link to the parent object at the top
    @commentable = @commentable.ultimate_parent
  end

  def unreviewed
    @comments = @commentable.find_all_comments
      .unreviewed_only
      .for_display
      .page(params[:page])
  end

  # GET /comments/1
  # GET /comments/1.xml
  def show
    @comments = CommentDecorator.wrap_comments([@comment])
    @thread_view = true
    @thread_root = @comment
    params[:comment_id] = params[:id]
  end

  # GET /comments/new
  def new
    if @commentable.nil?
      flash[:error] = ts("What did you want to comment on?")
      redirect_back_or_default(root_path)
    else
      @comment = Comment.new
      @controller_name = params[:controller_name] if params[:controller_name]
      @name =
        case @commentable.class.name
        when /Work/
          @commentable.title
        when /Chapter/
          @commentable.work.title
        when /Tag/
          @commentable.name
        when /AdminPost/
          @commentable.title
        when /Comment/
          ts("Previous Comment")
        else
          @commentable.class.name
        end
    end
  end

  # GET /comments/1/edit
  def edit
    respond_to do |format|
      format.html
      format.js
    end
  end

  # POST /comments
  # POST /comments.xml
  def create
    if @commentable.nil?
      flash[:error] = ts("What did you want to comment on?")
      redirect_back_or_default(root_path)
    else
      @comment = Comment.new(comment_params)
      @comment.ip_address = request.remote_ip
      @comment.user_agent = request.env["HTTP_USER_AGENT"]
      @comment.commentable = Comment.commentable_object(@commentable)
      @controller_name = params[:controller_name]

      # First, try saving the comment
      if @comment.save
        flash[:comment_notice] = if @comment.unreviewed?
                                   # i18n-tasks-use t("comments.create.success.moderated.admin_post")
                                   # i18n-tasks-use t("comments.create.success.moderated.work")
                                   t("comments.create.success.moderated.#{@comment.ultimate_parent.model_name.i18n_key}")
                                 else
                                   t("comments.create.success.not_moderated")
                                 end
        respond_to do |format|
          format.html do
            if request.referer&.match(/inbox/)
              redirect_to user_inbox_path(current_user, filters: filter_params[:filters], page: params[:page])
            elsif request.referer&.match(/new/) || (@comment.unreviewed? && current_user)
              # If the referer is the new comment page, go to the comment's page
              # instead of reloading the full work.
              # If the comment is unreviewed and commenter is logged in, take
              # them to the comment's page so they can access the edit and
              # delete options for the comment, since unreviewed comments don't
              # appear on the commentable.
              redirect_to comment_path(@comment)
            elsif request.referer == root_url
              # replying on the homepage
              redirect_to root_path
            elsif @comment.unreviewed?
              redirect_to_all_comments(@commentable)
            else
              redirect_to_comment(@comment, { view_full_work: (params[:view_full_work] == "true"), page: params[:page] })
            end
          end
        end
      else
        flash[:error] = ts("Couldn't save comment!")
        render action: "new"
      end
    end
  end

  # PUT /comments/1
  # PUT /comments/1.xml
  def update
    updated_comment_params = comment_params.merge(edited_at: Time.current)
    if @comment.update(updated_comment_params)
      flash[:comment_notice] = ts('Comment was successfully updated.')
      respond_to do |format|
        format.html do
          redirect_to comment_path(@comment) and return if @comment.unreviewed?
          redirect_to_comment(@comment)
        end
        format.js # updating the comment in place
      end
    else
      render action: "edit"
    end
  end

  # DELETE /comments/1
  # DELETE /comments/1.xml
  def destroy
    authorize @comment if logged_in_as_admin?

    parent = @comment.ultimate_parent
    parent_comment = @comment.reply_comment? ? @comment.commentable : nil
    unreviewed = @comment.unreviewed?

    if !@comment.destroy_or_mark_deleted
      # something went wrong?
      flash[:comment_error] = ts("We couldn't delete that comment.")
      redirect_to_comment(@comment)
    elsif unreviewed
      # go back to the rest of the unreviewed comments
      flash[:notice] = ts("Comment deleted.")
      redirect_back(fallback_location: unreviewed_work_comments_path(@comment.commentable))
    elsif parent_comment
      flash[:comment_notice] = ts("Comment deleted.")
      redirect_to_comment(parent_comment)
    else
      flash[:comment_notice] = ts("Comment deleted.")
      redirect_to_all_comments(parent, {show_comments: true})
    end
  end

  def review
    if logged_in_as_admin?
      authorize @comment
    else
      return unless current_user_owns?(@comment.ultimate_parent)
    end

    return unless @comment&.unreviewed?

    @comment.toggle!(:unreviewed)
    # mark associated inbox comments as read
    InboxComment.where(user_id: current_user.id, feedback_comment_id: @comment.id).update_all(read: true) unless logged_in_as_admin?
    flash[:notice] = ts("Comment approved.")
    respond_to do |format|
      format.html do
        if params[:approved_from] == "inbox"
          redirect_to user_inbox_path(current_user, page: params[:page], filters: filter_params[:filters])
        elsif params[:approved_from] == "home"
          redirect_to root_path
        elsif @comment.ultimate_parent.is_a?(AdminPost)
          redirect_to unreviewed_admin_post_comments_path(@comment.ultimate_parent)
        else
          redirect_to unreviewed_work_comments_path(@comment.ultimate_parent)
        end
        return
      end
      format.js
    end
  end

  def review_all
    authorize @commentable, policy_class: CommentPolicy if logged_in_as_admin?
    unless (@commentable && current_user_owns?(@commentable)) || (@commentable && logged_in_as_admin? && @commentable.is_a?(AdminPost))
      flash[:error] = ts("What did you want to review comments on?")
      redirect_back_or_default(root_path)
      return
    end

    @comments = @commentable.find_all_comments.unreviewed_only
    @comments.each { |c| c.toggle!(:unreviewed) }
    flash[:notice] = ts("All moderated comments approved.")
    redirect_to @commentable
  end

  def approve
    authorize @comment
    @comment.mark_as_ham!
    redirect_to_all_comments(@comment.ultimate_parent, show_comments: true)
  end

  def reject
    authorize @comment if logged_in_as_admin?
    @comment.mark_as_spam!
    redirect_to_all_comments(@comment.ultimate_parent, show_comments: true)
  end

  # PUT /comments/1/freeze
  def freeze
    # TODO: When AO3-5939 is fixed, we can use
    # @comment.full_set.each(&:mark_frozen!)
    if !@comment.iced? && @comment.save
      @comment.set_to_freeze_or_unfreeze.each(&:mark_frozen!)
      flash[:comment_notice] = t(".success")
    else
      flash[:comment_error] = t(".error")
    end
    redirect_to_all_comments(@comment.ultimate_parent, show_comments: true)
  end

  # PUT /comments/1/unfreeze
  def unfreeze
    # TODO: When AO3-5939 is fixed, we can use
    # @comment.full_set.each(&:mark_unfrozen!)
    if @comment.iced? && @comment.save
      @comment.set_to_freeze_or_unfreeze.each(&:mark_unfrozen!)
      flash[:comment_notice] = t(".success")
    else
      flash[:comment_error] = t(".error")
    end
    redirect_to_all_comments(@comment.ultimate_parent, show_comments: true)
  end

  # PUT /comments/1/hide
  def hide
    if !@comment.hidden_by_admin?
      @comment.mark_hidden!
      AdminActivity.log_action(current_admin, @comment, action: "hide comment")
      flash[:comment_notice] = t(".success")
    else
      flash[:comment_error] = t(".error")
    end
    redirect_to_all_comments(@comment.ultimate_parent, show_comments: true)
  end

  # PUT /comments/1/unhide
  def unhide
    if @comment.hidden_by_admin?
      @comment.mark_unhidden!
      AdminActivity.log_action(current_admin, @comment, action: "unhide comment")
      flash[:comment_notice] = t(".success")
    else
      flash[:comment_error] = t(".error")
    end
    redirect_to_all_comments(@comment.ultimate_parent, show_comments: true)
  end

  def show_comments
    respond_to do |format|
      format.html do
        # if non-ajax it could mean sudden javascript failure OR being redirected from login
        # so we're being extra-nice and preserving any intention to comment along with the show comments option
        options = {show_comments: true}
        options[:add_comment_reply_id] = params[:add_comment_reply_id] if params[:add_comment_reply_id]
        options[:view_full_work] = params[:view_full_work] if params[:view_full_work]
        options[:page] = params[:page]
        redirect_to_all_comments(@commentable, options)
      end

      format.js do
        @comments = CommentDecorator.for_commentable(@commentable, page: params[:page])
      end
    end
  end

  def hide_comments
    respond_to do |format|
      format.html do
        redirect_to_all_comments(@commentable)
      end
      format.js
    end
  end

  # If JavaScript is enabled, use add_comment_reply.js to load the reply form
  # Otherwise, redirect to a comment view with the form already loaded
  def add_comment_reply
    @comment = Comment.new
    respond_to do |format|
      format.html do
        options = {show_comments: true}
        options[:controller] = @commentable.class.to_s.underscore.pluralize
        options[:anchor] = "comment_#{params[:id]}"
        options[:page] = params[:page]
        options[:view_full_work] = params[:view_full_work]
        if @thread_view
          options[:id] = @thread_root
          options[:add_comment_reply_id] = params[:id]
          redirect_to_comment(@commentable, options)
        else
          options[:id] = @commentable.id # work, chapter or other stuff that is not a comment
          options[:add_comment_reply_id] = params[:id]
          redirect_to_all_comments(@commentable, options)
        end
      end
      format.js { @commentable = Comment.find(params[:id]) }
    end
  end

  def cancel_comment_reply
    respond_to do |format|
      format.html do
        options = {}
        options[:show_comments] = params[:show_comments] if params[:show_comments]
        redirect_to_all_comments(@commentable, options)
      end
      format.js { @commentable = Comment.find(params[:id]) }
    end
  end

  def cancel_comment_edit
    respond_to do |format|
      format.html { redirect_to_comment(@comment) }
      format.js
    end
  end

  def delete_comment
    respond_to do |format|
      format.html do
        options = {}
        options[:show_comments] = params[:show_comments] if params[:show_comments]
        options[:delete_comment_id] = params[:id] if params[:id]
        redirect_to_comment(@comment, options) # TO DO: deleting without javascript doesn't work and it never has!
      end
      format.js
    end
  end

  def cancel_comment_delete
    respond_to do |format|
      format.html do
        options = {}
        options[:show_comments] = params[:show_comments] if params[:show_comments]
        redirect_to_comment(@comment, options)
      end
      format.js
    end
  end

  protected

  # redirect to a particular comment in a thread, going into the thread
  # if necessary to display it
  def redirect_to_comment(comment, options = {})
    if comment.depth > ArchiveConfig.COMMENT_THREAD_MAX_DEPTH
      if comment.ultimate_parent.is_a?(Tag)
        default_options = {
           controller: :comments,
           action: :show,
           id: comment.commentable.id,
           tag_id: comment.ultimate_parent.to_param,
           anchor: "comment_#{comment.id}"
        }
      else
        default_options = {
           controller: comment.commentable.class.to_s.underscore.pluralize,
           action: :show,
           id: (comment.commentable.is_a?(Tag) ? comment.commentable.to_param : comment.commentable.id),
           anchor: "comment_#{comment.id}"
        }
      end
      # display the comment's direct parent (and its associated thread)
      redirect_to(url_for(default_options.merge(options)))
    else
      # need to redirect to the specific chapter; redirect_to_all will then retrieve full work view if applicable
      redirect_to_all_comments(comment.parent, options.merge({show_comments: true, anchor: "comment_#{comment.id}"}))
    end
  end

  def redirect_to_all_comments(commentable, options = {})
    default_options = {anchor: "comments"}
    options = default_options.merge(options)

    if commentable.is_a?(Tag)
      redirect_to comments_path(tag_id: commentable.to_param,
                  add_comment_reply_id: options[:add_comment_reply_id],
                  delete_comment_id: options[:delete_comment_id],
                  page: options[:page],
                  anchor: options[:anchor])
    else
      if commentable.is_a?(Chapter) && (options[:view_full_work] || current_user.try(:preference).try(:view_full_works))
        commentable = commentable.work
      end
      redirect_to controller: commentable.class.to_s.underscore.pluralize,
                  action: :show,
                  id: commentable.id,
                  show_comments: options[:show_comments],
                  add_comment_reply_id: options[:add_comment_reply_id],
                  delete_comment_id: options[:delete_comment_id],
                  view_full_work: options[:view_full_work],
                  anchor: options[:anchor],
                  page: options[:page]
    end
  end

  def permission_to_modify_frozen_status
    parent = find_parent
    return true if policy(@comment).can_freeze_comment?
    return true if parent.is_a?(Work) && current_user_owns?(parent)

    false
  end

  private

  def comment_params
    params.require(:comment).permit(
      :pseud_id, :comment_content, :name, :email, :edited_at
    )
  end

  def filter_params
    params.permit!
  end
end