Skip to content
Snippets Groups Projects
Select Git revision
  • 3c55bc47524045e0c732126cb33f34afdb750499
  • eip-develop default
  • 0valt/tour-fixes
  • 0valt/1815/tags-ordering
  • develop
  • 0valt/voting-improvements
  • art/mod-spam-tools
  • 0valt/1459/draft-discard
  • 0valt/1790/preferences
  • 0valt/query-optimizations
  • 0valt/1809/settings-wrap
  • 0valt/1805/sign-in-redirect-fix
  • 0valt/1804/kbd
  • 0valt/1292/flag-modal
  • MoshiKoi/1025/remove-special-case-notifying-author-of-threads
  • 0valt/1783/collection-caching
  • 0valt/notifications-fix
  • 0valt/1040/in-page-follow
  • trichoplax/1567/associate-checkboxes-with-their-labels
  • trichoplax/simplify-post-followed-by-user-method
  • trichoplax/correctly-name-system-test-as-plural
  • 0valt/1787/new-thread-validation
  • v0.12.2
  • v0.12.1
  • v0.12.0
  • v0.11.0
  • v0.10.0
  • v0.9.0
  • v0.8.0
  • v0.7.0
  • v0.6.1
  • v0.6.0
  • v0.5.0
  • v0.4.0
  • v0.3.0
  • v1.0
36 results

