browser 2.5.2 → 5.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (146) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE.md +1 -1
  3. data/.github/workflows/tests.yml +57 -0
  4. data/.gitignore +1 -0
  5. data/.prettierignore +1 -0
  6. data/.rubocop.yml +22 -107
  7. data/CHANGELOG.md +192 -11
  8. data/FUNDING.yml +3 -0
  9. data/Gemfile +2 -0
  10. data/README.md +139 -89
  11. data/Rakefile +11 -3
  12. data/bot_exceptions.yml +2 -0
  13. data/bots.yml +302 -247
  14. data/browser.gemspec +12 -7
  15. data/gemfiles/rails6_0.gemfile +6 -0
  16. data/gemfiles/rails6_1.gemfile +6 -0
  17. data/lib/browser/accept_language.rb +12 -9
  18. data/lib/browser/action_controller.rb +1 -3
  19. data/lib/browser/aliases.rb +5 -5
  20. data/lib/browser/alipay.rb +2 -2
  21. data/lib/browser/base.rb +101 -19
  22. data/lib/browser/blackberry.rb +3 -3
  23. data/lib/browser/bot/empty_user_agent_matcher.rb +11 -0
  24. data/lib/browser/bot/keyword_matcher.rb +11 -0
  25. data/lib/browser/bot/known_bots_matcher.rb +11 -0
  26. data/lib/browser/bot.rb +39 -27
  27. data/lib/browser/browser.rb +65 -54
  28. data/lib/browser/chrome.rb +18 -5
  29. data/lib/browser/detect_version.rb +5 -5
  30. data/lib/browser/device/android.rb +20 -0
  31. data/lib/browser/device/blackberry_playbook.rb +1 -1
  32. data/lib/browser/device/ipad.rb +1 -1
  33. data/lib/browser/device/iphone.rb +1 -1
  34. data/lib/browser/device/ipod_touch.rb +1 -1
  35. data/lib/browser/device/kindle.rb +1 -1
  36. data/lib/browser/device/kindle_fire.rb +1 -1
  37. data/lib/browser/device/playstation3.rb +1 -1
  38. data/lib/browser/device/playstation4.rb +1 -1
  39. data/lib/browser/device/psp.rb +1 -1
  40. data/lib/browser/device/psvita.rb +1 -1
  41. data/lib/browser/device/samsung.rb +33 -0
  42. data/lib/browser/device/surface.rb +2 -4
  43. data/lib/browser/device/switch.rb +19 -0
  44. data/lib/browser/device/tv.rb +1 -1
  45. data/lib/browser/device/unknown.rb +1 -1
  46. data/lib/browser/device/wii.rb +1 -1
  47. data/lib/browser/device/wiiu.rb +1 -1
  48. data/lib/browser/device/xbox_360.rb +1 -1
  49. data/lib/browser/device/xbox_one.rb +1 -1
  50. data/lib/browser/device.rb +47 -30
  51. data/lib/browser/duck_duck_go.rb +22 -0
  52. data/lib/browser/edge.rb +6 -2
  53. data/lib/browser/electron.rb +2 -2
  54. data/lib/browser/facebook.rb +4 -2
  55. data/lib/browser/firefox.rb +2 -2
  56. data/lib/browser/google_search_app.rb +21 -0
  57. data/lib/browser/huawei_browser.rb +21 -0
  58. data/lib/browser/instagram.rb +21 -0
  59. data/lib/browser/internet_explorer.rb +9 -10
  60. data/lib/browser/maxthon.rb +21 -0
  61. data/lib/browser/meta/base.rb +0 -1
  62. data/lib/browser/meta/generic_browser.rb +1 -3
  63. data/lib/browser/meta.rb +12 -13
  64. data/lib/browser/micro_messenger.rb +2 -2
  65. data/lib/browser/middleware/context/additions.rb +1 -1
  66. data/lib/browser/middleware.rb +4 -3
  67. data/lib/browser/miui_browser.rb +21 -0
  68. data/lib/browser/nokia.rb +2 -2
  69. data/lib/browser/opera.rb +2 -2
  70. data/lib/browser/otter.rb +2 -2
  71. data/lib/browser/phantom_js.rb +2 -2
  72. data/lib/browser/platform/adobe_air.rb +2 -2
  73. data/lib/browser/platform/android.rb +1 -1
  74. data/lib/browser/platform/base.rb +3 -2
  75. data/lib/browser/platform/blackberry.rb +2 -2
  76. data/lib/browser/platform/chrome_os.rb +1 -1
  77. data/lib/browser/platform/firefox_os.rb +1 -1
  78. data/lib/browser/platform/ios.rb +17 -4
  79. data/lib/browser/platform/kai_os.rb +23 -0
  80. data/lib/browser/platform/linux.rb +1 -1
  81. data/lib/browser/platform/mac.rb +5 -3
  82. data/lib/browser/platform/{other.rb → unknown.rb} +3 -3
  83. data/lib/browser/platform/windows.rb +2 -2
  84. data/lib/browser/platform/windows_mobile.rb +1 -1
  85. data/lib/browser/platform/windows_phone.rb +1 -1
  86. data/lib/browser/platform.rb +37 -28
  87. data/lib/browser/qq.rb +5 -5
  88. data/lib/browser/rails.rb +11 -5
  89. data/lib/browser/safari.rb +19 -4
  90. data/lib/browser/samsung_browser.rb +21 -0
  91. data/lib/browser/snapchat.rb +21 -0
  92. data/lib/browser/sougou_browser.rb +24 -0
  93. data/lib/browser/sputnik.rb +21 -0
  94. data/lib/browser/uc_browser.rb +2 -2
  95. data/lib/browser/{generic.rb → unknown.rb} +6 -8
  96. data/lib/browser/version.rb +1 -1
  97. data/lib/browser/weibo.rb +2 -2
  98. data/lib/browser/yandex.rb +21 -0
  99. data/lib/browser.rb +2 -2
  100. data/samsung.yml +138 -0
  101. data/search_engines.yml +2 -2
  102. data/test/browser_test.rb +42 -10
  103. data/test/rails_test.rb +10 -0
  104. data/test/sample_app.rb +14 -0
  105. data/test/test_helper.rb +9 -1
  106. data/test/ua.yml +147 -109
  107. data/test/ua_bots.yml +98 -45
  108. data/test/ua_search_engines.yml +7 -6
  109. data/test/unit/accept_language_test.rb +24 -0
  110. data/test/unit/adobe_air_test.rb +1 -1
  111. data/test/unit/alipay_test.rb +6 -0
  112. data/test/unit/blackberry_test.rb +0 -6
  113. data/test/unit/bots_test.rb +37 -27
  114. data/test/unit/chrome_test.rb +8 -19
  115. data/test/unit/console_test.rb +2 -2
  116. data/test/unit/device_test.rb +60 -3
  117. data/test/unit/duck_duck_go_test.rb +37 -0
  118. data/test/unit/edge_test.rb +49 -5
  119. data/test/unit/facebook_test.rb +20 -0
  120. data/test/unit/firefox_test.rb +0 -3
  121. data/test/unit/google_search_app_test.rb +54 -0
  122. data/test/unit/huawei_browser_test.rb +25 -0
  123. data/test/unit/instagram_test.rb +30 -0
  124. data/test/unit/internet_explorer_test.rb +0 -12
  125. data/test/unit/ios_test.rb +7 -5
  126. data/test/unit/kai_os_test.rb +31 -0
  127. data/test/unit/kindle_test.rb +0 -2
  128. data/test/unit/maxthon_test.rb +25 -0
  129. data/test/unit/meta_test.rb +10 -2
  130. data/test/unit/micro_messenger_test.rb +21 -6
  131. data/test/unit/miui_browser_test.rb +25 -0
  132. data/test/unit/opera_test.rb +1 -2
  133. data/test/unit/platform_test.rb +34 -8
  134. data/test/unit/qq_test.rb +12 -0
  135. data/test/unit/safari_test.rb +12 -7
  136. data/test/unit/samsung_browser_test.rb +23 -0
  137. data/test/unit/snapchat_test.rb +40 -0
  138. data/test/unit/sougou_browser_test.rb +41 -0
  139. data/test/unit/sputnik_test.rb +22 -0
  140. data/test/unit/yandex_test.rb +37 -0
  141. metadata +83 -26
  142. data/.bundle/config +0 -2
  143. data/.travis.yml +0 -16
  144. data/bin/rake +0 -17
  145. data/gemfiles/rails4.gemfile +0 -4
  146. data/lib/browser/meta/modern.rb +0 -11
