device_detector 1.0.7 → 1.1.1

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.
@@ -49,6 +49,8 @@ class DeviceDetector
49
49
  OPERATING_SYSTEMS = {
50
50
  'AIX' => 'AIX',
51
51
  'AND' => 'Android',
52
+ 'ADR' => 'Android TV',
53
+ 'AMZ' => 'Amazon Linux',
52
54
  'AMG' => 'AmigaOS',
53
55
  'ATV' => 'tvOS',
54
56
  'ARL' => 'Arch Linux',
@@ -57,10 +59,15 @@ class DeviceDetector
57
59
  'BEO' => 'BeOS',
58
60
  'BLB' => 'BlackBerry OS',
59
61
  'QNX' => 'BlackBerry Tablet OS',
62
+ 'BOS' => 'Bliss OS',
60
63
  'BMP' => 'Brew',
61
64
  'CAI' => 'Caixa Mágica',
62
65
  'CES' => 'CentOS',
66
+ 'CST' => 'CentOS Stream',
67
+ 'CLR' => 'ClearOS Mobile',
63
68
  'COS' => 'Chrome OS',
69
+ 'CRS' => 'Chromium OS',
70
+ 'CHN' => 'China OS',
64
71
  'CYN' => 'CyanogenMod',
65
72
  'DEB' => 'Debian',
66
73
  'DEE' => 'Deepin',
@@ -70,9 +77,11 @@ class DeviceDetector
70
77
  'FEN' => 'Fenix',
71
78
  'FOS' => 'Firefox OS',
72
79
  'FIR' => 'Fire OS',
80
+ 'FOR' => 'Foresight Linux',
73
81
  'FRE' => 'Freebox',
74
82
  'BSD' => 'FreeBSD',
75
83
  'FYD' => 'FydeOS',
84
+ 'FUC' => 'Fuchsia',
76
85
  'GNT' => 'Gentoo',
77
86
  'GRI' => 'GridOS',
78
87
  'GTV' => 'Google TV',
@@ -85,9 +94,14 @@ class DeviceDetector
85
94
  'INF' => 'Inferno',
86
95
  'JME' => 'Java ME',
87
96
  'KOS' => 'KaiOS',
97
+ 'KAN' => 'Kanotix',
88
98
  'KNO' => 'Knoppix',
99
+ 'KTV' => 'KreaTV',
89
100
  'KBT' => 'Kubuntu',
90
101
  'LIN' => 'GNU/Linux',
102
+ 'LND' => 'LindowsOS',
103
+ 'LNS' => 'Linspire',
104
+ 'LEN' => 'Lineage OS',
91
105
  'LBT' => 'Lubuntu',
92
106
  'LOS' => 'Lumin OS',
93
107
  'VLN' => 'VectorLinux',
@@ -97,6 +111,7 @@ class DeviceDetector
97
111
  'MDR' => 'Mandriva',
98
112
  'SMG' => 'MeeGo',
99
113
  'MCD' => 'MocorDroid',
114
+ 'MON' => 'moonOS',
100
115
  'MIN' => 'Mint',
101
116
  'MLD' => 'MildWild',
102
117
  'MOR' => 'MorphOS',
@@ -105,25 +120,34 @@ class DeviceDetector
105
120
  'MRE' => 'MRE',
106
121
  'WII' => 'Nintendo',
107
122
  'NDS' => 'Nintendo Mobile',
123
+ 'NOV' => 'Nova',
108
124
  'OS2' => 'OS/2',
109
125
  'T64' => 'OSF1',
110
126
  'OBS' => 'OpenBSD',
111
127
  'OWR' => 'OpenWrt',
128
+ 'OTV' => 'Opera TV',
112
129
  'ORD' => 'Ordissimo',
130
+ 'PAR' => 'Pardus',
113
131
  'PCL' => 'PCLinuxOS',
132
+ 'PLA' => 'Plasma Mobile',
114
133
  'PSP' => 'PlayStation Portable',
115
134
  'PS3' => 'PlayStation',
135
+ 'PUR' => 'PureOS',
116
136
  'RHT' => 'Red Hat',
137
+ 'REV' => 'Revenge OS',
117
138
  'ROS' => 'RISC OS',
118
139
  'ROK' => 'Roku OS',
119
140
  'RSO' => 'Rosa',
141
+ 'ROU' => 'RouterOS',
120
142
  'REM' => 'Remix OS',
143
+ 'RRS' => 'Resurrection Remix OS',
121
144
  'REX' => 'REX',
122
145
  'RZD' => 'RazoDroiD',
123
146
  'SAB' => 'Sabayon',
124
147
  'SSE' => 'SUSE',
125
148
  'SAF' => 'Sailfish OS',
126
149
  'SEE' => 'SeewoOS',
150
+ 'SIR' => 'Sirin OS',
127
151
  'SLW' => 'Slackware',
128
152
  'SOS' => 'Solaris',
129
153
  'SYL' => 'Syllable',
@@ -132,6 +156,7 @@ class DeviceDetector
132
156
  'S40' => 'Symbian OS Series 40',
133
157
  'S60' => 'Symbian OS Series 60',
134
158
  'SY3' => 'Symbian^3',
159
+ 'TEN' => 'TencentOS',
135
160
  'TDX' => 'ThreadX',
136
161
  'TIZ' => 'Tizen',
137
162
  'TOS' => 'TmaxOS',
@@ -148,6 +173,8 @@ class DeviceDetector
148
173
  'XBX' => 'Xbox',
149
174
  'XBT' => 'Xubuntu',
150
175
  'YNS' => 'YunOS',
176
+ 'ZEN' => 'Zenwalk',
177
+ 'ZOR' => 'ZorinOS',
151
178
  'IOS' => 'iOS',
152
179
  'POS' => 'palmOS',
153
180
  'WOS' => 'webOS'
@@ -158,12 +185,13 @@ class DeviceDetector
158
185
  end.freeze
159
186
 
160
187
  OS_FAMILIES = {
161
- 'Android' => %w[AND CYN FIR REM RZD MLD MCD YNS GRI HAR],
188
+ 'Android' => %w[ AND CYN FIR REM RZD MLD MCD YNS GRI HAR
189
+ ADR CLR BOS REV LEN SIR RRS],
162
190
  'AmigaOS' => %w[AMG MOR],
163
191
  'BlackBerry' => %w[BLB QNX],
164
192
  'Brew' => ['BMP'],
165
193
  'BeOS' => %w[BEO HAI],
166
- 'Chrome OS' => %w[COS FYD SEE],
194
+ 'Chrome OS' => %w[COS CRS FYD SEE],
167
195
  'Firefox OS' => %w[FOS KOS],
168
196
  'Gaming Console' => %w[WII PS3],
169
197
  'Google TV' => ['GTV'],
@@ -174,7 +202,9 @@ class DeviceDetector
174
202
  LIN ARL DEB KNO MIN UBT KBT XBT LBT FED
175
203
  RHT VLN MDR GNT SAB SLW SSE CES BTR SAF
176
204
  ORD TOS RSO DEE FRE MAG FEN CAI PCL HAS
177
- LOS DVK ROK OWR
205
+ LOS DVK ROK OWR OTV KTV PUR PLA FUC PAR
206
+ FOR MON KAN ZEN LND LNS CHN AMZ TEN CST
207
+ NOV ROU ZOR
178
208
  ],
179
209
  'Mac' => ['MAC'],
180
210
  'Mobile Gaming Console' => %w[PSP NDS XBX],
@@ -58,7 +58,29 @@ class DeviceDetector
58
58
  end
59
59
 
60
60
  def load_regexes(file_paths)
61
- file_paths.map { |path, full_path| [path, symbolize_keys!(YAML.load_file(full_path))] }
61
+ file_paths.map do |path, full_path|
62
+ object = YAML.load_file(full_path)
63
+ object = rewrite_device_object!(object) if is_device_yml_file?(full_path)
64
+ object = rewrite_vendor_object!(object) if is_vendor_yml_file?(full_path)
65
+
66
+ [path, symbolize_keys!(object)]
67
+ end
68
+ end
69
+
70
+ def is_device_yml_file?(file_path)
71
+ file_path.include?('/regexes/device/')
72
+ end
73
+
74
+ def is_vendor_yml_file?(file_path)
75
+ file_path.include?('/regexes/vendorfragments')
76
+ end
77
+
78
+ def rewrite_vendor_object!(object)
79
+ object.map { |key, values| values.map { |v| { 'regex_name' => key, 'regex' => v } } }.flatten
80
+ end
81
+
82
+ def rewrite_device_object!(object)
83
+ object.map { |key, value| [key, { 'regex_name' => key }.merge!(value)] }.to_h
62
84
  end
63
85
 
64
86
  def symbolize_keys!(object)
@@ -88,8 +110,8 @@ class DeviceDetector
88
110
  Regexp.new('(?:^|[^A-Z0-9\-_]|[^A-Z0-9\-]_|sprd-|MZ-)(?:' + src + ')', Regexp::IGNORECASE)
89
111
  end
90
112
 
91
- def from_cache(key)
92
- DeviceDetector.cache.get_or_set(key) { yield }
113
+ def from_cache(key, &block)
114
+ DeviceDetector.cache.get_or_set(key, &block)
93
115
  end
94
116
  end
95
117
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ class DeviceDetector
6
+ class VendorFragment < Parser
7
+ def name
8
+ vendor_fragment_info
9
+ end
10
+
11
+ private
12
+
13
+ def vendor_fragment_info
14
+ from_cache(['vendor_fragment', self.class.name, user_agent]) do
15
+ return if regex_meta.nil? || regex_meta.empty?
16
+
17
+ regex_meta[:regex_name]
18
+ end
19
+ end
20
+
21
+ def filenames
22
+ ['vendorfragments.yml']
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class DeviceDetector
4
- VERSION = '1.0.7'
4
+ VERSION = '1.1.1'
5
5
  end
@@ -14,42 +14,78 @@ require 'device_detector/client'
14
14
  require 'device_detector/device'
15
15
  require 'device_detector/os'
16
16
  require 'device_detector/browser'
17
+ require 'device_detector/client_hint'
18
+ require 'device_detector/vendor_fragment'
17
19
 
18
20
  class DeviceDetector
19
- attr_reader :user_agent
21
+ attr_reader :client_hint, :user_agent
20
22
 
21
- def initialize(user_agent)
22
- @user_agent = user_agent
23
+ def initialize(user_agent, headers = nil)
24
+ @client_hint = ClientHint.new(headers)
25
+ utf8_user_agent = encode_user_agent_if_needed(user_agent)
26
+ @user_agent = set_user_agent(utf8_user_agent)
27
+ end
28
+
29
+ # https://github.com/matomo-org/device-detector/blob/c235832dba13961ab0f71b681616baf1aa48de23/Parser/Device/AbstractDeviceParser.php#L1873
30
+ def set_user_agent(user_agent)
31
+ return user_agent if client_hint.model.nil?
32
+
33
+ regex = build_regex('Android 10[.\d]*; K(?: Build/|[;)])')
34
+ return user_agent unless user_agent =~ regex
35
+
36
+ version = client_hint.os_version || '10'
37
+
38
+ user_agent.gsub(regex, "Android #{version}, #{client_hint.model}")
39
+ end
40
+
41
+ def encode_user_agent_if_needed(user_agent)
42
+ return if user_agent.nil?
43
+ return user_agent if user_agent.encoding.name == 'UTF-8'
44
+
45
+ user_agent.encode('utf-8', 'binary', undef: :replace)
23
46
  end
24
47
 
25
48
  def name
26
- client.name
49
+ return client.name if mobile_fix?
50
+
51
+ client_hint.browser_name || client.name
27
52
  end
28
53
 
29
54
  def full_version
30
- client.full_version
55
+ client_hint.platform_version || client.full_version
31
56
  end
32
57
 
33
58
  def os_family
34
- os.family
59
+ return 'GNU/Linux' if linux_fix?
60
+
61
+ client_hint.os_family || os.family || client_hint.platform
35
62
  end
36
63
 
37
64
  def os_name
38
- os.name
65
+ return 'GNU/Linux' if linux_fix?
66
+
67
+ client_hint.os_name || os.name || client_hint.platform
39
68
  end
40
69
 
41
70
  def os_full_version
42
- os.full_version
71
+ return if skip_os_version?
72
+
73
+ client_hint.os_version || os.full_version
43
74
  end
44
75
 
45
76
  def device_name
46
- device.name
77
+ return if fake_ua?
78
+
79
+ device.name || client_hint.model || fix_for_x_music
47
80
  end
48
81
 
49
82
  def device_brand
83
+ return if fake_ua?
84
+
50
85
  # Assume all devices running iOS / Mac OS are from Apple
51
86
  brand = device.brand
52
87
  brand = 'Apple' if brand.nil? && ['Apple TV', 'iOS', 'Mac'].include?(os_name)
88
+
53
89
  brand
54
90
  end
55
91
 
@@ -62,7 +98,7 @@ class DeviceDetector
62
98
  # Note: We do not check for browser (family) here, as there might be mobile apps using Chrome,
63
99
  # that won't have a detected browser, but can still be detected. So we check the useragent for
64
100
  # Chrome instead.
65
- if t.nil? && os.family == 'Android' && user_agent =~ build_regex('Chrome\/[\.0-9]*')
101
+ if t.nil? && os_family == 'Android' && user_agent =~ build_regex('Chrome\/[\.0-9]*')
66
102
  if user_agent =~ build_regex('(?:Mobile|eliboM) Safari\/')
67
103
  t = 'smartphone'
68
104
  elsif user_agent =~ build_regex('(?!Mobile )Safari\/')
@@ -97,7 +133,7 @@ class DeviceDetector
97
133
  end
98
134
 
99
135
  # All detected feature phones running android are more likely a smartphone
100
- t = 'smartphone' if t == 'feature phone' && os.family == 'Android'
136
+ t = 'smartphone' if t == 'feature phone' && os_family == 'Android'
101
137
 
102
138
  # All unknown devices under running Java ME are more likely a features phones
103
139
  t = 'feature phone' if t.nil? && os_name == 'Java ME'
@@ -111,19 +147,25 @@ class DeviceDetector
111
147
  # assume that all Windows 8 touch devices are tablets.
112
148
  if t.nil? && touch_enabled? &&
113
149
  (os_name == 'Windows RT' ||
114
- (os_name == 'Windows' && os.full_version &&
115
- Gem::Version.new(os.full_version) >= VersionExtractor::MAJOR_VERSION_8))
150
+ (os_name == 'Windows' && os_full_version &&
151
+ Gem::Version.new(os_full_version) >= VersionExtractor::MAJOR_VERSION_8))
116
152
  t = 'tablet'