codeclimate-config.patch

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    users_controller.rb 13.05 KiB
    require 'net/http'
    class UsersController < ApplicationController
      before_action :authenticate_user!, only: [:edit_profile, :update_profile, :stack_redirect, :transfer_se_content,
                                                :qr_login_code, :me]
      before_action :verify_moderator, only: [:mod, :destroy, :soft_delete, :role_toggle, :full_log,
                                              :annotate, :annotations]
      before_action :set_user, only: [:show, :mod, :destroy, :soft_delete, :posts, :role_toggle, :full_log, :activity,
                                      :annotate, :annotations]
    
      def index
        sort_param = { reputation: :reputation, age: :created_at }[params[:sort]&.to_sym] || :reputation
        @users = if params[:search].present?
                   user_scope.search(params[:search])
                 else
                   user_scope.order(sort_param => :desc)
                 end.paginate(page: params[:page], per_page: 48)
        @post_counts = Post.where(user_id: @users.pluck(:id).uniq).group(:user_id).count
      end
    
      def show
        render layout: 'without_sidebar'
      end
    
      def me
        @user = current_user
        respond_to do |format|
          format.html do
            redirect_to user_path(@user)
          end
          format.json do
            data = [:id, :username, :is_moderator, :is_admin, :is_global_moderator, :is_global_admin, :trust_level,
                    :se_acct_id].map { |a| [a, @user.send(a)] }.to_h
            render json: data
          end
        end
      end
    
      def posts
        @posts = Post.undeleted.where(user: @user).user_sort({ term: params[:sort], default: :score },
                                                             age: :created_at, score: :score)
                     .paginate(page: params[:page], per_page: 25)
        respond_to do |format|
          format.html do
            render :posts
          end
          format.json do
            render json: @posts
          end
        end
      end
    
      def activity
        @posts = Post.undeleted.where(user: @user).count
        @comments = Comment.undeleted.where(user: @user).where(post: Post.undeleted).count
        @suggested_edits = SuggestedEdit.where(user: @user).count
        @edits = PostHistory.where(user: @user).where(post: Post.undeleted).count
    
        @all_edits = @suggested_edits + @edits
    
        items = case params[:filter]
                when 'posts'
                  Post.undeleted.where(user: @user).all
                when 'comments'
                  Comment.undeleted.where(user: @user).where(post: Post.undeleted).all
                when 'edits'
                  SuggestedEdit.where(user: @user).all + PostHistory.where(user: @user).where(post: Post.undeleted)
                else
                  Post.undeleted.where(user: @user).all + \
                  Comment.undeleted.where(user: @user).where(post: Post.undeleted).all + \
                  SuggestedEdit.where(user: @user).all + PostHistory.where(user: @user).where(post: Post.undeleted)
                end
    
        @items = items.sort_by(&:created_at).reverse
        render layout: 'without_sidebar'
      end
    
      def mod; end
    
      def full_log
        @posts = Post.where(user: @user).count
        @comments = Comment.where(user: @user).count
        @flags = Flag.where(user: @user).count
        @suggested_edits = SuggestedEdit.where(user: @user).count
        @edits = PostHistory.where(user: @user).count
        @mod_warnings_received = ModWarning.where(community_user: @user.community_user).count
    
        @all_edits = @suggested_edits + @edits
    
        @interesting_comments = Comment.where(user: @user, deleted: true).count
        @interesting_flags = Flag.where(user: @user, status: 'declined').count
        @interesting_edits = SuggestedEdit.where(user: @user, active: false, accepted: false).count
        @interesting_posts = Post.where(user: @user).where('score < 0.25 OR deleted=1').count
    
        @interesting = @interesting_comments + @interesting_flags + @mod_warnings_received + \
                       @interesting_edits + @interesting_posts
    
        @items = (case params[:filter]
                  when 'posts'
                    Post.where(user: @user).all
                  when 'comments'
                    Comment.where(user: @user).all
                  when 'flags'
                    Flag.where(user: @user).all
                  when 'edits'
                    SuggestedEdit.where(user: @user).all + PostHistory.where(user: @user).all
                  when 'warnings'
                    ModWarning.where(community_user: @user.community_user).all
                  when 'interesting'
                    Comment.where(user: @user, deleted: true).all + Flag.where(user: @user, status: 'declined').all + \
                      SuggestedEdit.where(user: @user, active: false, accepted: false).all + \
                      Post.where(user: @user).where('score < 0.25 OR deleted=1').all
                  else
                    Post.where(user: @user).all + Comment.where(user: @user).all + Flag.where(user: @user).all + \
                      SuggestedEdit.where(user: @user).all + PostHistory.where(user: @user).all + \
                      ModWarning.where(community_user: @user.community_user).all
                  end).sort_by(&:created_at).reverse
    
        render layout: 'without_sidebar'
      end
    
      def destroy
        if @user.votes.count > 100
          render(json: { status: 'failed', message: 'Users with more than 100 votes cannot be destroyed.' }, status: 422)
          return
        end
    
        if @user.is_admin || @user.is_moderator
          render(json: { status: 'failed', message: 'Admins and moderators cannot be destroyed.' }, status: 422)
          return
        end
    
        before = @user.attributes_print
        user_email = @user.email
        user_ip = [@user.last_sign_in_ip]
    
        if @user.current_sign_in_ip
          user_ip << @user.current_sign_in_ip
        end
    
        BlockedItem.new(item_type: 'email', value: user_email, expires: DateTime.now + 180.days,
                        automatic: true, reason: 'user destroyed: #' + @user.id.to_s).save
        user_ip.compact.map do |ip|
          BlockedItem.new(item_type: 'ip', value: ip, expires: 180.days.from_now,
                          automatic: true, reason: 'user destroyed: #' + @user.id.to_s).save
        end
        if @user.destroy!
          AuditLog.moderator_audit(event_type: 'user_destroy', user: current_user, comment: "<<User #{before}>>")
          render json: { status: 'success' }
        else
          render json: { status: 'failed',
                         message: 'Call to <code>@user.destroy!</code> failed; ask a DBA or dev to destroy.' },
                 status: 500
        end
      end
    
      def soft_delete
        if @user.is_admin || @user.is_moderator
          render(json: { status: 'failed', message: 'Admins and moderators cannot be deleted.' }, status: 422)
          return
        end
    
        relations = User.reflections
        transfer_id = SiteSetting['SoftDeleteTransferId']
        relations.select { |_, ref| ref.options[:dependent] == :destroy }.each do |name, ref|
          if ref.macro == :has_many || ref.macro == :has_and_belongs_to_many
            @user.send(name).destroy_all
          else
            @user.send(name).destroy
          end
        end
        relations.reject { |_, ref| ref.options[:dependent] == :destroy }.each do |name, ref|
          if ref.macro == :has_many || ref.macro == :has_and_belongs_to_many
            @user.send(name)&.update_all(ref.foreign_key => transfer_id)
          else
            @user.send(name)&.update(ref.foreign_key => transfer_id)
          end
        end
    
        before = @user.attributes_print
        unless @user.destroy
          render(json: { status: 'failed', message: "Failed to destroy UID #{@user.id}" }, status: 500)
          return
        end
        AuditLog.moderator_audit(event_type: 'user_delete', user: current_user, comment: "<<User #{before}>>")
    
        render json: { status: 'success', message: 'Ask a database administrator to verify the deletion is complete.' }
      end
    
      def edit_profile; end
    
      def update_profile
        profile_params = params.require(:user).permit(:username, :profile_markdown, :website, :twitter)
        profile_params[:twitter] = profile_params[:twitter].delete('@')
    
        if profile_params[:website].present? && URI.parse(profile_params[:website]).instance_of?(URI::Generic)
          # URI::Generic indicates the user didn't include a protocol, so we'll add one now so that it can be
          # parsed correctly in the view later on.
          profile_params[:website] = 'https://' + profile_params[:website]
        end
    
        @user = current_user
        before = @user.attributes_print
    
        if params[:user][:avatar].present?
          if helpers.valid_image?(params[:user][:avatar])
            @user.avatar.attach(params[:user][:avatar])
          else
            @user.errors.add(:avatar, 'must be a valid image')
            flash[:danger] = "Couldn't update your profile."
            render :edit_profile
            return
          end
        end
    
        profile_rendered = helpers.render_markdown(profile_params[:profile_markdown])
        if @user.update(profile_params.merge(profile: profile_rendered))
          flash[:success] = 'Your profile details were updated.'
          AuditLog.user_history(event_type: 'profile_update', related: @user, user: current_user,
                                comment: "from <<User #{before}>>\nto <<User #{@user.attributes_print}>>")
          redirect_to user_path(current_user)
        else
          flash[:danger] = "Couldn't update your profile."
          render :edit_profile
        end
      end
    
      def role_toggle
        role_map = { mod: :is_moderator, admin: :is_admin, mod_global: :is_global_moderator, admin_global: :is_global_admin,
                     staff: :staff }
        unless role_map.keys.include?(params[:role].underscore.to_sym)
          render json: { status: 'error', message: "Role not found: #{params[:role]}" }, status: 400
        end
    
        key = params[:role].underscore.to_sym
        attrib = role_map[key]
        if [:mod, :admin].include? key
          new_value = !@user.community_user.send(attrib)
          @user.community_user.update(attrib => new_value)
        else
          new_value = !@user.send(attrib)
          @user.update(attrib => new_value)
        end
        AuditLog.admin_audit(event_type: 'role_toggle', related: @user, user: current_user,
                             comment: "#{attrib} to #{new_value}")
    
        render json: { status: 'success' }
      end
    
      def stack_redirect
        response = Net::HTTP.post_form(URI('https://stackoverflow.com/oauth/access_token/json'),
                                       'client_id' => SiteSetting['SEApiClientId'],
                                       'client_secret' => SiteSetting['SEApiClientSecret'],
                                       'code' => params[:code], 'redirect_uri' => stack_redirect_url)
        access_token = JSON.parse(response.body)['access_token']
    
        uri = "https://api.stackexchange.com/2.2/me/associated?key=#{SiteSetting['SEApiKey']}" \
              "&access_token=#{access_token}&filter=!-rH86dva"
        accounts = JSON.parse(Net::HTTP.get(URI(uri)))
        network_id = accounts['items'][0]['account_id']
        current_user.update(se_acct_id: network_id)
        redirect_to edit_user_profile_path
      end
    
      def transfer_se_content
        unless params[:agree_to_relicense].present? && params[:agree_to_relicense] == 'true'
          flash[:danger] = 'To claim your content, we need you to agree to relicense your posts to us.'
          redirect_to(edit_user_profile_path) && return
        end
    
        auto_user = User.where(se_acct_id: current_user.se_acct_id).where.not(id: current_user.id).first
        if auto_user.nil?
          flash[:warning] = "There doesn't appear to be any of your content here."
          redirect_to(edit_user_profile_path) && return
        end
    
        community = RequestContext.community
    
        Thread.new do
          RequestContext.community = community
    
          auto_user.posts.each do |post|
            post.reassign_user(current_user)
            post.remove_attribution_notice!
          end
          auto_user.reload.destroy
          current_user.update(transferred_content: true)
        end
        flash[:success] = 'Your content is being transferred to you.'
        redirect_to edit_user_profile_path
      end
    
      def qr_login_code
        @token = SecureRandom.urlsafe_base64(64)
        @qr_code = RQRCode::QRCode.new(qr_login_url(@token))
        current_user.update(login_token: @token, login_token_expires_at: 5.minutes.from_now)
      end
    
      def do_qr_login
        user = User.find_by(login_token: params[:token])
        if user&.login_token_expires_at&.present? && user.login_token_expires_at >= DateTime.now
          flash[:success] = 'You are now signed in.'
          user.update(login_token: nil, login_token_expires_at: nil)
          sign_in user
          AuditLog.user_history(event_type: 'mobile_login', related: user)
          redirect_to root_path
        else
          flash[:danger] = "That login link isn't valid. Codes expire after 5 minutes - if it's been longer than that, " \
                           'get a new code and try again.'
          not_found
        end
      end
    
      def annotations
        @logs = AuditLog.where(log_type: 'user_annotation', related: @user).order(created_at: :desc)
                        .paginate(page: params[:page], per_page: 20)
        render layout: 'without_sidebar'
      end
    
      def annotate
        @log = AuditLog.user_annotation(event_type: 'annotation', user: current_user, related: @user,
                                        comment: params[:comment])
        if @log.errors.none?
          redirect_to user_annotations_path(@user)
        else
          flash[:danger] = 'Failed to save your annotation.'
          render :annotations
        end
      end
    
      private
    
      def set_user
        @user = user_scope.find_by(id: params[:id])
        not_found if @user.nil?
      end
    
      def user_scope
        User.joins(:community_user).includes(:community_user, :avatar_attachment)
      end
    end