@@ -14,8 +14,7 @@ module Browser
14
14
  .map {|string| string.squeeze(" ").strip }
15
15
  .map {|part| new(part) }
16
16
  .reject {|al| al.quality.zero? }
17
- .sort_by(&:quality)
18
- .reverse
17
+ .sort_by.with_index {|al, idx| [-al.quality, idx] }
19
18
  end
20
19
 
21
20
  attr_reader :part
@@ -35,26 +34,30 @@ module Browser
35
34
  def code
36
35
  @code ||= begin
37
36
  code = part[/\A([^-;]+)/, 1]
38
- code.downcase if code
37
+ code&.downcase
39
38
  end
40
39
  end
41
40
 
42
41
  def region
43
42
  @region ||= begin
44
43
  region = part[/\A(?:.*?)-([^;-]+)/, 1]
45
- region.upcase if region
44
+ region&.upcase
46
45
  end
47
46
  end
48
47
 
49
48
  def quality
50
- @quality ||= Float(quality_value || 1.0)
49
+ @quality ||= begin
50
+ Float(quality_value || 1.0)
51
+ rescue ArgumentError
52
+ 0.1
53
+ end
51
54
  end
52
55
 
53
- private
54
-
55
- def quality_value
56
+ private def quality_value
56
57
  qvalue = part[/;q=([\d.]+)/, 1]
