browser2 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.hound.yml +90 -0
  4. data/.rubocop.yml +90 -0
  5. data/.travis.yml +14 -0
  6. data/CONTRIBUTING.md +44 -0
  7. data/Gemfile +2 -0
  8. data/LICENSE +20 -0
  9. data/README.md +125 -0
  10. data/Rakefile +33 -0
  11. data/browser2.gemspec +42 -0
  12. data/config.ru +2 -0
  13. data/data/bots.json +176 -0
  14. data/data/languages.json +115 -0
  15. data/gemfiles/rails3.gemfile +4 -0
  16. data/lib/browser/action_controller.rb +20 -0
  17. data/lib/browser/data/bots.rb +5 -0
  18. data/lib/browser/data/languages.rb +5 -0
  19. data/lib/browser/data/search_engines.rb +14 -0
  20. data/lib/browser/meta/base.rb +20 -0
  21. data/lib/browser/meta/generic_browser.rb +15 -0
  22. data/lib/browser/meta/id.rb +9 -0
  23. data/lib/browser/meta/ie.rb +18 -0
  24. data/lib/browser/meta/ios.rb +9 -0
  25. data/lib/browser/meta/mobile.rb +9 -0
  26. data/lib/browser/meta/modern.rb +9 -0
  27. data/lib/browser/meta/platform.rb +9 -0
  28. data/lib/browser/meta/safari.rb +9 -0
  29. data/lib/browser/meta/webkit.rb +9 -0
  30. data/lib/browser/methods/blackberry.rb +51 -0
  31. data/lib/browser/methods/bots.rb +37 -0
  32. data/lib/browser/methods/consoles.rb +43 -0
  33. data/lib/browser/methods/devices.rb +41 -0
  34. data/lib/browser/methods/ie.rb +104 -0
  35. data/lib/browser/methods/language.rb +16 -0
  36. data/lib/browser/methods/mobile.rb +30 -0
  37. data/lib/browser/methods/platform.rb +142 -0
  38. data/lib/browser/methods/tv.rb +8 -0
  39. data/lib/browser/middleware/context/additions.rb +16 -0
  40. data/lib/browser/middleware/context/url_methods.rb +13 -0
  41. data/lib/browser/middleware/context.rb +20 -0
  42. data/lib/browser/middleware.rb +66 -0
  43. data/lib/browser/rails.rb +14 -0
  44. data/lib/browser/version.rb +8 -0
  45. data/lib/browser.rb +255 -0
  46. data/test/benchmark_test.rb +83 -0
  47. data/test/browser_test.rb +248 -0
  48. data/test/middleware_test.rb +50 -0
  49. data/test/sample_app.rb +31 -0
  50. data/test/test_helper.rb +20 -0
  51. data/test/ua.yml +122 -0
  52. data/test/unit/adobe_air_test.rb +13 -0
  53. data/test/unit/android_test.rb +93 -0
  54. data/test/unit/blackberry_test.rb +104 -0
  55. data/test/unit/bots_test.rb +116 -0
  56. data/test/unit/chrome_test.rb +56 -0
  57. data/test/unit/console_test.rb +67 -0
  58. data/test/unit/firefox_test.rb +37 -0
  59. data/test/unit/ie_test.rb +377 -0
  60. data/test/unit/ios_test.rb +139 -0
  61. data/test/unit/kindle_test.rb +37 -0
  62. data/test/unit/opera_test.rb +42 -0
  63. data/test/unit/windows_phone_test.rb +50 -0
  64. data/test/unit/windows_test.rb +60 -0
  65. metadata +240 -0
