File: allow_browser.rb

package info (click to toggle)
rails 2%3A7.2.2.1%2Bdfsg-7
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 43,352 kB
  • sloc: ruby: 349,799; javascript: 30,703; yacc: 46; sql: 43; sh: 29; makefile: 27
file content (123 lines) | stat: -rw-r--r-- 4,631 bytes parent folder | download | duplicates (2)
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# frozen_string_literal: true

# :markup: markdown

module ActionController # :nodoc:
  module AllowBrowser
    extend ActiveSupport::Concern

    module ClassMethods
      # Specify the browser versions that will be allowed to access all actions (or
      # some, as limited by `only:` or `except:`). Only browsers matched in the hash
      # or named set passed to `versions:` will be blocked if they're below the
      # versions specified. This means that all other browsers, as well as agents that
      # aren't reporting a user-agent header, will be allowed access.
      #
      # A browser that's blocked will by default be served the file in
      # public/406-unsupported-browser.html with a HTTP status code of "406 Not
      # Acceptable".
      #
      # In addition to specifically named browser versions, you can also pass
      # `:modern` as the set to restrict support to browsers natively supporting webp
      # images, web push, badges, import maps, CSS nesting, and CSS :has. This
      # includes Safari 17.2+, Chrome 120+, Firefox 121+, Opera 106+.
      #
      # You can use https://caniuse.com to check for browser versions supporting the
      # features you use.
      #
      # You can use `ActiveSupport::Notifications` to subscribe to events of browsers
      # being blocked using the `browser_block.action_controller` event name.
      #
      # Examples:
      #
      #     class ApplicationController < ActionController::Base
      #       # Allow only browsers natively supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has
      #       allow_browser versions: :modern
      #     end
      #
      #     class ApplicationController < ActionController::Base
      #       # All versions of Chrome and Opera will be allowed, but no versions of "internet explorer" (ie). Safari needs to be 16.4+ and Firefox 121+.
      #       allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
      #     end
      #
      #     class MessagesController < ApplicationController
      #       # In addition to the browsers blocked by ApplicationController, also block Opera below 104 and Chrome below 119 for the show action.
      #       allow_browser versions: { opera: 104, chrome: 119 }, only: :show
      #     end
      def allow_browser(versions:, block: -> { render file: Rails.root.join("public/406-unsupported-browser.html"), layout: false, status: :not_acceptable }, **options)
        before_action -> { allow_browser(versions: versions, block: block) }, **options
      end
    end

    private
      def allow_browser(versions:, block:)
        require "useragent"

        if BrowserBlocker.new(request, versions: versions).blocked?
          ActiveSupport::Notifications.instrument("browser_block.action_controller", request: request, versions: versions) do
            instance_exec(&block)
          end
        end
      end

      class BrowserBlocker # :nodoc:
        SETS = {
          modern: { safari: 17.2, chrome: 120, firefox: 121, opera: 106, ie: false }
        }

        attr_reader :request, :versions

        def initialize(request, versions:)
          @request, @versions = request, versions
        end

        def blocked?
          user_agent_version_reported? && unsupported_browser?
        end

        private
          def parsed_user_agent
            @parsed_user_agent ||= UserAgent.parse(request.user_agent)
          end

          def user_agent_version_reported?
            request.user_agent.present? && parsed_user_agent.version.to_s.present?
          end

          def unsupported_browser?
            version_guarded_browser? && version_below_minimum_required? && !bot?
          end

          def version_guarded_browser?
            minimum_browser_version_for_browser != nil
          end

          def bot?
            parsed_user_agent.bot?
          end

          def version_below_minimum_required?
            if minimum_browser_version_for_browser
              parsed_user_agent.version < UserAgent::Version.new(minimum_browser_version_for_browser.to_s)
            else
              true
            end
          end

          def minimum_browser_version_for_browser
            expanded_versions[normalized_browser_name]
          end

          def expanded_versions
            @expanded_versions ||= (SETS[versions] || versions).with_indifferent_access
          end

          def normalized_browser_name
            case name = parsed_user_agent.browser.downcase
            when "internet explorer" then "ie"
            else name
            end
          end
      end
  end
end