57
- qvalue =~ /\A0\.0?\z/ ? "0.0" : qvalue
58
+ qvalue = /\A0\.0?\z/.match?(qvalue) ? "0.0" : qvalue
59
+ qvalue = qvalue.squeeze(".") if qvalue
60
+ qvalue
58
61
  end
59
62
  end
60
63
  end
@@ -10,9 +10,7 @@ module Browser
10
10
  helper_method(:browser) if respond_to?(:helper_method)
11
11
  end
12
12
 
13
- private
14
-
15
- def browser
13
+ private def browser
16
14
  @browser ||= Browser.new(
17
15
  request.headers["User-Agent"],
18
16
  accept_language: request.headers["Accept-Language"]
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "browser"
3
+ require_relative "../browser"
4
4
  require "forwardable"
5
5
 
6
6
  module Browser
@@ -15,10 +15,10 @@ module Browser
15
15
 
16
16
  DEVICE_ALIASES = %w[
17
17
  blackberry_playbook? console? ipad? iphone? ipod_touch? kindle?
18
- kindle_fire? mobile? nintendo? nintendo_wii? nintendo_wiiu? playbook?
19
- playstation3? playstation4? playstation? playstation_vita? ps3? ps4? psp?
20
- psp_vita? silk? surface? tablet? tv? vita? wii? wiiu? xbox? xbox_360?
21
- xbox_one?
18
+ kindle_fire? mobile? nintendo? nintendo_switch? nintendo_wii?
19
+ nintendo_wiiu? playbook? playstation3? playstation4? playstation?
20
+ playstation_vita? ps3? ps4? psp? psp_vita? silk? surface? tablet? tv?
21
+ vita? wii? wiiu? xbox? xbox_360? xbox_one?
22
22
  ].freeze
23
23
 
24
24
  def self.included(target)
@@ -11,11 +11,11 @@ module Browser
11
11
  end
12
12
 
13
13
  def full_version
14
- ua[%r[(?:AlipayClient)/([\d.]+)]i, 1] || "0.0"
14
+ ua[%r{(?:AlipayClient)/([\d.]+)}i, 1] || "0.0"
15
15
  end
16
16
 
17
17
  def match?
18
- ua =~ /AlipayClient/i
18
+ ua.match?(/AlipayClient/i)
19
19
  end
20
20
  end
21
21
  end
data/lib/browser/base.rb CHANGED
@@ -6,12 +6,11 @@ module Browser
6
6
 
7
7
  attr_reader :ua
8
8
 
9
- # Return an array with all preferred languages that this browser accepts.
10
- attr_reader :accept_language
11
-
12
9
  def initialize(ua, accept_language: nil)
10
+ validate_size(:user_agent, ua.to_s)
11
+
13
12
  @ua = ua
14
- @accept_language = AcceptLanguage.parse(accept_language)
13
+ @accept_language_raw = accept_language.to_s
15
14
  end
16
15
 
17
16
  # Return a meta info about this browser.
@@ -19,6 +18,14 @@ module Browser
19
18
  Meta.get(self)
20
19
  end
21
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
+
22
29
  alias_method :to_a, :meta
23
30
 
24
31
  # Return meta representation as string.
@@ -50,11 +57,6 @@ module Browser
50
57
  @device ||= Device.new(ua)
51
58
  end
52
59
 
53
- # Return true if browser is modern (Webkit, Firefox 17+, IE9+, Opera 12+).
54
- def modern?
55
- Browser.modern_rules.any? {|rule| rule === self } # rubocop:disable Metrics/LineLength, Style/CaseEquality
56
- end
57
-
58
60
  # Detect if browser is Microsoft Internet Explorer.
59
61
  def ie?(expected_version = nil)
60
62
  InternetExplorer.new(ua).match? &&
@@ -78,6 +80,18 @@ module Browser
78
80
  "0"
79
81
  end
80
82
 
83
+ # Detect if browser is Instagram.
84
+ def instagram?(expected_version = nil)
85
+ Instagram.new(ua).match? &&
86
+ detect_version?(full_version, expected_version)
87
+ end
88
+
89
+ # Detect if browser is Snapchat.
90
+ def snapchat?(expected_version = nil)
91
+ Snapchat.new(ua).match? &&
92
+ detect_version?(full_version, expected_version)
93
+ end
94
+
81
95
  # Detect if browser if Facebook.
82
96
  def facebook?(expected_version = nil)
83
97
  Facebook.new(ua).match? &&
@@ -92,19 +106,20 @@ module Browser
92
106
 
93
107
  # Detect if browser is WebKit-based.
94
108
  def webkit?(expected_version = nil)
95
- ua =~ /AppleWebKit/i &&
96
- !edge? &&
109
+ ua.match?(/AppleWebKit/i) &&
110
+ (!edge? || Edge.new(ua).chrome_based?) &&
97
111
  detect_version?(webkit_full_version, expected_version)
98
112
  end
99
113
 
100
114
  # Detect if browser is QuickTime
101
115
  def quicktime?(expected_version = nil)
102
- ua =~ /QuickTime/i && detect_version?(full_version, expected_version)
116
+ ua.match?(/QuickTime/i) && detect_version?(full_version, expected_version)
103
117
  end
104
118
 
105
119
  # Detect if browser is Apple CoreMedia.
106
120
  def core_media?(expected_version = nil)
107
- ua =~ /CoreMedia/ && detect_version?(full_version, expected_version)
121
+ ua.include?("CoreMedia") && detect_version?(full_version,
122
+ expected_version)
108
123
  end
109
124
 
110
125
  # Detect if browser is PhantomJS
@@ -115,11 +130,11 @@ module Browser
115
130
 
116
131
  # Detect if browser is Safari.
117
132
  def safari?(expected_version = nil)
118
- Safari.new(ua).match? && detect_version?(version, expected_version)
133
+ Safari.new(ua).match? && detect_version?(full_version, expected_version)
119
134
  end
120
135
 
121
136
  def safari_webapp_mode?
122
- (device.ipad? || device.iphone?) && ua =~ /AppleWebKit/
137
+ (device.ipad? || device.iphone?) && ua.include?("AppleWebKit")
123
138
  end
124
139
 
125
140
  # Detect if browser is Firefox.
@@ -137,10 +152,16 @@ module Browser
137
152
  Opera.new(ua).match? && detect_version?(full_version, expected_version)
138
153
  end
139
154
 
155
+ # Detect if browser is Sputnik.
156
+ def sputnik?(expected_version = nil)
157
+ Sputnik.new(ua).match? && detect_version?(full_version, expected_version)
158
+ end
159
+
140
160
  # Detect if browser is Yandex.
141
161
  def yandex?(expected_version = nil)
142
- ua =~ /YaBrowser/ && detect_version?(full_version, expected_version)
162
+ Yandex.new(ua).match? && detect_version?(full_version, expected_version)
143
163
  end
164
+ alias_method :yandex_browser?, :yandex?
144
165
 
145
166
  # Detect if browser is UCBrowser.
146
167
  def uc_browser?(expected_version = nil)
@@ -171,15 +192,65 @@ module Browser
171
192
 
172
193
  # Detect if browser is Opera Mini.
173
194
  def opera_mini?(expected_version = nil)
174
- ua =~ /Opera Mini/ && detect_version?(full_version, expected_version)
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
+ HuaweiBrowser.new(ua).match? &&
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
+ MiuiBrowser.new(ua).match? &&
220
+ detect_version?(full_version, expected_version)
221
+ end
222
+
223
+ # Detect if browser is Maxthon.
224
+ def maxthon?(expected_version = nil)
225
+ Maxthon.new(ua).match? && detect_version?(full_version, expected_version)
226
+ end
227
+
228
+ # Detect if browser is QQ.
229
+ def qq?(expected_version = nil)
230
+ QQ.new(ua).match? && detect_version?(full_version, expected_version)
231
+ end
232
+
233
+ # Detect if browser is Sougou.
234
+ def sougou_browser?(expected_version = nil)
235
+ SougouBrowser.new(ua).match? &&
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)
175
242
  end
