handset_detection 0.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.
- checksums.yaml +7 -0
- data/lib/handset_detection/base.rb +554 -0
- data/lib/handset_detection/cache/filesystem.rb +87 -0
- data/lib/handset_detection/cache/memcached.rb +70 -0
- data/lib/handset_detection/cache/none.rb +60 -0
- data/lib/handset_detection/cache/rails.rb +59 -0
- data/lib/handset_detection/cache.rb +105 -0
- data/lib/handset_detection/device.rb +667 -0
- data/lib/handset_detection/extra.rb +221 -0
- data/lib/handset_detection/hd4.rb +458 -0
- data/lib/handset_detection/store.rb +178 -0
- data/lib/handset_detection.rb +27 -0
- metadata +54 -0
@@ -0,0 +1,667 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) Richard Uren 2016 <richard@teleport.com.au>
|
3
|
+
# All Rights Reserved
|
4
|
+
#
|
5
|
+
# LICENSE: Redistribution and use in source and binary forms, with or
|
6
|
+
# without modification, are permitted provided that the following
|
7
|
+
# conditions are met: Redistributions of source code must retain the
|
8
|
+
# above copyright notice, this list of conditions and the following
|
9
|
+
# disclaimer. Redistributions in binary form must reproduce the above
|
10
|
+
# copyright notice, this list of conditions and the following disclaimer
|
11
|
+
# in the documentation and/or other materials provided with the
|
12
|
+
# distribution.
|
13
|
+
#
|
14
|
+
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
15
|
+
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
16
|
+
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
|
17
|
+
# NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
18
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
19
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
20
|
+
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
21
|
+
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
22
|
+
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
23
|
+
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
24
|
+
# DAMAGE.
|
25
|
+
#++
|
26
|
+
|
27
|
+
require 'handset_detection/base'
|
28
|
+
require 'handset_detection/store'
|
29
|
+
require 'handset_detection/extra'
|
30
|
+
|
31
|
+
class Device < Base
|
32
|
+
def initialize(config={})
|
33
|
+
@device = nil
|
34
|
+
@platform = nil
|
35
|
+
@browser = nil
|
36
|
+
@app = nil
|
37
|
+
@rating_result = nil
|
38
|
+
@store = nil
|
39
|
+
@extra = nil
|
40
|
+
@config = nil
|
41
|
+
super()
|
42
|
+
set_config config
|
43
|
+
@device_headers = {}
|
44
|
+
@extra_headers = {}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set Config sets config vars
|
48
|
+
#
|
49
|
+
# +param+ hash config A config assoc array.
|
50
|
+
# +return+ true on success, false otherwise
|
51
|
+
#
|
52
|
+
def set_config(config)
|
53
|
+
config.each do |key, value|
|
54
|
+
@config[key] = value
|
55
|
+
end
|
56
|
+
@store = Store::get_instance
|
57
|
+
@store.set_config @config
|
58
|
+
@extra = Extra.new config
|
59
|
+
end
|
60
|
+
|
61
|
+
# Find all device vendors
|
62
|
+
#
|
63
|
+
# +param+ void
|
64
|
+
# +return+ bool true on success, false otherwise. Use getReply to inspect results on success.
|
65
|
+
#
|
66
|
+
def local_vendors
|
67
|
+
@reply = {}
|
68
|
+
data = fetch_devices
|
69
|
+
return false if data.blank?
|
70
|
+
|
71
|
+
tmp = []
|
72
|
+
data['devices'].each do |item|
|
73
|
+
tmp << item['Device']['hd_specs']['general_vendor']
|
74
|
+
end
|
75
|
+
@reply['vendor'] = tmp.uniq
|
76
|
+
@reply['vendor'].sort!
|
77
|
+
set_error 0, 'OK'
|
78
|
+
end
|
79
|
+
|
80
|
+
# Find all models for the sepecified vendor
|
81
|
+
#
|
82
|
+
# +param+ string vendor The device vendor
|
83
|
+
# +return+ bool true on success, false otherwise. Use getReply to inspect results on success.
|
84
|
+
#
|
85
|
+
def local_models(vendor)
|
86
|
+
@reply = {}
|
87
|
+
data = fetch_devices
|
88
|
+
return false if data.blank?
|
89
|
+
|
90
|
+
vendor = vendor.downcase
|
91
|
+
tmp = []
|
92
|
+
data['devices'].each do |item|
|
93
|
+
if vendor == item['Device']['hd_specs']['general_vendor'].downcase
|
94
|
+
tmp << item['Device']['hd_specs']['general_model']
|
95
|
+
end
|
96
|
+
key = vendor + " "
|
97
|
+
unless item['Device']['hd_specs']['general_aliases'].blank?
|
98
|
+
item['Device']['hd_specs']['general_aliases'].each do |alias_item|
|
99
|
+
result = alias_item.downcase.index(key.downcase)
|
100
|
+
tmp << alias_item.sub(key, '') if result === 0
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
tmp.sort!
|
105
|
+
@reply['model'] = tmp.uniq
|
106
|
+
set_error 0, 'OK'
|
107
|
+
end
|
108
|
+
|
109
|
+
# Finds all the specs for a specific device
|
110
|
+
#
|
111
|
+
# +param+ string vendor The device vendor
|
112
|
+
# +param+ string model The device model
|
113
|
+
# +return+ bool true on success, false otherwise. Use getReply to inspect results on success.
|
114
|
+
#
|
115
|
+
def local_view(vendor, model)
|
116
|
+
@reply = {}
|
117
|
+
data = fetch_devices
|
118
|
+
return false if data.blank?
|
119
|
+
|
120
|
+
vendor = vendor.downcase
|
121
|
+
model = model.downcase
|
122
|
+
data['devices'].each do |item|
|
123
|
+
if vendor == item['Device']['hd_specs']['general_vendor'].downcase and model == item['Device']['hd_specs']['general_model'].downcase
|
124
|
+
@reply['device'] = item['Device']['hd_specs']
|
125
|
+
return set_error 0, 'OK'
|
126
|
+
end
|
127
|
+
end
|
128
|
+
set_error 301, 'Nothing found'
|
129
|
+
end
|
130
|
+
|
131
|
+
# Finds all devices that have a specific property
|
132
|
+
#
|
133
|
+
# +param+ string key
|
134
|
+
# +param+ string value
|
135
|
+
# +return+ bool true on success, false otherwise. Use getReply to inspect results on success.
|
136
|
+
#
|
137
|
+
def local_what_has(key, value)
|
138
|
+
data = fetch_devices
|
139
|
+
return false if data.blank?
|
140
|
+
|
141
|
+
tmp = []
|
142
|
+
value = value.downcase
|
143
|
+
data['devices'].each do |item|
|
144
|
+
next if item['Device']['hd_specs'][key].blank?
|
145
|
+
match = false
|
146
|
+
if item['Device']['hd_specs'][key].is_a? Array
|
147
|
+
item['Device']['hd_specs'][key].each do |check|
|
148
|
+
if check.downcase.include? value.downcase
|
149
|
+
match = true
|
150
|
+
end
|
151
|
+
end
|
152
|
+
elsif item['Device']['hd_specs'][key].downcase.include? value.downcase
|
153
|
+
match = true
|
154
|
+
end
|
155
|
+
|
156
|
+
if match
|
157
|
+
tmp << { 'id' => item['Device']['_id'],
|
158
|
+
'general_vendor' => item['Device']['hd_specs']['general_vendor'],
|
159
|
+
'general_model' => item['Device']['hd_specs']['general_model'] }
|
160
|
+
end
|
161
|
+
end
|
162
|
+
@reply['devices'] = tmp
|
163
|
+
set_error 0, 'OK'
|
164
|
+
end
|
165
|
+
|
166
|
+
# Perform a local detection
|
167
|
+
#
|
168
|
+
# +param+ array headers HTTP headers as an assoc array. keys are standard http header names eg user-agent, x-wap-profile
|
169
|
+
# +return+ bool true on success, false otherwise
|
170
|
+
#
|
171
|
+
def local_detect(h)
|
172
|
+
headers = {}
|
173
|
+
# lowercase headers on the way in.
|
174
|
+
h.each {|k, v| headers[k.downcase] = v}
|
175
|
+
hardware_info = headers['x-local-hardwareinfo']
|
176
|
+
headers.delete('x-local-hardwareinfo')
|
177
|
+
|
178
|
+
# Is this a native detection or a HTTP detection ?
|
179
|
+
if has_bi_keys headers
|
180
|
+
return v4_match_build_info headers
|
181
|
+
end
|
182
|
+
v4_match_http_headers headers, hardware_info
|
183
|
+
end
|
184
|
+
|
185
|
+
# Returns the rating score for a device based on the passed values
|
186
|
+
#
|
187
|
+
# +param+ string deviceId : The ID of the device to check.
|
188
|
+
# +param+ hash props Properties extracted from the device (display_x, display_y etc .. )
|
189
|
+
# +return+ array of rating information. (which includes 'score' which is an int value that is a percentage.)
|
190
|
+
#
|
191
|
+
def find_rating(device_id, props)
|
192
|
+
device = find_by_id(device_id)
|
193
|
+
return nil if device['Device']['hd_specs'].blank?
|
194
|
+
|
195
|
+
specs = device['Device']['hd_specs']
|
196
|
+
|
197
|
+
total = 70
|
198
|
+
result = {}
|
199
|
+
|
200
|
+
# Display Resolution - Worth 40 points if correct
|
201
|
+
result['resolution'] = 0
|
202
|
+
unless props['display_x'].blank? or props['display_y'].blank?
|
203
|
+
p_max_res = [props['display_x'], props['display_y']].max.to_i
|
204
|
+
p_min_res = [props['display_x'], props['display_y']].min.to_i
|
205
|
+
s_max_res = [specs['display_x'], specs['display_y']].max.to_i
|
206
|
+
s_min_res = [specs['display_x'], specs['display_y']].min.to_i
|
207
|
+
if p_max_res == s_max_res and p_min_res == s_min_res
|
208
|
+
# Check for native match first
|
209
|
+
result['resolution'] = 40
|
210
|
+
else
|
211
|
+
# Check for css dimensions match.
|
212
|
+
# css dimensions should be display_[xy] / display_pixel_ratio or others in other modes.
|
213
|
+
# Devices can have multiple css display modes (eg. iPhone 6, iPhone 6+ Zoom mode)
|
214
|
+
css_screen_sizes = specs['display_css_screen_sizes'].blank? ? [] : specs['display_css_screen_sizes']
|
215
|
+
css_screen_sizes.each do |size|
|
216
|
+
dimensions = size.split('x')
|
217
|
+
tmp_max_res = dimensions.max.to_i
|
218
|
+
tmp_min_res = dimensions.min.to_i
|
219
|
+
if p_max_res == tmp_max_res and p_min_res == tmp_min_res
|
220
|
+
result['resolution'] = 40
|
221
|
+
break
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Display pixel ratio - 20 points
|
228
|
+
result['display_pixel_ratio'] = 20
|
229
|
+
unless props['display_pixel_ratio'].blank?
|
230
|
+
# Note : display_pixel_ratio will be a string stored as 1.33 or 1.5 or 2, perhaps 2.0 ..
|
231
|
+
if specs['display_pixel_ratio'].to_f.round(2) == (props['display_pixel_ratio'] / 100.to_f).round(2)
|
232
|
+
result['display_pixel_ratio'] = 40
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Benchmark - 10 points - Enough to tie break but not enough to overrule display or pixel ratio.
|
237
|
+
result['benchmark'] = 0
|
238
|
+
unless props['benchmark'].blank?
|
239
|
+
unless specs['benchmark_min'].blank? or specs['benchmark_max'].blank?
|
240
|
+
if props['benchmark'].to_i >= specs['benchmark_min'].to_i and props['benchmark'].to_i <= specs['benchmark_max'].to_i
|
241
|
+
# Inside range
|
242
|
+
result['benchmark'] = 10
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
result['score'] = result.values.inject(0){ |sum, x| sum + x }.to_i
|
248
|
+
result['possible'] = total
|
249
|
+
result['_id'] = device_id
|
250
|
+
|
251
|
+
# Distance from mean used in tie breaking situations if two devices have the same score.
|
252
|
+
result['distance'] = 100000
|
253
|
+
unless specs['benchmark_min'].blank? or specs['benchmark_max'].blank? or props['benchmark'].blank?
|
254
|
+
result['distance'] = ((specs['benchmark_min'] + specs['benchmark_max']) / 2 - props['benchmark']).abs.to_i
|
255
|
+
end
|
256
|
+
result
|
257
|
+
end
|
258
|
+
|
259
|
+
# Overlays specs onto a device
|
260
|
+
#
|
261
|
+
# +param+ string specsField : Either 'platform', 'browser', 'language'
|
262
|
+
# +return+ void
|
263
|
+
#
|
264
|
+
def specs_overlay(specs_field, device, specs)
|
265
|
+
if specs.include? 'hd_specs'
|
266
|
+
if specs_field == 'platform'
|
267
|
+
unless specs['hd_specs']['general_platform'].blank?
|
268
|
+
device['Device']['hd_specs']['general_platform'] = specs['hd_specs']['general_platform']
|
269
|
+
device['Device']['hd_specs']['general_platform_version'] = specs['hd_specs']['general_platform_version']
|
270
|
+
end
|
271
|
+
elsif specs_field == 'browser'
|
272
|
+
unless specs['hd_specs']['general_browser'].blank?
|
273
|
+
device['Device']['hd_specs']['general_browser'] = specs['hd_specs']['general_browser']
|
274
|
+
device['Device']['hd_specs']['general_browser_version'] = specs['hd_specs']['general_browser_version']
|
275
|
+
end
|
276
|
+
elsif specs_field == 'app'
|
277
|
+
unless specs['hd_specs']['general_app'].blank?
|
278
|
+
device['Device']['hd_specs']['general_app'] = specs['hd_specs']['general_app']
|
279
|
+
device['Device']['hd_specs']['general_app_version'] = specs['hd_specs']['general_app_version']
|
280
|
+
device['Device']['hd_specs']['general_app_category'] = specs['hd_specs']['general_app_category']
|
281
|
+
end
|
282
|
+
elsif specs_field == 'language'
|
283
|
+
unless specs['hd_specs']['general_language'].blank?
|
284
|
+
device['Device']['hd_specs']['general_language'] = specs['hd_specs']['general_language']
|
285
|
+
device['Device']['hd_specs']['general_language_full'] = specs['hd_specs']['general_language_full']
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
device
|
290
|
+
end
|
291
|
+
|
292
|
+
# Takes a string of onDeviceInformation and turns it into something that can be used for high accuracy checking.
|
293
|
+
#
|
294
|
+
# Strings a usually generated from cookies, but may also be supplied in headers.
|
295
|
+
# The format is w;h;r;b where w is the display width, h is the display height, r is the pixel ratio and b is the benchmark.
|
296
|
+
# display_x, display_y, display_pixel_ratio, general_benchmark
|
297
|
+
#
|
298
|
+
# +param+ string hardwareInfo String of light weight device property information, separated by ':'
|
299
|
+
# +return+ array partial specs array of information we can use to improve detection accuracy
|
300
|
+
#
|
301
|
+
def info_string_to_hash(hardware_info)
|
302
|
+
# Remove the header or cookie name from the string 'x-specs1a='
|
303
|
+
if hardware_info.include?('=')
|
304
|
+
tmp = hardware_info.split('=')
|
305
|
+
if tmp[1].blank?
|
306
|
+
return {}
|
307
|
+
else
|
308
|
+
hardware_info = tmp[1]
|
309
|
+
end
|
310
|
+
end
|
311
|
+
reply = {}
|
312
|
+
info = hardware_info.split(':')
|
313
|
+
return {} if info.length != 4
|
314
|
+
reply['display_x'] = info[0].strip.to_i
|
315
|
+
reply['display_y'] = info[1].strip.to_i
|
316
|
+
reply['display_pixel_ratio'] = info[2].strip.to_i
|
317
|
+
reply['benchmark'] = info[3].strip.to_i
|
318
|
+
reply
|
319
|
+
end
|
320
|
+
|
321
|
+
# Overlays hardware info onto a device - Used in generic replys
|
322
|
+
#
|
323
|
+
# +param+ hash device
|
324
|
+
# +param+ hardwareInfo
|
325
|
+
# +return+ void
|
326
|
+
#
|
327
|
+
def hardware_info_overlay(device, info)
|
328
|
+
unless info.blank?
|
329
|
+
device['Device']['hd_specs']['display_x'] = info['display_x'] unless info['display_x'].blank?
|
330
|
+
device['Device']['hd_specs']['display_y'] = info['display_y'] unless info['display_y'].blank?
|
331
|
+
device['Device']['hd_specs']['display_pixel_ratio'] = info['display_pixel_ratio'] unless info['display_pixel_ratio'].blank?
|
332
|
+
end
|
333
|
+
device
|
334
|
+
end
|
335
|
+
|
336
|
+
# Device matching
|
337
|
+
#
|
338
|
+
# Plan of attack :
|
339
|
+
# 1) Look for opera headers first - as they're definitive
|
340
|
+
# 2) Try profile match - only devices which have unique profiles will match.
|
341
|
+
# 3) Try user-agent match
|
342
|
+
# 4) Try other x-headers
|
343
|
+
# 5) Try all remaining headers
|
344
|
+
#
|
345
|
+
# +param+ void
|
346
|
+
# +return+ array The matched device or null if not found
|
347
|
+
#
|
348
|
+
def match_device(headers)
|
349
|
+
# Remember the agent for generic matching later.
|
350
|
+
agent = ""
|
351
|
+
# Opera mini sometimes puts the vendor # model in the header - nice! ... sometimes it puts ? # ? in as well
|
352
|
+
if (not headers['x-operamini-phone'].blank?) and headers['x-operamini-phone'].strip != "? # ?"
|
353
|
+
_id = get_match 'x-operamini-phone', headers['x-operamini-phone'], DETECTIONV4_STANDARD, 'x-operamini-phone', 'device'
|
354
|
+
return find_by_id(_id) if _id
|
355
|
+
agent = headers['x-operamini-phone']
|
356
|
+
headers.delete('x-operamini-phone')
|
357
|
+
end
|
358
|
+
|
359
|
+
# Profile header matching
|
360
|
+
unless headers['profile'].blank?
|
361
|
+
_id = get_match 'profile', headers['profile'], DETECTIONV4_STANDARD, 'profile', 'device'
|
362
|
+
return find_by_id _id if _id
|
363
|
+
headers.delete('profile')
|
364
|
+
end
|
365
|
+
|
366
|
+
# Profile header matching
|
367
|
+
unless headers['x-wap-profile'].blank?
|
368
|
+
_id = get_match 'profile', headers['x-wap-profile'], DETECTIONV4_STANDARD, 'x-wap-profile', 'device'
|
369
|
+
return find_by_id _id if _id
|
370
|
+
headers.delete('x-wap-profile')
|
371
|
+
end
|
372
|
+
|
373
|
+
# Match nominated headers ahead of x- headers
|
374
|
+
order = @detection_config['device-ua-order']
|
375
|
+
headers.each do |key, value|
|
376
|
+
order << key if (not order.include? key) and /^x-/i.match key
|
377
|
+
end
|
378
|
+
|
379
|
+
order.each do |item|
|
380
|
+
unless headers[item].blank?
|
381
|
+
# log "Trying user-agent match on header #{item}"
|
382
|
+
_id = get_match 'user-agent', headers[item], DETECTIONV4_STANDARD, item, 'device'
|
383
|
+
return find_by_id _id if _id
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
# Generic matching - Match of last resort
|
388
|
+
# log('Trying Generic Match')
|
389
|
+
|
390
|
+
if headers.include? 'x-operamini-phone-ua'
|
391
|
+
_id = get_match 'user-agent', headers['x-operamini-phone-ua'], DETECTIONV4_GENERIC, 'agent', 'device'
|
392
|
+
end
|
393
|
+
if _id.blank? and headers.include? 'agent'
|
394
|
+
_id =get_match 'user-agent', headers['agent'], DETECTIONV4_GENERIC, 'agent', 'device'
|
395
|
+
end
|
396
|
+
if _id.blank? and headers.include? 'user-agent'
|
397
|
+
_id = get_match 'user-agent', headers['user-agent'], DETECTIONV4_GENERIC, 'agent', 'device'
|
398
|
+
end
|
399
|
+
|
400
|
+
return find_by_id _id unless _id.blank?
|
401
|
+
false
|
402
|
+
end
|
403
|
+
|
404
|
+
# Find a device by its id
|
405
|
+
#
|
406
|
+
# +param+ string _id
|
407
|
+
# +return+ hash device on success, false otherwise
|
408
|
+
#
|
409
|
+
def find_by_id(_id)
|
410
|
+
@store.read("Device_#{_id}")
|
411
|
+
end
|
412
|
+
|
413
|
+
# Internal helper for building a list of all devices.
|
414
|
+
#
|
415
|
+
# +param+ void
|
416
|
+
# +return+ array List of all devices.
|
417
|
+
#
|
418
|
+
def fetch_devices
|
419
|
+
result = @store.fetch_devices
|
420
|
+
unless result
|
421
|
+
return set_error 299, "Error : fetchDevices cannot read files from store."
|
422
|
+
end
|
423
|
+
result
|
424
|
+
end
|
425
|
+
|
426
|
+
# BuildInfo Matching
|
427
|
+
#
|
428
|
+
# Takes a set of buildInfo key/value pairs & works out what the device is
|
429
|
+
#
|
430
|
+
# +param+ hash buildInfo - Buildinfo key/value array
|
431
|
+
# +return+ mixed device array on success, false otherwise
|
432
|
+
#
|
433
|
+
def v4_match_build_info(build_info)
|
434
|
+
@device = nil
|
435
|
+
@platform = nil
|
436
|
+
@browser = nil
|
437
|
+
@app = nil
|
438
|
+
@detected_rule_key = nil
|
439
|
+
@rating_result = nil
|
440
|
+
@reply = {}
|
441
|
+
|
442
|
+
# Nothing to check
|
443
|
+
return false if build_info.blank?
|
444
|
+
|
445
|
+
@build_info = build_info
|
446
|
+
|
447
|
+
# Device Detection
|
448
|
+
@device = v4_match_bi_helper build_info, 'device'
|
449
|
+
return false if @device.blank?
|
450
|
+
|
451
|
+
# Platform Detection
|
452
|
+
@platform = v4_match_bi_helper build_info, 'platform'
|
453
|
+
unless @platform.blank?
|
454
|
+
@device = specs_overlay 'platform', @device, @platform
|
455
|
+
end
|
456
|
+
|
457
|
+
@reply['hd_specs'] = @device['Device']['hd_specs']
|
458
|
+
set_error 0, "OK"
|
459
|
+
end
|
460
|
+
|
461
|
+
# buildInfo Match helper - Does the build info match heavy lifting
|
462
|
+
#
|
463
|
+
# +param+ hash buildInfo A buildInfo key/value array
|
464
|
+
# +param+ string category - 'device' or 'platform' (cant match browser or app with buildinfo)
|
465
|
+
# +return+ device or extra on success, false otherwise
|
466
|
+
#
|
467
|
+
def v4_match_bi_helper(build_info, category='device')
|
468
|
+
# ***** Device Detection *****
|
469
|
+
conf_bi_keys = @detection_config["#{category}-bi-order"]
|
470
|
+
return nil if conf_bi_keys.blank? or build_info.blank?
|
471
|
+
|
472
|
+
hints = []
|
473
|
+
conf_bi_keys.each do |platform, set|
|
474
|
+
value = ''
|
475
|
+
set.each do |tuple|
|
476
|
+
checking = true
|
477
|
+
tuple.each do |item|
|
478
|
+
unless build_info.include? item
|
479
|
+
checking = false
|
480
|
+
break
|
481
|
+
else
|
482
|
+
value += '|' + build_info[item]
|
483
|
+
end
|
484
|
+
end
|
485
|
+
if checking
|
486
|
+
value.gsub!(/^[| \t\n\r\0\x0B]*/, '')
|
487
|
+
value.gsub!(/[| \t\n\r\0\x0B]*$/, '')
|
488
|
+
hints << value
|
489
|
+
subtree = (category == 'device') ? DETECTIONV4_STANDARD : category
|
490
|
+
_id = get_match 'buildinfo', value, subtree, 'buildinfo', category
|
491
|
+
unless _id.blank?
|
492
|
+
return (category == 'device') ? (find_by_id _id) : (@extra.find_by_id _id)
|
493
|
+
end
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
# If we get this far then not found, so try generic.
|
498
|
+
platform = has_bi_keys build_info
|
499
|
+
unless platform.blank?
|
500
|
+
try = ["generic|#{platform}", "#{platform}|generic"]
|
501
|
+
try.each do |value|
|
502
|
+
subtree = (category == 'device') ? DETECTIONV4_GENERIC : category
|
503
|
+
_id = get_match 'buildinfo', value, subtree, 'buildinfo', category
|
504
|
+
unless _id.blank?
|
505
|
+
return (category == 'device') ? (find_by_id _id) : (@extra.find_by_id _id)
|
506
|
+
end
|
507
|
+
end
|
508
|
+
end
|
509
|
+
nil
|
510
|
+
end
|
511
|
+
|
512
|
+
# Find the best device match for a given set of headers and optional device properties.
|
513
|
+
#
|
514
|
+
# In 'all' mode all conflicted devces will be returned as a list.
|
515
|
+
# In 'default' mode if there is a conflict then the detected device is returned only (backwards compatible with v3).
|
516
|
+
#
|
517
|
+
# +param+ hash headers Set of sanitized http headers
|
518
|
+
# +param+ string hardwareInfo Information about the hardware
|
519
|
+
# +return+ array device specs. (device.hd_specs)
|
520
|
+
#
|
521
|
+
def v4_match_http_headers(headers, hardware_info=nil)
|
522
|
+
@device = nil
|
523
|
+
@platform = nil
|
524
|
+
@browser = nil
|
525
|
+
@app = nil
|
526
|
+
@rating_result = nil
|
527
|
+
@detected_rule_key = {}
|
528
|
+
@reply = {}
|
529
|
+
hw_props = nil
|
530
|
+
|
531
|
+
# Nothing to check
|
532
|
+
return false if headers.blank?
|
533
|
+
|
534
|
+
headers.delete('ip')
|
535
|
+
headers.delete('host')
|
536
|
+
|
537
|
+
# Sanitize headers & cleanup language
|
538
|
+
headers.each do |key, value|
|
539
|
+
if key == 'accept-language' or key == 'content-language'
|
540
|
+
key = 'language'
|
541
|
+
tmp = value.downcase.gsub(/ /, '').split(/[,;]/)
|
542
|
+
unless tmp[0].blank?
|
543
|
+
value = tmp[0]
|
544
|
+
else
|
545
|
+
next
|
546
|
+
end
|
547
|
+
end
|
548
|
+
@device_headers[key.downcase] = clean_str value
|
549
|
+
@extra_headers[key.downcase] = @extra.extra_clean_str value
|
550
|
+
end
|
551
|
+
|
552
|
+
@device = match_device @device_headers
|
553
|
+
return set_error 301, "Not Found" if @device.blank?
|
554
|
+
|
555
|
+
unless hardware_info.blank?
|
556
|
+
hw_props = info_string_to_hash hardware_info
|
557
|
+
end
|
558
|
+
|
559
|
+
# Stop on detect set - Tidy up and return
|
560
|
+
if @device['Device']['hd_ops']['stop_on_detect'] == 1
|
561
|
+
# Check for hardwareInfo overlay
|
562
|
+
unless @device['Device']['hd_ops']['overlay_result_specs'].blank?
|
563
|
+
@device = hardware_info_overlay(@device, hw_props)
|
564
|
+
end
|
565
|
+
@reply['hd_specs'] = @device['Device']['hd_specs']
|
566
|
+
return set_error 0, "OK"
|
567
|
+
end
|
568
|
+
|
569
|
+
# Get extra info
|
570
|
+
@platform = @extra.match_extra 'platform', @extra_headers
|
571
|
+
@browser = @extra.match_extra 'browser', @extra_headers
|
572
|
+
@app = @extra.match_extra 'app', @extra_headers
|
573
|
+
@language = @extra.match_language @extra_headers
|
574
|
+
|
575
|
+
# Find out if there is any contention on the detected rule.
|
576
|
+
device_list = get_high_accuracy_candidates
|
577
|
+
unless device_list.blank?
|
578
|
+
|
579
|
+
# Resolve contention with OS check
|
580
|
+
@extra.set @platform
|
581
|
+
pass1_list = []
|
582
|
+
device_list.each do |_id|
|
583
|
+
try_device = find_by_id _id
|
584
|
+
if @extra.verify_platform try_device['Device']['hd_specs']
|
585
|
+
pass1_list << _id
|
586
|
+
end
|
587
|
+
end
|
588
|
+
# Contention still not resolved .. check hardware
|
589
|
+
if pass1_list.length >= 2 and (not hw_props.blank?)
|
590
|
+
|
591
|
+
# Score the list based on hardware
|
592
|
+
result = []
|
593
|
+
pass1_list.each do |_id|
|
594
|
+
tmp = find_rating _id, hw_props
|
595
|
+
unless tmp.blank?
|
596
|
+
tmp['_id'] = _id
|
597
|
+
result << tmp
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
# Sort the results
|
602
|
+
result.sort! do |d1, d2| case
|
603
|
+
when d2['score'].to_i - d1['score'].to_i != 0
|
604
|
+
d2['score'].to_i - d1['score'].to_i
|
605
|
+
else
|
606
|
+
d1['distance'].to_i - d2['distance'].to_i
|
607
|
+
end
|
608
|
+
end
|
609
|
+
@rating_result = result
|
610
|
+
# Take the first one
|
611
|
+
if @rating_result[0]['score'] != 0
|
612
|
+
device = find_by_id result[0]['_id']
|
613
|
+
unless device.blank?
|
614
|
+
@device = device
|
615
|
+
end
|
616
|
+
end
|
617
|
+
end
|
618
|
+
end
|
619
|
+
# Overlay specs
|
620
|
+
@device = specs_overlay 'platform', @device, @platform['Extra'] if @platform
|
621
|
+
@device = specs_overlay 'browser', @device, @browser['Extra'] if @browser
|
622
|
+
@device = specs_overlay 'app', @device, @app['Extra'] if @app
|
623
|
+
@device = specs_overlay 'language', @device, @language['Extra'] if @language
|
624
|
+
|
625
|
+
# Overlay hardware info result if required
|
626
|
+
unless @device['Device']['hd_ops']['overlay_result_specs'].blank? or hardware_info.blank?
|
627
|
+
@device = hardware_info_overlay @device, hw_props
|
628
|
+
end
|
629
|
+
|
630
|
+
@reply['hd_specs'] = @device['Device']['hd_specs']
|
631
|
+
set_error 0, "OK"
|
632
|
+
end
|
633
|
+
|
634
|
+
# Determines if high accuracy checks are available on the device which was just detected
|
635
|
+
#
|
636
|
+
# +param+ void
|
637
|
+
# +return+ array, a list of candidate devices which have this detection rule or false otherwise.
|
638
|
+
#
|
639
|
+
def get_high_accuracy_candidates
|
640
|
+
branch = get_branch 'hachecks'
|
641
|
+
rule_key = @detected_rule_key['device']
|
642
|
+
unless branch[rule_key].blank?
|
643
|
+
return branch[rule_key]
|
644
|
+
end
|
645
|
+
false
|
646
|
+
end
|
647
|
+
|
648
|
+
# Determines if hd4Helper would provide more accurate results.
|
649
|
+
#
|
650
|
+
# +param+ hash headers HTTP Headers
|
651
|
+
# +return+ true if required, false otherwise
|
652
|
+
#
|
653
|
+
def is_helper_useful(headers)
|
654
|
+
return false if headers.blank?
|
655
|
+
|
656
|
+
headers.delete('ip')
|
657
|
+
headers.delete('host')
|
658
|
+
|
659
|
+
tmp = local_detect(headers)
|
660
|
+
return false if tmp.blank?
|
661
|
+
|
662
|
+
tmp = get_high_accuracy_candidates
|
663
|
+
return false if tmp.blank?
|
664
|
+
|
665
|
+
true
|
666
|
+
end
|
667
|
+
end
|