summaryrefslogtreecommitdiff
path: root/app/controllers/concerns/page_limiter.rb
blob: 97df540d31b8eed138aa3654fed2ee30284e0d52 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# frozen_string_literal: true

# Include this in your controller and call `limit_pages` in order
# to configure the limiter.
#
#   Examples:
#     class MyController < ApplicationController
#       include PageLimiter
#
#       before_action only: [:index] do
#         limit_pages(500)
#       end
#
#       # You can override the default response
#       rescue_from PageOutOfBoundsError, with: :page_out_of_bounds
#
#       def page_out_of_bounds(error)
#         # Page limit number is available as error.message
#         head :ok
#       end
#

module PageLimiter
  extend ActiveSupport::Concern

  PageLimiterError          = Class.new(StandardError)
  PageLimitNotANumberError  = Class.new(PageLimiterError)
  PageLimitNotSensibleError = Class.new(PageLimiterError)
  PageOutOfBoundsError      = Class.new(PageLimiterError)

  included do
    rescue_from PageOutOfBoundsError, with: :default_page_out_of_bounds_response
  end

  def limit_pages(max_page_number)
    check_page_number!(max_page_number)
  end

  private

  # If the page exceeds the defined maximum, raise a PageOutOfBoundsError
  # If the page doesn't exceed the limit, it does nothing.
  def check_page_number!(max_page_number)
    raise PageLimitNotANumberError unless max_page_number.is_a?(Integer)
    raise PageLimitNotSensibleError unless max_page_number > 0

    return if params[:page].blank?
    return if params[:page].to_i <= max_page_number

    record_page_limit_interception
    raise PageOutOfBoundsError, max_page_number
  end

  # By default just return a HTTP status code and an empty response
  def default_page_out_of_bounds_response
    head :bad_request
  end

  # Record the page limit being hit in Prometheus
  def record_page_limit_interception
    dd = Gitlab::SafeDeviceDetector.new(request.user_agent)

    Gitlab::Metrics.counter(:gitlab_page_out_of_bounds,
      controller: params[:controller],
      action: params[:action],
      bot: dd.bot?
    ).increment
  end
end