176
243
 
177
244
  def webkit_full_version
178
- ua[%r[AppleWebKit/([\d.]+)], 1] || "0.0"
245
+ ua[%r{AppleWebKit/([\d.]+)}, 1] || "0.0"
179
246
  end
180
247
 
181
248
  def known?
182
- id != :generic
249
+ !unknown?
250
+ end
251
+
252
+ def unknown?
253
+ id == :unknown_browser
183
254
  end
184
255
 
185
256
  # Detect if browser is a proxy browser.
@@ -191,5 +262,16 @@ module Browser
191
262
  def electron?(expected_version = nil)
192
263
  Electron.new(ua).match? && detect_version?(full_version, expected_version)
193
264
  end
265
+
266
+ private def validate_size(subject, input)
267
+ actual_bytesize = input.bytesize
268
+ size_limit = Browser.public_send("#{subject}_size_limit")
269
+
270
+ return if actual_bytesize < size_limit
271
+
272
+ raise Error,
273
+ "#{subject} cannot be larger than #{size_limit} bytes; " \
274
+ "actual size is #{actual_bytesize} bytes"
275
+ end
194
276
  end
195
277
  end
@@ -11,13 +11,13 @@ module Browser
11
11
  end
12
12
 
13
13
  def full_version
14
- ua[%r[BlackBerry[\da-z]+/([\d.]+)], 1] ||
15
- ua[%r[Version/([\d.]+)], 1] ||
14
+ ua[%r{BlackBerry[\da-z]+/([\d.]+)}, 1] ||
15
+ ua[%r{Version/([\d.]+)}, 1] ||
16
16
  "0.0"