@@ -0,0 +1,142 @@
1
+ class Browser
2
+ module Platform
3
+ # Detect if browser is Android.
4
+ def android?(version = nil)
5
+ !!(ua =~ /Android/ && !opera?) && detect_version?(android_version, version)
6
+ end
7
+
8
+ # Detect Android version.
9
+ def android_version
10
+ ua[/Android ([\d.]+)/, 1]
11
+ end
12
+
13
+ # Return the iOS version.
14
+ def ios_version
15
+ ua[/OS (\d)/, 1]
16
+ end
17
+
18
+ # Detect if browser is ios?.
19
+ def ios?(version = nil)
20
+ (ipod? || ipad? || iphone?) && detect_version?(ios_version, version)
21
+ end
22
+
23
+ # Detect if is iOS4.
24
+ def ios4?
25
+ deprecate "Browser##{__method__} is deprecated; use Browser#ios?(version) instead"
26
+ ios?(4)
27
+ end
28
+
29
+ # Detect if is iOS5.
30
+ def ios5?
31
+ deprecate "Browser##{__method__} is deprecated; use Browser#ios?(version) instead"
32
+ ios?(5)
33
+ end
34
+
35
+ # Detect if is iOS6.
36
+ def ios6?
37
+ deprecate "Browser##{__method__} is deprecated; use Browser#ios?(version) instead"
38
+ ios?(6)
39
+ end
40
+
41
+ # Detect if is iOS7.
42
+ def ios7?
43
+ deprecate "Browser##{__method__} is deprecated; use Browser#ios?(version) instead"
44
+ ios?(7)
45
+ end
46
+
47
+ # Detect if is iOS8.
48
+ def ios8?
49
+ deprecate "Browser##{__method__} is deprecated; use Browser#ios?(version) instead"
50
+ ios?(8)
51
+ end
52
+
53
+ # Detect if is iOS9.
54
+ def ios9?
55
+ deprecate "Browser##{__method__} is deprecated; use Browser#ios?(version) instead"
56
+ ios?(9)
57
+ end
58
+
59
+ # Detect if current platform is Macintosh.
60
+ def mac?
61
+ !!(ua =~ /Mac OS X/ && !ios?)
62
+ end
63
+
64
+ # Detect if current platform is Windows.
65
+ def windows?
66
+ !!(ua =~ /Windows/)
67
+ end
68
+
69
+ ## More info here => http://msdn.microsoft.com/fr-FR/library/ms537503.aspx#PltToken
70
+ def windows_xp?
71
+ windows? && !!(ua =~ /Windows NT 5.1/)
72
+ end
73
+
74
+ def windows_vista?
75
+ windows? && !!(ua =~ /Windows NT 6.0/)
76
+ end
77
+
78
+ def windows7?
79
+ windows? && !!(ua =~ /Windows NT 6.1/)
80
+ end
81
+
82
+ def windows8?
83
+ windows? && !!(ua =~ /Windows NT 6.[2-3]/)
84
+ end
85
+
86
+ def windows8_1?
87
+ windows? && !!(ua =~ /Windows NT 6\.3/)
88
+ end
89
+
90
+ def windows10?
91
+ windows? && !!(ua =~ /Windows NT 10/)
92
+ end
93
+
94
+ def windows_rt?
95
+ windows8? && !!(ua =~ /ARM/)
96
+ end
97
+
98
+ # Detect if current platform is Windows Mobile.
99
+ def windows_mobile?
100
+ !!(ua =~ /Windows CE/)
101
+ end
102
+
103
+ # Detect if current platform is Windows Phone.
104
+ def windows_phone?
105
+ !!(ua =~ /Windows Phone/)
106
+ end
107
+
108
+ # Detect if current platform is Windows in 64-bit architecture.
109
+ def windows_x64?
110
+ windows? && !!(ua =~ /(Win64|x64)/) && !!(ua =~ /x64/)
111
+ end
112
+
113
+ def windows_wow64?
114
+ windows? && !!(ua =~ /WOW64/i)
115
+ end
116
+
117
+ def windows_x64_inclusive?
118
+ windows_x64? || windows_wow64?
119
+ end
120
+
121
+ # Detect if current platform is Linux flavor.
122
+ def linux?
123
+ in_ua? 'Linux'
124
+ end
125
+
126
+ # Detect if current platform is ChromeOS
127
+ def chrome_os?
128
+ in_ua? 'CrOS'
129
+ end
130
+
131
+ # Return the platform.
132
+ def platform
133
+ case
134
+ when linux? then :linux
135
+ when mac? then :mac
136
+ when windows? then :windows
137
+ else
138
+ :other
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,8 @@
1
+ class Browser
2
+ module Tv
3
+ # Detect if browser is Television
4
+ def tv?
5
+ !!(ua =~ /(tv|Android.*?ADT-1|Nexus Player)/i)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ require "browser/middleware/context/url_methods"
2
+
3
+ class Browser
4
+ class Middleware
5
+ class Context
6
+ module Additions
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ include Rails.application.routes.url_helpers
11
+ include UrlMethods
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ class Browser
2
+ class Middleware
3
+ class Context
4
+ module UrlMethods
5
+ def default_url_options
6
+ Rails.configuration.browser.default_url_options ||
7
+ Rails.configuration.action_mailer.default_url_options ||
8
+ {}
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ class Browser
2
+ class Middleware
3
+ class Context
4
+ attr_reader :browser, :request
5
+
6
+ def initialize(request)
7
+ @request = request
8
+
9
+ @browser = Browser.new(
10
+ ua: request.user_agent,
11
+ accept_language: request.env["HTTP_ACCEPT_LANGUAGE"]
12
+ )
13
+ end
14
+
15
+ def redirect_to(path)
16
+ throw :redirected, path.to_s
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,66 @@
1
+ require "uri"
2
+
3
+ class Browser
4
+ class Middleware
5
+ # Detect the most common assets.
6
+ ASSETS_REGEX = /\.(css|png|jpe?g|gif|js|svg|ico|flv|mov|m4v|ogg|swf)\z/i
7
+
8
+ # Detect the ACCEPT header. IE8 send */*.
9
+ ACCEPT_REGEX = %r[(text/html|\*/\*)]
10
+
11
+ def initialize(app, &block)
12
+ fail ArgumentError, "Browser::Middleware requires a block" unless block
13
+
14
+ @app = app
15
+ @block = block
16
+ end
17
+
18
+ def call(env)
19
+ request = Rack::Request.new(env)
20
+
21
+ # Only apply verification on HTML requests.
22
+ # This ensures that images, CSS and JavaScript
23
+ # will be rendered.
24
+ return run_app(env) unless process?(request)
25
+
26
+ path = catch(:redirected) do
27
+ Context.new(request).instance_eval(&@block)
28
+ end
29
+
30
+ # No path, no match.
31
+ return run_app(env) unless path
32
+
33
+ resolve_redirection(env, request.path, path)
34
+ end
35
+
36
+ def resolve_redirection(env, current_path, path)
37
+ uri = URI.parse(path)
38
+
39
+ if uri.path == current_path
40
+ run_app(env)
41
+ else
42
+ redirect(path)
43
+ end
44
+ end
45
+
46
+ def redirect(path)
47
+ [302, {"Content-Type" => "text/html", "Location" => path}, []]
48
+ end
49
+
50
+ def run_app(env)
51
+ @app.call(env)
52
+ end
53
+
54
+ def process?(request)
55
+ html?(request) && !assets?(request)
56
+ end
57
+
58
+ def html?(request)
59
+ request.env["HTTP_ACCEPT"].to_s.match(ACCEPT_REGEX)
60
+ end
61
+
62
+ def assets?(request)
63
+ request.path.match(ASSETS_REGEX)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,14 @@
1
+ require "rails/railtie"
2
+ require "browser/action_controller"
3
+ require "browser/middleware/context/additions"
4
+
5
+ class Browser
6
+ class Railtie < Rails::Railtie
7
+ config.browser = ActiveSupport::OrderedOptions.new
8
+
9
+ initializer "browser" do
10
+ ::ActionController::Base.send :include, Browser::ActionController
11
+ Browser::Middleware::Context.send :include, Browser::Middleware::Context::Additions
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ class Browser
2
+ module Version
3
+ MAJOR = 1
4
+ MINOR = 0
5
+ PATCH = 0
6
+ STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
7
+ end
8
+ end
data/lib/browser.rb ADDED
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "multi_json"
4
+ require "pathname"
5
+ require "set"
6
+ require "yaml"
7
+
8
+ require "browser/data/bots"
9
+ require "browser/data/languages"
10
+ require "browser/data/search_engines"
11
+
12
+ require "browser/middleware"
13
+ require "browser/middleware/context"
14
+ require "browser/rails" if defined?(::Rails)
15
+
16
+ require "browser/methods/ie"
17
+ require "browser/methods/blackberry"
18
+ require "browser/methods/platform"
19
+ require "browser/methods/mobile"
20
+ require "browser/methods/devices"
21
+ require "browser/methods/consoles"
22
+ require "browser/methods/language"
23
+ require "browser/methods/bots"
24
+ require "browser/methods/tv"
25
+
26
+ require "browser/meta/base"
27
+ require "browser/meta/generic_browser"
28
+ require "browser/meta/id"
29
+ require "browser/meta/ie"
30
+ require "browser/meta/ios"
31
+ require "browser/meta/mobile"
32
+ require "browser/meta/modern"
33
+ require "browser/meta/platform"
34
+ require "browser/meta/safari"
35
+ require "browser/meta/webkit"
36
+
37
+ class Browser
38
+ include IE
39
+ include BlackBerry
40
+ include Platform
41
+ include Mobile
42
+ include Devices
43
+ include Consoles
44
+ include Language
45
+ include Bots
46
+ include Tv
47
+
48
+ # Get the browser's UA string; this is immutable after initialization.
49
+ attr_reader :user_agent
50
+ alias_method :ua, :user_agent
51
+
52
+ NAMES = {
53
+ edge: "Microsoft Edge", # Must come before everything
54
+ ie: "Internet Explorer", # Must come before android
55
+ chrome: "Chrome", # Must come before android
56
+ firefox: "Firefox", # Must come before android
57
+ android: "Android",
58
+ blackberry_running_safari: "Safari",
59
+ blackberry: "BlackBerry",
60
+ core_media: "Apple CoreMedia",
61
+ ipad: "iPad", # Must come before safari
62
+ iphone: "iPhone", # Must come before safari
63
+ ipod: "iPod Touch", # Must come before safari
64
+ nintendo: "Nintendo",
65
+ opera: "Opera",
66
+ phantom_js: "PhantomJS",
67
+ psp: "PlayStation Portable",
68
+ playstation: "PlayStation",
69
+ quicktime: "QuickTime",
70
+ safari: "Safari",
71
+ xbox: "Xbox",
72
+
73
+ # This must be last item, since Ruby 1.9+ has ordered keys.
74
+ other: "Other",
75
+ }
76
+
77
+ VERSIONS = {
78
+ edge: %r[Edge/([\d.]+)],
79
+ chrome: %r[(?:Chrome|CriOS)/([\d.]+)],
80
+ default: %r[(?:Version|MSIE|Firefox|QuickTime|BlackBerry[^/]+|CoreMedia v|PhantomJS|AdobeAIR)[/ ]?([a-z0-9.]+)]i,
81
+ opera: %r[(?:Opera/.*? Version/([\d.]+)|Chrome/.*?OPR/([\d.]+))],
82
+ ie: %r[(?:MSIE |Trident/.*?; rv:)([\d.]+)]
83
+ }
84
+
85
+ # Define the rules which define a modern browser.
86
+ # A rule must be a proc/lambda or any object that implements the method
87
+ # === and accepts the browser object.
88
+ #
89
+ # To redefine all rules, clear the existing rules before adding your own.
90
+ #
91
+ # # Only Chrome Canary is considered modern.
92
+ # Browser.modern_rules.clear
93
+ # Browser.modern_rules << -> b { b.chrome? && b.version >= '37' }
94
+ #
95
+ def self.modern_rules
96
+ @modern_rules ||= []
97
+ end
98
+
99
+ self.modern_rules.tap do |rules|
100
+ rules << -> b { b.webkit? }
101
+ rules << -> b { b.firefox? && b.version.to_i >= 17 }
102
+ rules << -> b { b.ie? && b.version.to_i >= 9 && !b.compatibility_view? }
103
+ rules << -> b { b.edge? && !b.compatibility_view? }
104
+ rules << -> b { b.opera? && b.version.to_i >= 12 }
105
+ rules << -> b { b.firefox? && b.tablet? && b.android? && b.version.to_i >= 14 }
106
+ end
107
+
108
+ # Create a new browser instance and set
109
+ # the UA and Accept-Language headers.
110
+ #
111
+ # browser = Browser.new({
112
+ # :ua => "Safari",
113
+ # :accept_language => "pt-br"
114
+ # })
115
+ #
116
+ def initialize(options = {})
117
+ user_agent = options[:user_agent] || options[:ua]
118
+ unless user_agent
119
+ raise ArgumentError, 'Must receive a :user_agent or :ua option'
120
+ end
121
+
122
+ @user_agent = user_agent.to_s
123
+ self.accept_language = options[:accept_language].to_s
124
+
125
+ yield self if block_given?
126
+ end
127
+
128
+ # Get readable browser name.
129
+ def name
130
+ NAMES[id]
131
+ end
132
+
133
+ # Get the browser identifier.
134
+ def id
135
+ @id ||=
136
+ NAMES.keys
137
+ .find {|id| respond_to?("#{id}?", true) ? send("#{id}?") : id }
138
+ end
139
+
140
+ # Return major version.
141
+ def version
142
+ if ie?
143
+ ie_version
144
+ else
145
+ full_version.to_s.split(".").first
146
+ end
147
+ end
148
+
149
+ # Return the full version.
150
+ def full_version
151
+ @full_version ||=
152
+ if ie?
153
+ ie_full_version
154
+ else
155
+ _, *v = *ua.match(VERSIONS.fetch(id, VERSIONS[:default]))
156
+ v.compact.first || "0.0"
157
+ end
158
+ end
159
+
160
+ # Return true if browser is modern (Webkit, Firefox 17+, IE9+, Opera 12+).
161
+ def modern?
162
+ self.class.modern_rules.any? { |rule| rule.call self }
163
+ end
164
+
165
+ # Detect if browser is WebKit-based.
166
+ def webkit?
167
+ ua =~ /AppleWebKit/i && !edge?
168
+ end
169
+
170
+ # Detect if browser is QuickTime
171
+ def quicktime?
172
+ !!(ua =~ /QuickTime/i)
173
+ end
174
+
175
+ # Detect if browser is Apple CoreMedia.
176
+ def core_media?
177
+ !!(ua =~ /CoreMedia/)
178
+ end
179
+
180
+ # Detect if browser is PhantomJS
181
+ def phantom_js?
182
+ !!(ua =~ /PhantomJS/)
183
+ end
184
+
185
+ # Detect if browser is Safari.
186
+ def safari?
187
+ (in_ua?('Safari') || safari_webapp_mode?) && ua !~ /Android|Chrome|CriOS|PhantomJS/
188
+ end
189
+
190
+ def safari_webapp_mode?
191
+ (ipad? || iphone?) && in_ua?('AppleWebKit')
192
+ end
193
+
194
+ # Detect if browser is Firefox.
195
+ def firefox?
196
+ in_ua? 'Firefox'
197
+ end
198
+
199
+ # Detect if browser is Chrome.
200
+ def chrome?
201
+ (in_ua?('Chrome') || in_ua?('CriOS')) && !opera? && !edge?
202
+ end
203
+
204
+ # Detect if browser is Opera.
205
+ def opera?
206
+ in_ua?('Opera') || in_ua?('OPR')
207
+ end
208
+
209
+ # Detect if browser is Silk.
210
+ def silk?
211
+ in_ua? 'Silk'
212
+ end
213
+
214
+ # Detect if browser is Yandex.
215
+ def yandex?
216
+ in_ua? 'YaBrowser'
217
+ end
218
+
219
+ def known?
220
+ id != :other
221
+ end
222
+
223
+ # Return a meta info about this browser.
224
+ def meta
225
+ @meta ||=
226
+ Meta.constants.each_with_object(Set.new) do |meta_name, meta|
227
+ meta_class = Meta.const_get(meta_name)
228
+ meta.merge(meta_class.new(self).to_a)
229
+ end.to_a
230
+ end
231
+
232
+ alias_method :to_a, :meta
233
+
234
+ # Return meta representation as string.
235
+ def to_s
236
+ meta.to_a.join(" ")
237
+ end
238
+
239
+ private
240
+
241
+ def detect_version?(actual_version, expected_version)
242
+ return true unless expected_version
243
+ actual_version.to_s.start_with?(expected_version.to_s)
244
+ end
245
+
246
+ # Private shorthand for checking if a given string is in the user agent.
247
+ def in_ua? string
248
+ ua.include? string
249
+ end
250
+
251
+ def deprecate(message)
252
+ offender = caller[1].to_s[/^(.*?\.rb:\d+).*?$/, 1]
253
+ $stderr << "\n#{message} (called from #{offender})\n"
254
+ end
255
+ end
@@ -0,0 +1,83 @@
1
+ require 'test_helper'
2
+
3
+ require 'benchmark'
4
+ require 'minitest/benchmark'
5
+
6
+ class BenchmarkTest < Minitest::Benchmark
7
+ def self.bench_range
8
+ [100, 10_000]
9
+ end
10
+
11
+ def bench_chrome
12
+ assert_browser_performance($ua['CHROME']) do |browser|
13
+ assert_equal "Chrome", browser.name
14
+ assert browser.chrome?
15
+ refute browser.safari?
16
+ assert browser.webkit?
17
+ assert browser.modern?
18
+ assert_equal "5.0.375.99", browser.full_version
19
+ assert_equal "5", browser.version
20
+ end
21
+ end
22
+
23
+ def bench_firefox
24
+ assert_browser_performance($ua["FIREFOX_MODERN"]) do |browser|
25
+ assert_equal :firefox, browser.id
26
+ assert_equal "Firefox", browser.name
27
+ assert browser.firefox?
28
+ assert browser.modern?
29
+ assert_equal "17.0", browser.full_version
30
+ assert_equal "17", browser.version
31
+ end
32
+ end
33
+
34
+ def bench_bots
35
+ assert_browser_performance($ua['GOOGLE_BOT']) do |browser|
36
+ assert browser.bot?
37
+ assert_equal browser.bot_name, 'Googlebot'
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ WARMUP = 1_000
44
+ ITERATIONS = 10_000
45
+ GC_ITERATIONS = 1_00
46
+
47
+ def assert_browser_performance ua, &test_block
48
+ block = proc do
49
+ browser = Browser.new ua: ua
50
+
51
+ test_block.call browser
52
+ end
53
+
54
+ WARMUP.times &block
55
+
56
+ time = Benchmark.realtime do
57
+ ITERATIONS.times &block
58
+ end
59
+
60
+ objects = benchmark_memory &block
61
+
62
+ print "#{name}\t%10.5f seconds\t%d objects\n" % [time, objects]
63
+ end
64
+
65
+ def validation_noop
66
+ proc { }
67
+ end
68
+
69
+ def benchmark_memory &block
70
+ GC.start
71
+
72
+ before_allocated = GC.stat[:total_allocated_objects]
73
+ GC.disable
74
+
75
+ GC_ITERATIONS.times &block
76
+
77
+ after_allocated = GC.stat[:total_allocated_objects]
78
+ GC.enable
79
+
80
+ return after_allocated - before_allocated
81
+ end
82
+
83
+ end