├── spec ├── spec_helper.rb └── browsernizer │ ├── browser_version_spec.rb │ ├── browser_spec.rb │ ├── config_spec.rb │ └── router_spec.rb ├── lib ├── browsernizer │ ├── version.rb │ ├── browser_version.rb │ ├── browser.rb │ ├── config.rb │ └── router.rb ├── browsernizer.rb └── generators │ ├── templates │ └── browsernizer.rb │ └── browsernizer │ └── install_generator.rb ├── .gitignore ├── Gemfile ├── Rakefile ├── CHANGELOG ├── browsernizer.gemspec ├── LICENSE └── README.md /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'browsernizer' 3 | -------------------------------------------------------------------------------- /lib/browsernizer/version.rb: -------------------------------------------------------------------------------- 1 | module Browsernizer 2 | VERSION = "0.2.4" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rvmrc 2 | .rspec 3 | *.gem 4 | .bundle 5 | Gemfile.lock 6 | pkg/* 7 | /bin 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in browsernizer.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rspec/core/rake_task' 3 | 4 | task :default => :spec 5 | 6 | RSpec::Core::RakeTask.new do |t| 7 | t.ruby_opts = ["-w"] 8 | end 9 | -------------------------------------------------------------------------------- /lib/browsernizer.rb: -------------------------------------------------------------------------------- 1 | require "browsernizer/browser" 2 | require "browsernizer/browser_version" 3 | require "browsernizer/config" 4 | require "browsernizer/router" 5 | require "browsernizer/version" 6 | 7 | require 'browser' 8 | -------------------------------------------------------------------------------- /lib/generators/templates/browsernizer.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.middleware.use Browsernizer::Router do |config| 2 | config.supported "Internet Explorer", "9" 3 | config.supported "Firefox", "4" 4 | config.supported "Opera", "11.1" 5 | config.supported "Chrome", "7" 6 | 7 | config.location "/browser.html" 8 | config.exclude %r{^/assets} 9 | end 10 | -------------------------------------------------------------------------------- /lib/generators/browsernizer/install_generator.rb: -------------------------------------------------------------------------------- 1 | module Browsernizer 2 | 3 | class InstallGenerator < Rails::Generators::Base 4 | source_root File.expand_path("../../templates", __FILE__) 5 | 6 | desc "Copies initializer script" 7 | def copy_initializer 8 | copy_file "browsernizer.rb", "config/initializers/browsernizer.rb" 9 | end 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | = Version 0.2.3 2 | * Allow arbitrary include/exclude logic - #13 by @romanbsd 3 | 4 | = Version 0.2.2 5 | * Relax version dependency of the 'browser' gem - by @adammck 6 | 7 | = Version 0.2.1 8 | * Code cleanup by @Bertg 9 | * Getting rid of warnings by @prowler 10 | 11 | = Version 0.2.0 12 | * ditched useragent, now using browser gem (backward compatibility might be broken) 13 | -------------------------------------------------------------------------------- /lib/browsernizer/browser_version.rb: -------------------------------------------------------------------------------- 1 | module Browsernizer 2 | class BrowserVersion 3 | include ::Comparable 4 | 5 | attr_accessor :to_a 6 | 7 | def initialize(version) 8 | @version = version 9 | end 10 | 11 | def to_a 12 | @version.split(".").map{ |s| s.to_i } 13 | end 14 | 15 | def <=>(other) 16 | ([0]*6).zip(to_a, [*other]).each do |dump, a, b| 17 | r = (a||0) <=> (b||0) 18 | return r unless r.zero? 19 | end 20 | 0 21 | end 22 | 23 | def to_s 24 | @version 25 | end 26 | 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/browsernizer/browser.rb: -------------------------------------------------------------------------------- 1 | module Browsernizer 2 | 3 | class Browser 4 | 5 | attr_reader :name, :version 6 | 7 | def initialize(name, version) 8 | @name = name.to_s 9 | if version === false 10 | @version = false 11 | else 12 | @version = BrowserVersion.new version.to_s 13 | end 14 | end 15 | 16 | def meets?(requirement) 17 | if name.downcase == requirement.name.downcase 18 | if requirement.version === false 19 | false 20 | else 21 | version >= requirement.version 22 | end 23 | else 24 | nil 25 | end 26 | end 27 | 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /spec/browsernizer/browser_version_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Browsernizer::BrowserVersion do 4 | 5 | describe "Comparable" do 6 | 7 | it "is greater" do 8 | expect(version("2")).to be > version("1") 9 | expect(version("1.1")).to be > version("1") 10 | expect(version("1.1")).to be > version("1.0") 11 | expect(version("2")).to be > version("1.9") 12 | end 13 | 14 | it "is equal" do 15 | expect(version("1")).to eq(version("1")) 16 | expect(version("1")).to eq(version("1.0")) 17 | expect(version("1.0")).to eq(version("1")) 18 | end 19 | 20 | it "handles strings" do 21 | expect(version("1.0")).to eq(version("1.0.alpha")) 22 | end 23 | 24 | end 25 | 26 | 27 | def version(str) 28 | Browsernizer::BrowserVersion.new(str) 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /spec/browsernizer/browser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Browsernizer::Browser do 4 | 5 | describe "#meets?(requirement)" do 6 | context "same vendor" do 7 | it "returns true if version is >= to requirement" do 8 | expect(browser("Chrome", "10.0").meets?(browser("Chrome", "10" ))).to be true 9 | expect(browser("Chrome", "10.0").meets?(browser("Chrome", "10.1"))).to be false 10 | expect(browser("Chrome", "10" ).meets?(browser("Chrome", " 9.1"))).to be true 11 | end 12 | it "returns false if requirement version is set to false" do 13 | expect(browser("Chrome", "10" ).meets?(browser("Chrome", false ))).to be false 14 | end 15 | end 16 | 17 | context "different vendors" do 18 | it "returns nil" do 19 | expect(browser("Chrome", "10").meets?(browser("Firefox", "10"))).to be_nil 20 | end 21 | end 22 | end 23 | 24 | def browser(name, version) 25 | Browsernizer::Browser.new(name, version) 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /browsernizer.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "browsernizer/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "browsernizer" 7 | s.version = Browsernizer::VERSION 8 | s.authors = ["Milovan Zogovic"] 9 | s.email = ["milovan.zogovic@gmail.com"] 10 | s.homepage = "" 11 | s.summary = %q{Want friendly "please upgrade your browser" page? This gem is for you.} 12 | s.description = %q{Rack middleware for redirecting unsupported user agents to "please upgrade" page} 13 | 14 | s.rubyforge_project = "browsernizer" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.require_paths = ["lib"] 20 | 21 | s.add_development_dependency "rake" 22 | s.add_development_dependency "rspec" 23 | 24 | s.add_runtime_dependency "browser", ">= 2.0", "< 3.0" 25 | end 26 | -------------------------------------------------------------------------------- /lib/browsernizer/config.rb: -------------------------------------------------------------------------------- 1 | module Browsernizer 2 | class Config 3 | 4 | def initialize 5 | @supported = [] 6 | @location = nil 7 | @exclusions = [] 8 | @handler = lambda { } 9 | end 10 | 11 | def supported(*args, &block) 12 | if args.length == 2 13 | @supported << Browser.new(args[0], args[1]) 14 | elsif block_given? 15 | @supported << block 16 | else 17 | raise ArgumentError, "accepts either (browser, version) or block" 18 | end 19 | end 20 | 21 | def location(path) 22 | @location = path 23 | end 24 | 25 | def exclude(path) 26 | @exclusions << path 27 | end 28 | 29 | def get_supported 30 | @supported 31 | end 32 | 33 | def get_location 34 | @location 35 | end 36 | 37 | def excluded?(path) 38 | @exclusions.any? do |exclusion| 39 | case exclusion 40 | when String 41 | exclusion == path 42 | when Regexp 43 | exclusion =~ path 44 | end 45 | end 46 | end 47 | 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/browsernizer/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Browsernizer::Config do 4 | 5 | subject { config = Browsernizer::Config.new } 6 | 7 | describe "supported(name, version)" do 8 | it "defines new supported browser" do 9 | subject.supported "Chrome", "16.0" 10 | subject.supported "Firefox", "10.0" 11 | expect(subject.get_supported.size).to eq(2) 12 | end 13 | 14 | it "allows to unsupport browser by using false as version number" do 15 | subject.supported "Chrome", false 16 | expect(subject.get_supported[0].version).to be false 17 | end 18 | end 19 | 20 | describe "location(path)" do 21 | it "sets the redirection path for unsupported browsers" do 22 | subject.location "foo.html" 23 | expect(subject.get_location).to eq("foo.html") 24 | end 25 | end 26 | 27 | describe "exclude(path)" do 28 | it "defines new excluded path" do 29 | subject.exclude %r{^/assets} 30 | subject.exclude "/foo/bar.html" 31 | 32 | expect(subject.excluded?("/assets/foo.jpg")).to be true 33 | expect(subject.excluded?("/Assets/foo.jpg")).to be false 34 | expect(subject.excluded?("/prefix/assets/foo.jpg")).to be false 35 | expect(subject.excluded?("/foo/bar.html")).to be true 36 | expect(subject.excluded?("/foo/bar2.html")).to be false 37 | end 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Milovan Zogovic 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of the FreeBSD Project. 27 | -------------------------------------------------------------------------------- /lib/browsernizer/router.rb: -------------------------------------------------------------------------------- 1 | module Browsernizer 2 | 3 | class Router 4 | attr_reader :config 5 | 6 | def initialize(app, &block) 7 | @app = app 8 | @config = Config.new 9 | yield(@config) 10 | end 11 | 12 | def call(env) 13 | raw_browser, browser = get_browsers(env) 14 | env["browsernizer"] = { 15 | "supported" => supported?(raw_browser, browser), 16 | "browser" => browser.name.to_s, 17 | "version" => browser.version.to_s 18 | } 19 | redirect_request(env) || @app.call(env) 20 | end 21 | 22 | private 23 | 24 | def redirect_request(env) 25 | return if path_excluded?(env) 26 | if !env["browsernizer"]["supported"] 27 | return redirect_to_specified if @config.get_location && !on_redirection_path?(env) 28 | elsif on_redirection_path?(env) 29 | return redirect_to_root 30 | end 31 | end 32 | 33 | def redirect_to_specified 34 | [307, {"Content-Type" => "text/plain", "Location" => @config.get_location}, []] 35 | end 36 | 37 | def redirect_to_root 38 | [303, {"Content-Type" => "text/plain", "Location" => "/"}, []] 39 | end 40 | 41 | def path_excluded?(env) 42 | @config.excluded? env["PATH_INFO"] 43 | end 44 | 45 | def on_redirection_path?(env) 46 | @config.get_location && @config.get_location == env["PATH_INFO"] 47 | end 48 | 49 | def get_browsers(env) 50 | raw_browser = ::Browser.new env["HTTP_USER_AGENT"] 51 | browser = Browsernizer::Browser.new raw_browser.name.to_s, raw_browser.full_version.to_s 52 | [raw_browser, browser] 53 | end 54 | 55 | # supported by default 56 | def supported?(raw_browser, browser) 57 | @config.get_supported.inject(true) do |default, requirement| 58 | supported = if requirement.respond_to?(:call) 59 | requirement.call(raw_browser) 60 | else 61 | browser.meets?(requirement) 62 | end 63 | break supported unless supported.nil? 64 | default 65 | end 66 | end 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Browsernizer 2 | 3 | [![Gem Version](https://badge.fury.io/rb/browsernizer.png)](http://badge.fury.io/rb/browsernizer) 4 | 5 | Want friendly "please upgrade your browser" page? This gem is for you. 6 | 7 | You can redirect your visitors to any page you like (static or dynamic). 8 | Just specify `config.location` option in initializer file. 9 | 10 | 11 | ## Installation 12 | 13 | Add following line to your `Gemfile`: 14 | 15 | gem 'browsernizer' 16 | 17 | Hook it up: 18 | 19 | rails generate browsernizer:install 20 | 21 | Configure browser support in `config/initializers/browsernizer.rb` file. 22 | 23 | 24 | ## Configuration 25 | 26 | Initializer file is pretty self explanatory. Here is the default one: 27 | 28 | Rails.application.config.middleware.use Browsernizer::Router do |config| 29 | config.supported "Internet Explorer", "9" 30 | config.supported "Firefox", "4" 31 | config.supported "Opera", "11.1" 32 | config.supported "Chrome", "7" 33 | 34 | config.location "/browser.html" 35 | config.exclude %r{^/assets} 36 | end 37 | 38 | It states that IE9+, FF4+, Opera 11.1+ and Chrome 7+ are supported. 39 | Non listed browsers are considered to be supported regardless of their version. 40 | Unsupported browsers will be redirected to `/browser.html` page. 41 | 42 | You can specify which paths you wish to exclude with `exclude` method. 43 | It accepts string or regular expression. You can specify multiple paths by 44 | calling the `config.exclude` multiple times. 45 | 46 | If you wish to completely prevent some browsers from accessing website 47 | (regardless of their version), just set browser version to `false`. 48 | 49 | config.supported "Internet Explorer", false 50 | 51 | There is also an option to provide block for more advanced checking. 52 | [Browser object](https://github.com/fnando/browser/blob/master/lib/browser.rb) will be 53 | passed to it. If you wish to state that *mobile safari* is not supported, you 54 | can declare it like this: 55 | 56 | config.supported do |browser| 57 | !(browser.name == "Safari" && browser.device.mobile?) 58 | end 59 | 60 | The block should return false to block the browser, true to allow it, and nil if it 61 | cannot decide. This way you can make any arbitrary User-Agent explicitly allowed, 62 | even if its version would have been blocked otherwise: 63 | 64 | config.supported do |browser| 65 | true if browser.ua.include?('Linux') 66 | end 67 | 68 | Please note, that the order is important, thus the first block or requirement returning 69 | a boolean wins. 70 | 71 | Specifying location is optional. If you prefer handling unsupported browsers on 72 | your own, you can access browsernizer info from `request.env['browsernizer']` 73 | within your controller. 74 | 75 | For example, you can set before filter to display flash notice: 76 | 77 | before_filter :check_browser_support 78 | 79 | def check_browser_support 80 | unless request.env['browsernizer']['supported'] 81 | flash.notice = "Your browser is not supported" 82 | end 83 | end 84 | 85 | You can also access `browser` and `version` variables from this env hash. 86 | 87 | 88 | ## Browsers 89 | 90 | You should specify browser name as a string. Here are the available options: 91 | 92 | * Chrome 93 | * Firefox 94 | * Safari 95 | * Opera 96 | * Internet Explorer 97 | 98 | And some less popular: 99 | 100 | * Android 101 | * BlackBerry 102 | * iPad 103 | * iPhone 104 | * iPod Touch 105 | * PlayStation Portable 106 | * QuickTime 107 | * Apple CoreMedia 108 | 109 | Browser detection is done using [browser gem](https://github.com/fnando/browser). 110 | 111 | 112 | 113 | ## Credits and License 114 | 115 | Developed by Milovan Zogovic. 116 | 117 | This software is released under the Simplified BSD License. 118 | 119 | -------------------------------------------------------------------------------- /spec/browsernizer/router_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Browsernizer::Router do 4 | 5 | let(:app) { double } 6 | 7 | subject do 8 | Browsernizer::Router.new(app) do |config| 9 | config.supported do |browser| 10 | true if browser.ua.include?('Spec') 11 | end 12 | config.supported "Firefox", false 13 | config.supported "Chrome", "7.1" 14 | config.supported do |browser| 15 | !(browser.safari? && browser.device.mobile?) 16 | end 17 | end 18 | end 19 | 20 | let(:default_env) do 21 | { 22 | "HTTP_USER_AGENT" => chrome_agent("7.1.1"), 23 | "PATH_INFO" => "/index" 24 | } 25 | end 26 | 27 | context "All Good" do 28 | it "propagates request with updated env" do 29 | expect(app).to receive(:call) do |env| 30 | expect(env['browsernizer']['supported']).to be true 31 | expect(env['browsernizer']['browser']).to eq("Chrome") 32 | expect(env['browsernizer']['version']).to eq("7.1.1") 33 | end 34 | subject.call(default_env) 35 | end 36 | end 37 | 38 | 39 | shared_examples "unsupported browser" do 40 | context "location not set" do 41 | it "propagates request with updated env" do 42 | expect(app).to receive(:call) do |env| 43 | expect(env['browsernizer']['supported']).to be false 44 | end 45 | subject.call(@env) 46 | end 47 | end 48 | 49 | context "location is set" do 50 | before do 51 | subject.config.location "/browser.html" 52 | end 53 | 54 | it "prevents propagation" do 55 | expect(app).not_to receive(:call) 56 | subject.call(@env) 57 | end 58 | 59 | it "redirects to proper location" do 60 | response = subject.call(@env) 61 | expect(response[0]).to eq(307) 62 | expect(response[1]["Location"]).to eq("/browser.html") 63 | end 64 | 65 | context "Excluded path" do 66 | before do 67 | subject.config.exclude %r{^/assets} 68 | @env = @env.merge({ 69 | "PATH_INFO" => "/assets/foo.jpg", 70 | }) 71 | end 72 | it "propagates request" do 73 | expect(app).to receive(:call).with(@env) 74 | subject.call(@env) 75 | end 76 | end 77 | 78 | context "Already on /browser.html page" do 79 | before do 80 | @env = @env.merge({ 81 | "PATH_INFO" => "/browser.html" 82 | }) 83 | end 84 | it "propagates request with updated env" do 85 | expect(app).to receive(:call) do |env| 86 | expect(env['browsernizer']['supported']).to be false 87 | end 88 | subject.call(@env) 89 | end 90 | end 91 | end 92 | end 93 | 94 | context "Unsupported Version" do 95 | before do 96 | @env = default_env.merge({ 97 | "HTTP_USER_AGENT" => chrome_agent("7") 98 | }) 99 | end 100 | it_behaves_like "unsupported browser" 101 | end 102 | 103 | context "Unsupported Vendor" do 104 | before do 105 | @env = default_env.merge({ 106 | "HTTP_USER_AGENT" => firefox_agent("10.0.1") 107 | }) 108 | end 109 | it_behaves_like "unsupported browser" 110 | end 111 | 112 | context "Unsupported by proc" do 113 | before do 114 | @env = default_env.merge({ 115 | "HTTP_USER_AGENT" => mobile_safari_agent 116 | }) 117 | end 118 | it_behaves_like "unsupported browser" 119 | end 120 | 121 | context "Supported by proc" do 122 | before do 123 | @env = default_env.merge({ 124 | "HTTP_USER_AGENT" => firefox_agent("10.0.1") + ' Spec' 125 | }) 126 | end 127 | 128 | it "propagates request" do 129 | expect(app).to receive(:call) do |env| 130 | expect(env['browsernizer']['supported']).to be true 131 | end 132 | subject.call(@env) 133 | end 134 | end 135 | 136 | def chrome_agent(version) 137 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/#{version} Safari/535.7" 138 | end 139 | 140 | def firefox_agent(version) 141 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:10.0.1) Gecko/20100101 Firefox/#{version}" 142 | end 143 | 144 | def mobile_safari_agent 145 | "Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/4A102 Safari/419" 146 | end 147 | 148 | end 149 | --------------------------------------------------------------------------------