17
17
  end
18
18
 
19
19
  def match?
20
- ua =~ /BlackBerry|BB10/
20
+ ua.match?(/BlackBerry|BB10/)
21
21
  end
22
22
  end
23
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
+ Browser::Bot.bots.any? {|key, _| ua.include?(key) }
8
+ end
9
+ end
10
+ end
11
+ end
data/lib/browser/bot.rb CHANGED
@@ -2,64 +2,76 @@
2
2
 
3
3
  module Browser
4
4
  class Bot
5
- def self.detect_empty_ua!
6
- @detect_empty_ua = true
5
+ GENERIC_NAME = "Generic Bot"
6
+
7
+ def self.matchers
8
+ @matchers ||= default_matchers
9
+ end
10
+
11
+ def self.default_matchers
12
+ [
13
+ EmptyUserAgentMatcher,
14
+ KnownBotsMatcher,
15
+ KeywordMatcher
16
+ ]
7
17
  end
8
18
 
9
- def self.detect_empty_ua?
10
- @detect_empty_ua
19
+ def self.load_yaml(path)
20
+ YAML.load_file(Browser.root.join(path))
11
21
  end
12
22
 
13
23
  def self.bots
14
- @bots ||= YAML.load_file(Browser.root.join("bots.yml"))
24
+ @bots ||= load_yaml("bots.yml")
15
25
  end