117
153
  end
118
154
 
119
155
  # All devices running Opera TV Store are assumed to be a tv
120
156
  t = 'tv' if opera_tv_store?
121
157
 
158
+ # All devices that contain Andr0id in string are assumed to be a tv
159
+ t = 'tv' if user_agent =~ build_regex('Andr0id|Android TV')
160
+
122
161
  # All devices running Tizen TV or SmartTV are assumed to be a tv
123
162
  t = 'tv' if t.nil? && tizen_samsung_tv?
124
163
 
125
164
  # Devices running Kylo or Espital TV Browsers are assumed to be a TV
126
- t = 'tv' if t.nil? && ['Kylo', 'Espial TV Browser'].include?(client.name)
165
+ t = 'tv' if t.nil? && ['Kylo', 'Espial TV Browser'].include?(name)
166
+
167
+ # All devices containing TV fragment are assumed to be a tv
168
+ t = 'tv' if t.nil? && user_agent =~ build_regex('\(TV;')
127
169
 
128
170
  has_desktop = t != 'desktop' && desktop_string? && desktop_fragment?
129
171
  t = 'desktop' if has_desktop
@@ -190,12 +232,36 @@ class DeviceDetector
190
232
  @os ||= OS.new(user_agent)
191
233
  end
192
234
 
