browserx 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 (112) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +29 -0
  3. data/LICENSE.md +21 -0
  4. data/README.md +438 -0
  5. data/bot_exceptions.yml +5 -0
  6. data/bots.yml +327 -0
  7. data/languages.yml +113 -0
  8. data/lib/browser/accept_language.rb +55 -0
  9. data/lib/browser/action_controller.rb +20 -0
  10. data/lib/browser/aliases.rb +33 -0
  11. data/lib/browser/alipay.rb +21 -0
  12. data/lib/browser/base.rb +292 -0
  13. data/lib/browser/blackberry.rb +23 -0
  14. data/lib/browser/bot/empty_user_agent_matcher.rb +11 -0
  15. data/lib/browser/bot/keyword_matcher.rb +11 -0
  16. data/lib/browser/bot/known_bots_matcher.rb +11 -0
  17. data/lib/browser/bot.rb +119 -0
  18. data/lib/browser/browser.rb +116 -0
  19. data/lib/browser/chrome.rb +43 -0
  20. data/lib/browser/detect_version.rb +22 -0
  21. data/lib/browser/device/android.rb +20 -0
  22. data/lib/browser/device/base.rb +13 -0
  23. data/lib/browser/device/blackberry_playbook.rb +19 -0
  24. data/lib/browser/device/ipad.rb +19 -0
  25. data/lib/browser/device/iphone.rb +19 -0
  26. data/lib/browser/device/ipod_touch.rb +19 -0
  27. data/lib/browser/device/kindle.rb +19 -0
  28. data/lib/browser/device/kindle_fire.rb +19 -0
  29. data/lib/browser/device/playstation3.rb +19 -0
  30. data/lib/browser/device/playstation4.rb +19 -0
  31. data/lib/browser/device/psp.rb +19 -0
  32. data/lib/browser/device/psvita.rb +19 -0
  33. data/lib/browser/device/samsung.rb +33 -0
  34. data/lib/browser/device/surface.rb +23 -0
  35. data/lib/browser/device/switch.rb +19 -0
  36. data/lib/browser/device/tv.rb +19 -0
  37. data/lib/browser/device/unknown.rb +19 -0
  38. data/lib/browser/device/wii.rb +19 -0
  39. data/lib/browser/device/wiiu.rb +19 -0
  40. data/lib/browser/device/xbox_360.rb +19 -0
  41. data/lib/browser/device/xbox_one.rb +19 -0
  42. data/lib/browser/device.rb +222 -0
  43. data/lib/browser/duck_duck_go.rb +22 -0
  44. data/lib/browser/edge.rb +25 -0
  45. data/lib/browser/electron.rb +22 -0
  46. data/lib/browser/epiphany.rb +21 -0
  47. data/lib/browser/facebook.rb +23 -0
  48. data/lib/browser/firefox.rb +21 -0
  49. data/lib/browser/google_search_app.rb +21 -0
  50. data/lib/browser/huawei_browser.rb +21 -0
  51. data/lib/browser/instagram.rb +21 -0
  52. data/lib/browser/internet_explorer.rb +63 -0
  53. data/lib/browser/maxthon.rb +21 -0
  54. data/lib/browser/meta/base.rb +42 -0
  55. data/lib/browser/meta/device.rb +11 -0
  56. data/lib/browser/meta/generic_browser.rb +15 -0
  57. data/lib/browser/meta/id.rb +11 -0
  58. data/lib/browser/meta/ie.rb +20 -0
  59. data/lib/browser/meta/ios.rb +11 -0
  60. data/lib/browser/meta/mobile.rb +11 -0
  61. data/lib/browser/meta/platform.rb +11 -0
  62. data/lib/browser/meta/proxy.rb +11 -0
  63. data/lib/browser/meta/safari.rb +11 -0
  64. data/lib/browser/meta/tablet.rb +11 -0
  65. data/lib/browser/meta/webkit.rb +11 -0
  66. data/lib/browser/meta.rb +14 -0
  67. data/lib/browser/micro_messenger.rb +21 -0
  68. data/lib/browser/middleware/context/additions.rb +18 -0
  69. data/lib/browser/middleware/context/url_methods.rb +15 -0
  70. data/lib/browser/middleware/context.rb +22 -0
  71. data/lib/browser/middleware.rb +70 -0
  72. data/lib/browser/miui_browser.rb +21 -0
  73. data/lib/browser/nokia.rb +21 -0
  74. data/lib/browser/opera.rb +21 -0
  75. data/lib/browser/otter.rb +21 -0
  76. data/lib/browser/phantom_js.rb +21 -0
  77. data/lib/browser/platform/adobe_air.rb +23 -0
  78. data/lib/browser/platform/android.rb +23 -0
  79. data/lib/browser/platform/base.rb +18 -0
  80. data/lib/browser/platform/blackberry.rb +23 -0
  81. data/lib/browser/platform/chrome_os.rb +23 -0
  82. data/lib/browser/platform/firefox_os.rb +23 -0
  83. data/lib/browser/platform/ios.rb +36 -0
  84. data/lib/browser/platform/kai_os.rb +23 -0
  85. data/lib/browser/platform/linux.rb +23 -0
  86. data/lib/browser/platform/mac.rb +25 -0
  87. data/lib/browser/platform/unknown.rb +23 -0
  88. data/lib/browser/platform/windows.rb +23 -0
  89. data/lib/browser/platform/windows_mobile.rb +23 -0
  90. data/lib/browser/platform/windows_phone.rb +23 -0
  91. data/lib/browser/platform.rb +193 -0
  92. data/lib/browser/qq.rb +25 -0
  93. data/lib/browser/rails.rb +23 -0
  94. data/lib/browser/safari.rb +40 -0
  95. data/lib/browser/samsung_browser.rb +21 -0
  96. data/lib/browser/snapchat.rb +21 -0
  97. data/lib/browser/sougou_browser.rb +24 -0
  98. data/lib/browser/sputnik.rb +21 -0
  99. data/lib/browser/testing.rb +27 -0
  100. data/lib/browser/uc_browser.rb +21 -0
  101. data/lib/browser/unknown.rb +32 -0
  102. data/lib/browser/version.rb +5 -0
  103. data/lib/browser/weibo.rb +21 -0
  104. data/lib/browser/yandex.rb +21 -0
  105. data/lib/browser.rb +4 -0
  106. data/lib/browserx.rb +3 -0
  107. data/samsung.yml +138 -0
  108. data/search_engines.yml +7 -0
  109. data/test/ua.yml +181 -0
  110. data/test/ua_bots.yml +121 -0
  111. data/test/ua_search_engines.yml +7 -0
  112. metadata +267 -0
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Browser
4
+ class Base
5
+ include DetectVersion
6
+
7
+ attr_reader :ua
8
+
9
+ def initialize(ua, accept_language: nil)
10
+ validate_size(:user_agent, ua.to_s)
11
+
12
+ @ua = ua
13
+ @accept_language_raw = accept_language.to_s
14
+ end
15
+
16
+ # Return a meta info about this browser.
17
+ def meta
18
+ Meta.get(self)
19
+ end
20
+
21
+ # Return an array with all preferred languages that this browser accepts.
22
+ def accept_language
23
+ @accept_language ||= begin
24
+ validate_size(:accept_language, @accept_language_raw)
25
+ AcceptLanguage.parse(@accept_language_raw)
26
+ end
27
+ end
28
+
29
+ alias_method :to_a, :meta
30
+
31
+ # Return meta representation as string.
32
+ def to_s
33
+ meta.to_a.join(" ")
34
+ end
35
+
36
+ def version
37
+ full_version.split(".").first
38
+ end
39
+
40
+ # Return the platform.
41
+ def platform
42
+ @platform ||= Platform.new(ua)
43
+ end
44
+
45
+ # Return the bot info.
46
+ def bot
47
+ @bot ||= Bot.new(ua)
48
+ end
49
+
50
+ # Detect if current user agent is from a bot.
51
+ def bot?
52
+ bot.bot?
53
+ end
54
+
55
+ # Return the device info.
56
+ def device
57
+ @device ||= Device.new(ua, platform:)
58
+ end
59
+
60
+ # Detect if browser is Microsoft Internet Explorer.
61
+ def ie?(expected_version = nil)
62
+ instance_of?(InternetExplorer) &&
63
+ detect_version?(full_version, expected_version)
64
+ end
65
+
66
+ # Detect if browser is Microsoft Edge.
67
+ def edge?(expected_version = nil)
68
+ instance_of?(Edge) && detect_version?(full_version, expected_version)
69
+ end
70
+
71
+ def compatibility_view?
72
+ false
73
+ end
74
+
75
+ def msie_full_version
76
+ "0.0"
77
+ end
78
+
79
+ def msie_version
80
+ "0"
81
+ end
82
+
83
+ # Detect if browser is Instagram.
84
+ def instagram?(expected_version = nil)
85
+ instance_of?(Instagram) &&
86
+ detect_version?(full_version, expected_version)
87
+ end
88
+
89
+ # Detect if browser is Snapchat.
90
+ def snapchat?(expected_version = nil)
91
+ instance_of?(Snapchat) &&
92
+ detect_version?(full_version, expected_version)
93
+ end
94
+
95
+ # Detect if browser if Facebook.
96
+ def facebook?(expected_version = nil)
97
+ instance_of?(Facebook) &&
98
+ detect_version?(full_version, expected_version)
99
+ end
100
+
101
+ # Detect if browser is Otter.
102
+ def otter?(expected_version = nil)
103
+ instance_of?(Otter) &&
104
+ detect_version?(full_version, expected_version)
105
+ end
106
+
107
+ # Detect if browser is WebKit-based.
108
+ def webkit?(expected_version = nil)
109
+ ua.match?(/AppleWebKit/i) &&
110
+ (!edge? || chromium_based?) &&
111
+ detect_version?(webkit_full_version, expected_version)
112
+ end
113
+
114
+ # Detect if browser is QuickTime
115
+ def quicktime?(expected_version = nil)
116
+ ua.match?(/QuickTime/i) && detect_version?(full_version, expected_version)
117
+ end
118
+
119
+ # Detect if browser is Apple CoreMedia.
120
+ def core_media?(expected_version = nil)
121
+ ua.include?("CoreMedia") && detect_version?(full_version,
122
+ expected_version)
123
+ end
124
+
125
+ # Detect if browser is PhantomJS
126
+ def phantom_js?(expected_version = nil)
127
+ instance_of?(PhantomJS) &&
128
+ detect_version?(full_version, expected_version)
129
+ end
130
+
131
+ # Detect if browser is Safari.
132
+ def safari?(expected_version = nil)
133
+ instance_of?(Safari) && detect_version?(full_version, expected_version)
134
+ end
135
+
136
+ def safari_webapp_mode?
137
+ (device.ipad? || device.iphone?) && ua.include?("AppleWebKit")
138
+ end
139
+
140
+ # Detect if browser is Firefox.
141
+ def firefox?(expected_version = nil)
142
+ instance_of?(Firefox) && detect_version?(full_version, expected_version)
143
+ end
144
+
145
+ # Detect if browser is Chrome.
146
+ def chrome?(expected_version = nil)
147
+ instance_of?(Chrome) && detect_version?(full_version, expected_version)
148
+ end
149
+
150
+ # Detect if browser is Opera.
151
+ def opera?(expected_version = nil)
152
+ instance_of?(Opera) && detect_version?(full_version, expected_version)
153
+ end
154
+
155
+ # Detect if browser is Sputnik.
156
+ def sputnik?(expected_version = nil)
157
+ instance_of?(Sputnik) && detect_version?(full_version, expected_version)
158
+ end
159
+
160
+ # Detect if browser is Yandex.
161
+ def yandex?(expected_version = nil)
162
+ instance_of?(Yandex) && detect_version?(full_version, expected_version)
163
+ end
164
+ alias_method :yandex_browser?, :yandex?
165
+
166
+ # Detect if browser is UCBrowser.
167
+ def uc_browser?(expected_version = nil)
168
+ instance_of?(UCBrowser) &&
169
+ detect_version?(full_version, expected_version)
170
+ end
171
+
172
+ # Detect if browser is Nokia S40 Ovi Browser.
173
+ def nokia?(expected_version = nil)
174
+ instance_of?(Nokia) && detect_version?(full_version, expected_version)
175
+ end
176
+
177
+ # Detect if browser is MicroMessenger.
178
+ def micro_messenger?(expected_version = nil)
179
+ instance_of?(MicroMessenger) &&
180
+ detect_version?(full_version, expected_version)
181
+ end
182
+
183
+ alias_method :wechat?, :micro_messenger?
184
+
185
+ def weibo?(expected_version = nil)
186
+ instance_of?(Weibo) && detect_version?(full_version, expected_version)
187
+ end
188
+
189
+ def alipay?(expected_version = nil)
190
+ instance_of?(Alipay) && detect_version?(full_version, expected_version)
191
+ end
192
+
193
+ # Detect if browser is Opera Mini.
194
+ def opera_mini?(expected_version = nil)
195
+ ua.include?("Opera Mini") && detect_version?(full_version,
196
+ expected_version)
197
+ end
198
+
199
+ # Detect if browser is DuckDuckGo.
200
+ def duck_duck_go?(expected_version = nil)
201
+ ua.include?("DuckDuckGo") && detect_version?(full_version,
202
+ expected_version)
203
+ end
204
+
205
+ # Detect if browser is Samsung.
206
+ def samsung_browser?(expected_version = nil)
207
+ ua.include?("SamsungBrowser") && detect_version?(full_version,
208
+ expected_version)
209
+ end
210
+
211
+ # Detect if browser is Huawei.
212
+ def huawei_browser?(expected_version = nil)
213
+ instance_of?(HuaweiBrowser) &&
214
+ detect_version?(full_version, expected_version)
215
+ end
216
+
217
+ # Detect if browser is Xiaomi Miui.
218
+ def miui_browser?(expected_version = nil)
219
+ instance_of?(MiuiBrowser) &&
220
+ detect_version?(full_version, expected_version)
221
+ end
222
+
223
+ # Detect if browser is Maxthon.
224
+ def maxthon?(expected_version = nil)
225
+ instance_of?(Maxthon) && detect_version?(full_version, expected_version)
226
+ end
227
+
228
+ # Detect if browser is QQ.
229
+ def qq?(expected_version = nil)
230
+ instance_of?(QQ) && detect_version?(full_version, expected_version)
231
+ end
232
+
233
+ # Detect if browser is Sougou.
234
+ def sougou_browser?(expected_version = nil)
235
+ instance_of?(SougouBrowser) &&
236
+ detect_version?(full_version, expected_version)
237
+ end
238
+
239
+ # Detect if browser is Google Search App
240
+ def google_search_app?(expected_version = nil)
241
+ ua.include?("GSA") && detect_version?(full_version, expected_version)
242
+ end
243
+
244
+ # Detect if browser is Chromium-based.
245
+ def chromium_based?
246
+ false
247
+ end
248
+
249
+ def webkit_full_version
250
+ ua[%r{AppleWebKit/([\d.]+)}, 1] || "0.0"
251
+ end
252
+
253
+ def known?
254
+ !unknown?
255
+ end
256
+
257
+ def unknown?
258
+ id == :unknown_browser
259
+ end
260
+
261
+ # Detect if browser is a proxy browser.
262
+ def proxy?
263
+ nokia? || uc_browser? || opera_mini?
264
+ end
265
+
266
+ # Detect if the browser is Electron.
267
+ def electron?(expected_version = nil)
268
+ instance_of?(Electron) && detect_version?(full_version, expected_version)
269
+ end
270
+
271
+ # Detect if the browser is Epiphany.
272
+ def epiphany?(expected_version = nil)
273
+ instance_of?(Epiphany) && detect_version?(full_version, expected_version)
274
+ end
275
+
276
+ private def validate_size(subject, input)
277
+ actual_bytesize = input.bytesize
278
+ size_limit =
279
+ if subject == :user_agent
280
+ Browser.user_agent_size_limit
281
+ else
282
+ Browser.accept_language_size_limit
283
+ end
284
+
285
+ return if actual_bytesize < size_limit
286
+
287
+ raise Error,
288
+ "#{subject} cannot be larger than #{size_limit} bytes; " \
289
+ "actual size is #{actual_bytesize} bytes"
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Browser
4
+ class BlackBerry < Base
5
+ def id
6
+ :blackberry
7
+ end
8
+
9
+ def name
10
+ "BlackBerry"
11
+ end
12
+
13
+ def full_version
14
+ ua[%r{BlackBerry[\da-z]+/([\d.]+)}, 1] ||
15
+ ua[%r{Version/([\d.]+)}, 1] ||
16
+ "0.0"
17
+ end
18
+
19
+ def match?
20
+ ua.match?(/BlackBerry|BB10/)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Browser
4
+ class Bot
5
+ class EmptyUserAgentMatcher
6
+ def self.call(ua, _browser)
7
+ ua == ""
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Browser
4
+ class Bot
5
+ class KeywordMatcher
6
+ def self.call(ua, _browser)
7
+ ua.match?(/crawl|fetch|search|monitoring|spider|bot/)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Browser
4
+ class Bot
5
+ class KnownBotsMatcher
6
+ def self.call(ua, _browser)
7
+ ua.match?(Browser::Bot.keywords_pattern(Browser::Bot.bots))
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Browser
4
+ class Bot
5
+ GENERIC_NAME = "Generic Bot"
6
+
7
+ def self.matchers
8
+ @matchers ||= default_matchers
9
+ end
10
+
11
+ def self.default_matchers
12
+ DEFAULT_MATCHERS.dup
13
+ end
14
+
15
+ def self.load_yaml(path)
16
+ YAML.load_file(Browser.root.join(path))
17
+ end
18
+
19
+ def self.bots
20
+ @bots ||= load_yaml("bots.yml")
21
+ end
22
+
23
+ def self.bot_exceptions
24
+ @bot_exceptions ||= load_yaml("bot_exceptions.yml")
25
+ end
26
+
27
+ def self.search_engines
28
+ @search_engines ||= load_yaml("search_engines.yml")
29
+ end
30
+
31
+ # One regexp per keyword list (an Array, or a Hash keyed by keyword),
32
+ # rebuilt when the list grows or is replaced.
33
+ def self.keywords_pattern(keywords)
34
+ @keywords_pattern_by_list ||= {}.compare_by_identity
35
+ size, pattern = @keywords_pattern_by_list[keywords]
36
+ return pattern if size == keywords.size
37
+
38
+ words = (keywords.is_a?(Hash) ? keywords.keys : keywords).map(&:to_s)
39
+ pattern = Regexp.new(trie_source(words))
40
+ @keywords_pattern_by_list[keywords] = [keywords.size, pattern]
41
+ pattern
42
+ end
43
+
44
+ # A Regexp.union of hundreds of words tries each of them at every
45
+ # position; nested by common prefix, the regexp follows a single branch
46
+ # per character.
47
+ def self.trie_source(words)
48
+ trie = {}
49
+ words.each do |word|
50
+ leaf = word.each_char.reduce(trie) { |node, char| node[char] ||= {} }
51
+ leaf[:end] = true
52
+ end
53
+ trie_node_source(trie) || "(?!)"
54
+ end
55
+
56
+ def self.trie_node_source(node)
57
+ # A shorter keyword matches wherever a longer one would.
58
+ return "" if node.key?(:end)
59
+
60
+ branches = node.map do |char, child|
61
+ Regexp.escape(char) + trie_node_source(child)
62
+ end
63
+ (branches.size == 1) ? branches.first : "(?:#{branches.join("|")})"
64
+ end
65
+ private_class_method :trie_source, :trie_node_source
66
+
67
+ def self.why?(ua)
68
+ ua = ua.downcase.strip
69
+ browser = Browser.new(ua)
70
+ matchers.find { |matcher| matcher.call(ua, browser) }
71
+ end
72
+
73
+ attr_reader :ua
74
+
75
+ def initialize(ua)
76
+ @ua = ua.downcase.strip
77
+ end
78
+
79
+ def bot?
80
+ !bot_exception? && detect_bot?
81
+ end
82
+
83
+ def why?
84
+ self.class.matchers.find { |matcher| matcher.call(ua, self) }
85
+ end
86
+
87
+ def search_engine?
88
+ ua.match?(self.class.keywords_pattern(self.class.search_engines))
89
+ end
90
+
91
+ def name
92
+ return unless bot?
93
+
94
+ self.class.bots.find { |key, _| ua.include?(key) }&.last || GENERIC_NAME
95
+ end
96
+
97
+ private def bot_exception?
98
+ ua.match?(self.class.keywords_pattern(self.class.bot_exceptions))
99
+ end
100
+
101
+ private def detect_bot?
102
+ self.class.matchers.any? do |matcher|
103
+ matcher.call(ua, default_matchers.include?(matcher) ? nil : browser)
104
+ end
105
+ end
106
+
107
+ # The built-in matchers ignore the browser: building it would detect the
108
+ # user agent twice.
109
+ private def default_matchers
110
+ DEFAULT_MATCHERS
111
+ end
112
+
113
+ private def browser
114
+ @browser ||= Browser.new(ua)
115
+ end
116
+
117
+ private :ua
118
+ end
119
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "pathname"
5
+
6
+ require_relative "version"
7
+ require_relative "detect_version"
8
+ require_relative "accept_language"
9
+ require_relative "base"
10
+ require_relative "safari"
11
+ require_relative "chrome"
12
+ require_relative "internet_explorer"
13
+ require_relative "firefox"
14
+ require_relative "edge"
15
+ require_relative "opera"
16
+ require_relative "blackberry"
17
+ require_relative "unknown"
18
+ require_relative "phantom_js"
19
+ require_relative "uc_browser"
20
+ require_relative "nokia"
21
+ require_relative "micro_messenger"
22
+ require_relative "weibo"
23
+ require_relative "qq"
24
+ require_relative "alipay"
25
+ require_relative "electron"
26
+ require_relative "facebook"
27
+ require_relative "otter"
28
+ require_relative "instagram"
29
+ require_relative "yandex"
30
+ require_relative "sputnik"
31
+ require_relative "snapchat"
32
+ require_relative "duck_duck_go"
33
+ require_relative "samsung_browser"
34
+ require_relative "huawei_browser"
35
+ require_relative "miui_browser"
36
+ require_relative "maxthon"
37
+ require_relative "sougou_browser"
38
+ require_relative "google_search_app"
39
+ require_relative "epiphany"
40
+
41
+ require_relative "bot"
42
+ require_relative "bot/empty_user_agent_matcher"
43
+ require_relative "bot/keyword_matcher"
44
+ require_relative "bot/known_bots_matcher"
45
+
46
+ require_relative "middleware"
47
+ require_relative "platform"
48
+ require_relative "device"
49
+ require_relative "meta"
50
+
51
+ module Browser
52
+ EMPTY_STRING = ""
53
+
54
+ Bot::DEFAULT_MATCHERS =
55
+ [Bot::EmptyUserAgentMatcher, Bot::KnownBotsMatcher, Bot::KeywordMatcher]
56
+ .freeze
57
+
58
+ Error = Class.new(StandardError)
59
+
60
+ def self.root
61
+ @root ||= Pathname.new(File.expand_path("../..", __dir__))
62
+ end
63
+
64
+ class << self
65
+ attr_accessor :user_agent_size_limit, :accept_language_size_limit
66
+ end
67
+
68
+ self.user_agent_size_limit = 2048
69
+ self.accept_language_size_limit = 2048
70
+
71
+ # Hold the list of browser matchers.
72
+ # Order is important.
73
+ def self.matchers
74
+ @matchers ||= [
75
+ Nokia,
76
+ UCBrowser,
77
+ PhantomJS,
78
+ BlackBerry,
79
+ Opera,
80
+ Edge,
81
+ InternetExplorer,
82
+ Firefox,
83
+ Otter,
84
+ Epiphany,
85
+ Facebook,
86
+ Instagram,
87
+ Snapchat,
88
+ Weibo,
89
+ MicroMessenger, # must be placed before QQ
90
+ QQ,
91
+ Alipay,
92
+ Electron,
93
+ Yandex,
94
+ Sputnik,
95
+ DuckDuckGo,
96
+ SamsungBrowser,
97
+ HuaweiBrowser,
98
+ MiuiBrowser,
99
+ Maxthon,
100
+ SougouBrowser,
101
+ GoogleSearchApp,
102
+
103
+ # Must be last
104
+ Chrome,
105
+ Safari,
106
+ Unknown
107
+ ]
108
+ end
109
+
110
+ def self.new(user_agent, **)
111
+ matchers.each do |matcher_class|
112
+ matcher = matcher_class.new(user_agent || EMPTY_STRING, **)
113
+ return matcher if matcher.match?
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Browser
4
+ class Chrome < Base
5
+ def id
6
+ :chrome
7
+ end
8
+
9
+ def name
10
+ "Chrome"
11
+ end
12
+
13
+ def full_version
14
+ # Each regex on its own line to enforce precedence.
15
+ ua[%r{Chrome/([\d.]+)}, 1] ||
16
+ ua[%r{CriOS/([\d.]+)}, 1] ||
17
+ ua[%r{Safari/([\d.]+)}, 1] ||
18
+ ua[%r{AppleWebKit/([\d.]+)}, 1] ||
19
+ "0.0"
20
+ end
21
+
22
+ def chromium_based?
23
+ true
24
+ end
25
+
26
+ def match?
27
+ ua.match?(/Chrome|CriOS/) &&
28
+ !ua.match?(/PhantomJS|FxiOS|ArchiveBot/) &&
29
+ !opera? &&
30
+ !edge? &&
31
+ !duck_duck_go? &&
32
+ !yandex? &&
33
+ !sputnik? &&
34
+ !samsung_browser? &&
35
+ !huawei_browser? &&
36
+ !miui_browser? &&
37
+ !maxthon? &&
38
+ !qq? &&
39
+ !sougou_browser? &&
40
+ !google_search_app?
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Browser
4
+ module DetectVersion
5
+ private def detect_version?(actual_version, expected_version)
6
+ return true unless expected_version
7
+ return false unless actual_version
8
+
9
+ expected_version = parse_version(expected_version)
10
+ actual_version = parse_version(actual_version)
11
+
12
+ Gem::Requirement.create(expected_version)
13
+ .satisfied_by?(Gem::Version.create(actual_version))
14
+ rescue ArgumentError
15
+ false
16
+ end
17
+
18
+ private def parse_version(version)
19
+ version.is_a?(Numeric) ? version.to_s : version
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Browser
4
+ class Device
5
+ class Android < Base
6
+ def id
7
+ :unknown
8
+ end
9
+
10
+ def name
11
+ ua[/\(Linux.*?; Android.*?; ([-_a-z0-9 ]+) Build[^)]+\)/i, 1] ||
12
+ "Unknown"
13
+ end
14
+
15
+ def match?
16
+ ua.include?("Android")
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Browser
4
+ class Device
5
+ class Base
6
+ attr_reader :ua
7
+
8
+ def initialize(ua)
9
+ @ua = ua
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Browser
4
+ class Device
5
+ class BlackBerryPlaybook < Base
6
+ def id
7
+ :playbook
8
+ end
9
+
10
+ def name
11
+ "BlackBerry Playbook"
12
+ end
13
+
14
+ def match?
15
+ ua.match?(/PlayBook.*?RIM Tablet/)
16
+ end
17
+ end
18
+ end
19
+ end