16
26
 
17
27
  def self.bot_exceptions
18
- @bot_exceptions ||= YAML
19
- .load_file(Browser.root.join("bot_exceptions.yml"))
28
+ @bot_exceptions ||= load_yaml("bot_exceptions.yml")
20
29
  end
21
30
 
22
31
  def self.search_engines
23
- @search_engines ||= YAML
24
- .load_file(Browser.root.join("search_engines.yml"))
32
+ @search_engines ||= load_yaml("search_engines.yml")
33
+ end
34
+
35
+ def self.why?(ua)
36
+ ua = ua.downcase.strip
37
+ browser = Browser.new(ua)
38
+ matchers.find {|matcher| matcher.call(ua, browser) }
25
39
  end
26
40
 
27
- attr_reader :ua
41
+ attr_reader :ua, :browser
28
42
 
29
43
  def initialize(ua)
30
- @ua = ua
44
+ @ua = ua.downcase.strip
45
+ @browser = Browser.new(@ua)
31
46
  end
32
47
 
33
48
  def bot?
34
- bot_with_empty_ua? || (!bot_exception? && detect_bot?)
49
+ !bot_exception? && detect_bot?
50
+ end
51
+
52
+ def why?
53
+ self.class.matchers.find {|matcher| matcher.call(ua, self) }
35
54
  end
36
55
 
37
56
  def search_engine?
38
- self.class.search_engines.any? {|key, _| downcased_ua.include?(key) }
57
+ self.class.search_engines.any? {|key, _| ua.include?(key) }
39
58
  end
40
59
 
41
60
  def name
42
61
  return unless bot?
43
- return "Generic Bot" if bot_with_empty_ua?
44
- self.class.bots.find {|key, _| downcased_ua.include?(key) }.last
45
- end
46
62
 
47
- private
48
-
49
- def bot_with_empty_ua?
50
- self.class.detect_empty_ua? && ua.strip == ""
63
+ self.class.bots.find {|key, _| ua.include?(key) }&.last || GENERIC_NAME
51
64
  end
52
65
 
53
- def bot_exception?
54
- self.class.bot_exceptions.any? {|key| downcased_ua.include?(key) }
66
+ private def bot_exception?
67
+ self.class.bot_exceptions.any? {|key| ua.include?(key) }
55
68
  end
56
69
 
57
- def detect_bot?
58
- self.class.bots.any? {|key, _| downcased_ua.include?(key) }
70
+ private def detect_bot?
71
+ self.class.matchers.any? {|matcher| matcher.call(ua, browser) }
59
72
  end
