Add allow_browser to set minimum versions for your application (#50505)
* Add allow_browser to set minimum versions for your application
This commit is contained in:
parent
813afbdd74
commit
e3da4fc53d
1
Gemfile
1
Gemfile
@ -97,6 +97,7 @@ else
|
||||
end
|
||||
|
||||
gem "kredis", ">= 1.7.0", require: false
|
||||
gem "useragent", require: false
|
||||
|
||||
# Active Job
|
||||
group :job do
|
||||
|
@ -56,6 +56,7 @@ PATH
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (7.2.0.alpha)
|
||||
actionpack (= 7.2.0.alpha)
|
||||
activerecord (= 7.2.0.alpha)
|
||||
@ -191,8 +192,6 @@ GEM
|
||||
railties (>= 6.0.0)
|
||||
date (3.3.3)
|
||||
debug (1.7.1)
|
||||
irb (>= 1.5.0)
|
||||
reline (>= 0.3.1)
|
||||
declarative (0.0.20)
|
||||
delayed_job (4.1.11)
|
||||
activesupport (>= 3.0, < 8.0)
|
||||
@ -550,6 +549,7 @@ GEM
|
||||
concurrent-ruby (~> 1.0)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.5.0)
|
||||
useragent (0.16.10)
|
||||
w3c_validators (1.3.7)
|
||||
json (>= 1.8)
|
||||
nokogiri (~> 1.6)
|
||||
@ -654,6 +654,7 @@ DEPENDENCIES
|
||||
trilogy (>= 2.5.0)
|
||||
turbo-rails
|
||||
tzinfo-data
|
||||
useragent
|
||||
w3c_validators (~> 1.3.6)
|
||||
wdm (>= 0.1.0)
|
||||
web-console
|
||||
|
@ -42,6 +42,7 @@
|
||||
s.add_dependency "rack-test", ">= 0.6.3"
|
||||
s.add_dependency "rails-html-sanitizer", "~> 1.6"
|
||||
s.add_dependency "rails-dom-testing", "~> 2.2"
|
||||
s.add_dependency "useragent", "~> 0.16"
|
||||
s.add_dependency "actionview", version
|
||||
|
||||
s.add_development_dependency "activemodel", version
|
||||
|
@ -27,6 +27,7 @@ module ActionController
|
||||
end
|
||||
|
||||
autoload_under "metal" do
|
||||
autoload :AllowBrowser
|
||||
autoload :ConditionalGet
|
||||
autoload :ContentSecurityPolicy
|
||||
autoload :Cookies
|
||||
|
@ -215,6 +215,7 @@ def self.without_modules(*modules)
|
||||
ContentSecurityPolicy,
|
||||
PermissionsPolicy,
|
||||
RateLimiting,
|
||||
AllowBrowser,
|
||||
Streaming,
|
||||
DataStreaming,
|
||||
HttpAuthentication::Basic::ControllerMethods,
|
||||
|
111
actionpack/lib/action_controller/metal/allow_browser.rb
Normal file
111
actionpack/lib/action_controller/metal/allow_browser.rb
Normal file
@ -0,0 +1,111 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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 <tt>only:</tt> or <tt>except:</tt>).
|
||||
# Only browsers matched in the hash or named set passed to <tt>versions:</tt> 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/426.html with a HTTP status code of "426 Upgrade Required".
|
||||
#
|
||||
# In addition to specifically named browser versions, you can also pass <tt>:modern</tt> 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 119+, Firefox 121+, Opera 104+.
|
||||
#
|
||||
# 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 + :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/426.html"), layout: false, status: :upgrade_required }, **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
|
||||
SETS = {
|
||||
modern: { safari: 17.2, chrome: 119, firefox: 121, opera: 104, 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?
|
||||
end
|
||||
|
||||
def version_guarded_browser?
|
||||
minimum_browser_version_for_browser != nil
|
||||
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
|
67
actionpack/test/controller/allow_browser_test.rb
Normal file
67
actionpack/test/controller/allow_browser_test.rb
Normal file
@ -0,0 +1,67 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "abstract_unit"
|
||||
|
||||
class AllowBrowserController < ActionController::Base
|
||||
allow_browser versions: { safari: "16.4", chrome: "119", firefox: "123", opera: "104", ie: false }, block: -> { head :upgrade_required }, only: :hello
|
||||
def hello
|
||||
head :ok
|
||||
end
|
||||
|
||||
allow_browser versions: :modern, block: -> { head :upgrade_required }, only: :modern
|
||||
def modern
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
|
||||
class AllowBrowserTest < ActionController::TestCase
|
||||
tests AllowBrowserController
|
||||
|
||||
CHROME_118 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118 Safari/537.36"
|
||||
CHROME_120 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36"
|
||||
SAFARI_17_2_0 = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.0 Safari/605.1.15"
|
||||
FIREFOX_114 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0"
|
||||
IE_11 = "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"
|
||||
OPERA_104 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 OPR/104.0.4638.54"
|
||||
|
||||
test "blocked browser below version limit" do
|
||||
get_with_agent :hello, FIREFOX_114
|
||||
assert_response :upgrade_required
|
||||
end
|
||||
|
||||
test "blocked browser by name" do
|
||||
get_with_agent :hello, IE_11
|
||||
assert_response :upgrade_required
|
||||
end
|
||||
|
||||
test "allowed browsers above specific version limit" do
|
||||
get_with_agent :hello, SAFARI_17_2_0
|
||||
assert_response :ok
|
||||
|
||||
get_with_agent :hello, CHROME_120
|
||||
assert_response :ok
|
||||
|
||||
get_with_agent :hello, OPERA_104
|
||||
assert_response :ok
|
||||
end
|
||||
|
||||
test "browsers against modern limit" do
|
||||
get_with_agent :modern, SAFARI_17_2_0
|
||||
assert_response :ok
|
||||
|
||||
get_with_agent :modern, CHROME_118
|
||||
assert_response :upgrade_required
|
||||
|
||||
get_with_agent :modern, CHROME_120
|
||||
assert_response :ok
|
||||
|
||||
get_with_agent :modern, OPERA_104
|
||||
assert_response :ok
|
||||
end
|
||||
|
||||
private
|
||||
def get_with_agent(action, agent)
|
||||
@request.headers["User-Agent"] = agent
|
||||
get action
|
||||
end
|
||||
end
|
@ -475,6 +475,7 @@ def delete_public_files_if_api_option
|
||||
if options[:api]
|
||||
remove_file "public/404.html"
|
||||
remove_file "public/422.html"
|
||||
remove_file "public/426.html"
|
||||
remove_file "public/500.html"
|
||||
remove_file "public/apple-touch-icon-precomposed.png"
|
||||
remove_file "public/apple-touch-icon.png"
|
||||
|
@ -1,2 +1,6 @@
|
||||
class ApplicationController < ActionController::<%= options.api? ? "API" : "Base" %>
|
||||
<%- unless options.api? -%>
|
||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||
allow_browser versions: :modern
|
||||
<% end -%>
|
||||
end
|
||||
|
@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Your browser is not supported (426)</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<style>
|
||||
.rails-default-error-page {
|
||||
background-color: #EFEFEF;
|
||||
color: #2E2F30;
|
||||
text-align: center;
|
||||
font-family: arial, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rails-default-error-page div.dialog {
|
||||
width: 95%;
|
||||
max-width: 33em;
|
||||
margin: 4em auto 0;
|
||||
}
|
||||
|
||||
.rails-default-error-page div.dialog > div {
|
||||
border: 1px solid #CCC;
|
||||
border-right-color: #999;
|
||||
border-left-color: #999;
|
||||
border-bottom-color: #BBB;
|
||||
border-top: #B00100 solid 4px;
|
||||
border-top-left-radius: 9px;
|
||||
border-top-right-radius: 9px;
|
||||
background-color: white;
|
||||
padding: 7px 12% 0;
|
||||
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
||||
}
|
||||
|
||||
.rails-default-error-page h1 {
|
||||
font-size: 100%;
|
||||
color: #730E15;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.rails-default-error-page div.dialog > p {
|
||||
margin: 0 0 1em;
|
||||
padding: 1em;
|
||||
background-color: #F7F7F7;
|
||||
border: 1px solid #CCC;
|
||||
border-right-color: #999;
|
||||
border-left-color: #999;
|
||||
border-bottom-color: #999;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-top-color: #DADADA;
|
||||
color: #666;
|
||||
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="rails-default-error-page">
|
||||
<!-- This file lives in public/426.html -->
|
||||
<div class="dialog">
|
||||
<div>
|
||||
<h1>Your browser is not supported.</h1>
|
||||
<p>Please upgrade your browser to continue.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -182,7 +182,7 @@ class Hello
|
||||
end
|
||||
|
||||
def test_code_statistics
|
||||
assert_match "Code LOC: 62 Test LOC: 5 Code to Test Ratio: 1:0.1",
|
||||
assert_match "Code LOC: 63 Test LOC: 5 Code to Test Ratio: 1:0.1",
|
||||
rails("stats")
|
||||
end
|
||||
|
||||
|
@ -180,6 +180,7 @@ def skipped_files
|
||||
tmp/cache/assets
|
||||
public/404.html
|
||||
public/422.html
|
||||
public/426.html
|
||||
public/500.html
|
||||
public/apple-touch-icon-precomposed.png
|
||||
public/apple-touch-icon.png
|
||||
|
Loading…
Reference in New Issue
Block a user