235
+ # https://github.com/matomo-org/device-detector/blob/827a3fab7e38c3274c18d2f5f5bc2a78b7ef4a3a/DeviceDetector.php#L921C5-L921C5
236
+ def fake_ua?
237
+ os_name == 'Android' && device.brand == 'Apple'
238
+ end
239
+
240
+ # https://github.com/matomo-org/device-detector/blob/be1c9ef486c247dc4886668da5ed0b1c49d90ba8/Parser/Client/Browser.php#L772
241
+ # Fix mobile browser names e.g. Chrome => Chrome Mobile
242
+ def mobile_fix?
243
+ client.name == "#{client_hint.browser_name} Mobile"
244
+ end
245
+
246
+ def linux_fix?
247
+ client_hint.platform == 'Linux' && os.name == 'Android' && client_hint.mobile == '?0'
248
+ end
249
+
250
+ # Related to issue mentionned in device.rb#1562
251
+ def fix_for_x_music
252
+ user_agent&.include?('X-music Ⅲ') ? 'X-Music III' : nil
253
+ end
254
+
255
+ def skip_os_version?
256
+ !client_hint.os_family.nil? && client_hint.os_family != os.family
257
+ end
258
+
193
259
  def android_tablet_fragment?
194
- user_agent =~ build_regex('Android(?: \d.\d(?:.\d)?)?; Tablet;')
260
+ user_agent =~ build_regex('Android( [\.0-9]+)?; Tablet;')
195
261
  end
196
262
 
197
263
  def android_mobile_fragment?
198
- user_agent =~ build_regex('Android(?: \d.\d(?:.\d)?)?; Mobile;')
264
+ user_agent =~ build_regex('Android( [\.0-9]+)?; Mobile;')
199
265
  end
200
266
 
201
267
  def desktop_fragment?