60
73
 
61
- def downcased_ua
62
- @downcased_ua ||= ua.downcase
63
- end
74
+ private :ua
75
+ private :browser
64
76
  end
65
77
  end
@@ -4,43 +4,66 @@ require "set"
4
4
  require "yaml"
5
5
  require "pathname"
6
6
 
7
- require "browser/version"
8
- require "browser/detect_version"
9
- require "browser/accept_language"
10
- require "browser/base"
11
- require "browser/safari"
12
- require "browser/chrome"
13
- require "browser/internet_explorer"
14
- require "browser/firefox"
15
- require "browser/edge"
16
- require "browser/opera"
17
- require "browser/blackberry"
18
- require "browser/generic"
19
- require "browser/phantom_js"
20
- require "browser/uc_browser"
21
- require "browser/nokia"
22
- require "browser/micro_messenger"
23
- require "browser/weibo"
24
- require "browser/qq"
25
- require "browser/alipay"
26
- require "browser/electron"
27
- require "browser/facebook"
28
- require "browser/otter"
7
+ require_relative "version"
8
+ require_relative "detect_version"
9
+ require_relative "accept_language"
10
+ require_relative "base"
11
+ require_relative "safari"
12
+ require_relative "chrome"
13
+ require_relative "internet_explorer"
14
+ require_relative "firefox"
15
+ require_relative "edge"
16
+ require_relative "opera"
17
+ require_relative "blackberry"
18
+ require_relative "unknown"
19
+ require_relative "phantom_js"
20
+ require_relative "uc_browser"
21
+ require_relative "nokia"
22
+ require_relative "micro_messenger"
23
+ require_relative "weibo"
24
+ require_relative "qq"
25
+ require_relative "alipay"
26
+ require_relative "electron"
27
+ require_relative "facebook"
28
+ require_relative "otter"
29
+ require_relative "instagram"
30
+ require_relative "yandex"
31
+ require_relative "sputnik"
32
+ require_relative "snapchat"
33
+ require_relative "duck_duck_go"
34
+ require_relative "samsung_browser"
35
+ require_relative "huawei_browser"
36
+ require_relative "miui_browser"
37
+ require_relative "maxthon"
38
+ require_relative "sougou_browser"
39
+ require_relative "google_search_app"
29
40
 
30
- require "browser/bot"
31
- require "browser/middleware"
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"
32
45
 
33
- require "browser/platform"
34
- require "browser/device"
35
- require "browser/meta"
46
+ require_relative "middleware"
47
+ require_relative "platform"
48
+ require_relative "device"
49
+ require_relative "meta"
36
50
 
37
51
  module Browser
38
- EMPTY_STRING = "".freeze
52
+ EMPTY_STRING = ""
53
+
54
+ Error = Class.new(StandardError)
39
55
 
40
56
  def self.root
41
- @root ||= Pathname.new(File.expand_path("../../..", __FILE__))
57
+ @root ||= Pathname.new(File.expand_path("../..", __dir__))
58
+ end
59
+
60
+ class << self
61
+ attr_accessor :user_agent_size_limit, :accept_language_size_limit
42
62
  end
43
63
 
64
+ self.user_agent_size_limit = 2048
65
+ self.accept_language_size_limit = 2048
66
+
44
67
  # Hold the list of browser matchers.
45
68
  # Order is important.
46
69
  def self.matchers
@@ -55,40 +78,28 @@ module Browser
55
78
  Firefox,
56
79
  Otter,
57
80
  Facebook, # must be placed before Chrome and Safari
81
+ Instagram, # must be placed before Chrome and Safari
82
+ Snapchat, # must be placed before Chrome and Safari
58
83
  Weibo, # must be placed before Chrome and Safari
84
+ MicroMessenger, # must be placed before QQ
59
85
  QQ, # must be placed before Chrome and Safari
60
86
  Alipay, # must be placed before Chrome and Safari
61
87
  Electron, # must be placed before Chrome and Safari
88
+ Yandex, # must be placed before Chrome and Safari
89
+ Sputnik, # must be placed before Chrome and Safari
90
+ DuckDuckGo, # must be placed before Chrome and Safari
91
+ SamsungBrowser, # must be placed before Chrome and Safari
92
+ HuaweiBrowser, # must be placed before Chrome and Safari
93
+ MiuiBrowser, # must be placed before Chrome and Safari
94
+ Maxthon, # must be placed before Chrome and Safari
95
+ SougouBrowser, # must be placed before Chrome and Safari
96
+ GoogleSearchApp, # must be placed before Chrome and Safari
62
97
  Chrome,
63
98
  Safari,
64
- MicroMessenger,
65
- Generic
99
+ Unknown
66
100
  ]
67
101
  end
68
102
 
69
- # Define the rules which define a modern browser.
70
- # A rule must be a proc/lambda or any object that implements the method
71
- # === and accepts the browser object.
72
- #
73
- # To redefine all rules, clear the existing rules before adding your own.
74
- #
75
- # # Only Chrome Canary is considered modern.
76
- # Browser.modern_rules.clear
77
- # Browser.modern_rules << -> b { b.chrome? && b.version >= "37" }
78
- #
79
- def self.modern_rules
80
- @modern_rules ||= []
81
- end
82
-
83
- modern_rules.tap do |rules|
84
- rules << ->(b) { b.webkit? }
85
- rules << ->(b) { b.firefox? && b.version.to_i >= 17 }
86
- rules << ->(b) { b.ie? && b.version.to_i >= 9 && !b.compatibility_view? }
87
- rules << ->(b) { b.edge? && !b.compatibility_view? }
88
- rules << ->(b) { b.opera? && b.version.to_i >= 12 }
89
- rules << ->(b) { b.firefox? && b.device.tablet? && b.platform.android? && b.version.to_i >= 14 } # rubocop:disable Metrics/LineLength
90
- end
91
-
92
103
  def self.new(user_agent, **kwargs)
93
104
  matchers
94
105
  .map {|klass| klass.new(user_agent || EMPTY_STRING, **kwargs) }
@@ -12,15 +12,28 @@ module Browser
12
12
 
13
13
  def full_version
14
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] ||
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
19
  "0.0"
20
20
  end
21
21
 
22
22
  def match?
23
- ua =~ /Chrome|CriOS/ && !opera? && !edge?
23
+ ua.match?(/Chrome|CriOS/) &&
24
+ !ua.match?(/PhantomJS|FxiOS|ArchiveBot/) &&
25
+ !opera? &&
26
+ !edge? &&
27
+ !duck_duck_go? &&
28
+ !yandex? &&
29
+ !sputnik? &&
30
+ !samsung_browser? &&
31
+ !huawei_browser? &&
32
+ !miui_browser? &&
33
+ !maxthon? &&
34
+ !qq? &&
35
+ !sougou_browser? &&
36
+ !google_search_app?
24
37
  end
25
38
  end
26
39
  end
@@ -2,9 +2,7 @@
2
2
 
3
3
  module Browser
4
4
  module DetectVersion
5
- private
6
-
7
- def detect_version?(actual_version, expected_version)
5
+ private def detect_version?(actual_version, expected_version)
8
6
  return true unless expected_version
9
7
  return false if expected_version && !actual_version
10
8
 
@@ -13,10 +11,12 @@ module Browser
13
11
 
14
12
  Gem::Requirement.create(expected_version)
15
13
  .satisfied_by?(Gem::Version.create(actual_version))
14
+ rescue ArgumentError
15
+ false
16
16
  end
17
17
 
18
- def parse_version(version)
19
- version.kind_of?(Numeric) ? version.to_s : version
18
+ private def parse_version(version)
19
+ version.is_a?(Numeric) ? version.to_s : version
20
20
  end
21
21
  end
22
22
  end