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.
@@ -0,0 +1,221 @@
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
+
29
+ class Extra < Base
30
+ def initialize(config={})
31
+ @data = nil
32
+ @store = nil
33
+ super()
34
+ set_config config
35
+ end
36
+
37
+ # Set Config variables
38
+ #
39
+ # +param+ hash config A config hash
40
+ # +return+ boolean true on success, false otherwise
41
+ #
42
+ def set_config(config)
43
+ @store = Store::get_instance
44
+ @store.set_config config
45
+ true
46
+ end
47
+
48
+ def set(data)
49
+ @data = data
50
+ end
51
+
52
+ # Matches all HTTP header extras - platform, browser and app
53
+ #
54
+ # +param+ string cls Is 'platform','browser' or 'app'
55
+ # +return+ an Extra on success, false otherwise
56
+ #
57
+ def match_extra(cls, headers)
58
+ headers.delete 'profile'
59
+ order = @detection_config["#{cls}-ua-order"]
60
+
61
+ headers.keys.each do |key|
62
+ # Append any x- headers to the list of headers to check
63
+ if (not order.include?(key)) and /^x-/i.match(key)
64
+ order << key
65
+ end
66
+ end
67
+ order.each do |field|
68
+ unless headers[field].blank?
69
+ _id = get_match 'user-agent', headers[field], cls, field, cls
70
+ if _id
71
+ extra = find_by_id _id
72
+ return extra
73
+ end
74
+ end
75
+ end
76
+ false
77
+ end
78
+
79
+ # Find a device by its id
80
+ #
81
+ # +param+ string _id
82
+ # +return+ hash device on success, false otherwise
83
+ #
84
+ def find_by_id(_id)
85
+ @store.read "Extra_#{_id}"
86
+ end
87
+
88
+ # Can learn language from language header or agent
89
+ #
90
+ # +param+ hash headers A key => value hash of sanitized http headers
91
+ # +return+ hash Extra on success, false otherwise
92
+ #
93
+ def match_language(headers)
94
+ extra = { 'Extra' => { 'hd_specs' => {}} }
95
+
96
+ # Mock up a fake Extra for merge into detection reply.
97
+ extra['_id'] = 0.to_int
98
+ extra['Extra']['hd_specs']['general_language'] = ''
99
+ extra['Extra']['hd_specs']['general_language_full'] = ''
100
+
101
+ # Try directly from http header first
102
+ unless headers['language'].blank?
103
+ candidate = headers['language']
104
+ if @detection_languages.include?(candidate) and @detection_languages[candidate]
105
+ extra['Extra']['hd_specs']['general_language'] = candidate
106
+ extra['Extra']['hd_specs']['general_language_full'] = @detection_languages[candidate]
107
+ return extra
108
+ end
109
+ end
110
+
111
+ check_order = @detection_config['language-ua-order'] + headers.keys
112
+ language_list = @detection_languages
113
+ check_order.each do |header|
114
+ if headers.include?(header) and not headers[header].blank?
115
+ agent = headers[header]
116
+ language_list.each do |code, full|
117
+ if /[; \(]#{code}[; \)]/i.match(agent)
118
+ extra['Extra']['hd_specs']['general_language'] = code
119
+ extra['Extra']['hd_specs']['general_language_full'] = full
120
+ return extra
121
+ end
122
+ end
123
+ end
124
+ end
125
+ false
126
+ end
127
+
128
+ # Returns false if this device definitively cannot run this platform and platform version.
129
+ # Returns true if its possible of if there is any doubt.
130
+ #
131
+ # Note : The detected platform must match the device platform. This is the stock OS as shipped
132
+ # on the device. If someone is running a variant (eg CyanogenMod) then all bets are off.
133
+ #
134
+ # +param+ string specs The specs we want to check.
135
+ # +return+ boolean false if these specs can not run the detected OS, true otherwise.
136
+ #
137
+ def verify_platform(specs=nil)
138
+ platform = @data
139
+ platform.default = ''
140
+ platform_name = platform['Extra']['hd_specs']['general_platform'].downcase.strip
141
+ platform_version = platform['Extra']['hd_specs']['general_platform_version'].downcase.strip
142
+ device_platform_name = specs['general_platform'].downcase.strip
143
+ device_platform_version_min = specs['general_platform_version'].downcase.strip
144
+ device_platform_version_max = specs['general_platform_version_max'].downcase.strip
145
+
146
+ # Its possible that we didnt pickup the platform correctly or the device has no platform info
147
+ # Return true in this case because we cant give a concrete false (it might run this version).
148
+ if platform.blank? or platform_name.blank? or device_platform_name.blank?
149
+ return true
150
+ end
151
+
152
+ # Make sure device is running stock OS / Platform
153
+ # Return true in this case because its possible the device can run a different OS (mods / hacks etc..)
154
+ if platform_name != device_platform_name
155
+ return true
156
+ end
157
+
158
+ # Detected version is lower than the min version - so definetly false.
159
+ if not platform_version.blank? and not device_platform_version_min.blank? and compare_platform_versions(platform_version, device_platform_version_min) <= -1
160
+ return false
161
+ end
162
+
163
+ # Detected version is greater than the max version - so definetly false.
164
+ if not platform_version.blank? and not device_platform_version_max.blank? and compare_platform_versions(platform_version, device_platform_version_max) >= 1
165
+ return false
166
+ end
167
+
168
+ # Maybe Ok ..
169
+ true
170
+ end
171
+
172
+ # Breaks a version number apart into its Major, minor and point release numbers for comparison.
173
+ #
174
+ # Big Assumption : That version numbers separate their release bits by '.' !!!
175
+ # might need to do some analysis on the string to rip it up right.
176
+ #
177
+ # +param+ string version_number
178
+ # +return+ hash of ('major' => x, 'minor' => y and 'point' => z) on success, null otherwise
179
+ #
180
+ def break_version_apart(version_number)
181
+ tmp = "#{version_number}.0.0.0".split('.', 4)
182
+ reply = {}
183
+ reply['major'] = tmp[0].blank? ? '0' : tmp[0]
184
+ reply['minor'] = tmp[1].blank? ? '0' : tmp[1]
185
+ reply['point'] = tmp[2].blank? ? '0' : tmp[2]
186
+ reply
187
+ end
188
+
189
+ # Helper for comparing two strings (numerically if possible)
190
+ #
191
+ # +param+ string a Generally a number, but might be a string
192
+ # +param+ string b Generally a number, but might be a string
193
+ # +return+ int
194
+ #
195
+ def compare_smartly(a, b)
196
+ begin
197
+ return Integer(a) - Integer(b)
198
+ rescue
199
+ return a <=> b
200
+ end
201
+ end
202
+
203
+ # Compares two platform version numbers
204
+ #
205
+ # +param+ string va Version A
206
+ # +param+ string vb Version B
207
+ # +return+ < 0 if a < b, 0 if a == b and > 0 if a > b : Also returns 0 if data is absent from either.
208
+ #
209
+ def compare_platform_versions(va, vb)
210
+ return 0 if va.blank? or vb.blank?
211
+ version_a = break_version_apart va
212
+ version_b = break_version_apart vb
213
+ major = compare_smartly version_a['major'], version_b['major']
214
+ minor = compare_smartly version_a['minor'], version_b['minor']
215
+ point = compare_smartly version_a['point'], version_b['point']
216
+ return major if major != 0
217
+ return minor if minor != 0
218
+ return point if point != 0
219
+ return 0
220
+ end
221
+ end
@@ -0,0 +1,458 @@
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 'fileutils'
28
+ require 'handset_detection/base'
29
+ require 'handset_detection/device'
30
+ require 'yaml'
31
+ require 'zip'
32
+
33
+ # HD4 Class
34
+ #
35
+ class HD4 < Base
36
+ attr_reader :cache
37
+
38
+ # This is the main constructor for the class HD4
39
+ #
40
+ # +param+ mixed config can be a hash of config options or a fully qualified path to an alternate config file.
41
+ # +return+ void
42
+ #
43
+ def initialize(config=nil)
44
+ @realm = 'APIv4'
45
+ @reply = nil
46
+ @raw_reply = nil
47
+ @detect_request = {}
48
+ @error = ''
49
+ @logger = nil
50
+ @debug = false
51
+ @config = {
52
+ 'username' => '',
53
+ 'secret' => '',
54
+ 'site_id' => '',
55
+ 'use_proxy' => 0,
56
+ 'proxy_server' => '',
57
+ 'proxy_port' => '',
58
+ 'proxy_user' => '',
59
+ 'proxy_pass' => '',
60
+ 'use_local' => false,
61
+ 'api_server' => 'api.handsetdetection.com',
62
+ 'timeout' => 10,
63
+ 'debug' => false,
64
+ 'filesdir' => '',
65
+ 'retries' => 3,
66
+ 'cache_requests' => false,
67
+ 'geoip' => false,
68
+ 'log_unknown' => true
69
+ }
70
+ @tree = {}
71
+ if config.blank?
72
+ if defined? Rails
73
+ config = File.join Rails.root, 'config', 'hdconfig.yml'
74
+ else
75
+ config = 'hdconfig.yml'
76
+ end
77
+ end
78
+ super()
79
+ set_config config
80
+ if @config['username'].blank?
81
+ raise 'Error : API username not set. Download a premade config from your Site Settings.'
82
+ elsif @config['secret'].blank?
83
+ raise 'Error : API secret not set. Download a premade config from your Site Settings.'
84
+ end
85
+ end
86
+
87
+ def set_local_detection(enable)
88
+ @config['use_local'] = enable
89
+ end
90
+
91
+ def set_proxy_user(user)
92
+ @config['proxy_user'] = user
93
+ end
94
+
95
+ def set_proxy_pass(pass)
96
+ @config['proxy_pass'] = pass
97
+ end
98
+
99
+ def set_use_proxy(proxy)
100
+ @config['use_proxy'] = proxy
101
+ end
102
+
103
+ def set_proxy_server(name)
104
+ @config['proxy_server'] = name
105
+ end
106
+
107
+ def set_proxy_port(number)
108
+ @config['proxy_port'] = number
109
+ end
110
+
111
+ def set_secret(secret)
112
+ @config['secret'] = secret
113
+ end
114
+
115
+ def set_username(user)
116
+ @config['username'] = user
117
+ end
118
+
119
+ def set_timeout(timeout)
120
+ @config['timeout'] = timeout
121
+ end
122
+
123
+ def set_detect_var(key, value)
124
+ @detect_request[key.downcase] = value
125
+ end
126
+
127
+ def set_site_id(siteid)
128
+ @config['site_id'] = siteid.to_i
129
+ end
130
+
131
+ def set_use_local(value)
132
+ @config['use_local'] = value
133
+ end
134
+
135
+ def set_api_server(value)
136
+ @config['api_server'] = value
137
+ end
138
+
139
+ def set_logger(function)
140
+ @config['logger'] = function
141
+ end
142
+
143
+ def set_files_dir(directory)
144
+ @config['filesdir'] = directory
145
+ unless @store.set_directory(directory)
146
+ raise "Error : Failed to create cache directory in #{directory}. Set your filesdir config setting or check directory permissions."
147
+ end
148
+ end
149
+
150
+ def get_local_detection
151
+ @config['use_local']
152
+ end
153
+
154
+ def get_proxy_user
155
+ @config['proxy_user']
156
+ end
157
+
158
+ def get_proxy_pass
159
+ @config['proxy_pass']
160
+ end
161
+
162
+ def get_use_proxy
163
+ @config['use_proxy']
164
+ end
165
+
166
+ def get_proxy_server
167
+ @config['proxy_server']
168
+ end
169
+
170
+ def get_proxy_port
171
+ @config['proxy_port']
172
+ end
173
+
174
+ def get_error
175
+ @error
176
+ end
177
+
178
+ def get_error_msg
179
+ @error
180
+ end
181
+
182
+ def get_secret
183
+ @config['secret']
184
+ end
185
+
186
+ def get_username
187
+ @config['username']
188
+ end
189
+
190
+ def get_timeout
191
+ @config['timeout']
192
+ end
193
+
194
+ def get_reply
195
+ @reply
196
+ end
197
+
198
+ def get_raw_reply
199
+ @raw_reply
200
+ end
201
+
202
+ def get_site_id
203
+ @config['site_id']
204
+ end
205
+
206
+ def get_use_local
207
+ @config['use_local']
208
+ end
209
+
210
+ def get_api_server
211
+ @config['api_server']
212
+ end
213
+
214
+ def get_detect_request
215
+ @detect_request
216
+ end
217
+
218
+ def get_files_dir
219
+ @config['filesdir']
220
+ end
221
+
222
+ # Set config file
223
+ #
224
+ # +param+ hash config An assoc array of config data
225
+ # +return+ true on success, false otherwise
226
+ #
227
+ def set_config(cfg)
228
+ if cfg.is_a? Hash
229
+ @config = cfg
230
+ elsif cfg.is_a? String
231
+ @config = YAML::load_file cfg
232
+ end
233
+
234
+ if @config['filesdir'].blank?
235
+ @config['filesdir'] = File.dirname __FILE__
236
+ end
237
+
238
+ @store = Store::get_instance
239
+ @store.set_config @config, true
240
+ @cache = Cache.new(@config)
241
+ @device = Device.new(@config)
242
+ true
243
+ end
244
+
245
+ # Setup the api kit with HTTP headers from the web server environment.
246
+ #
247
+ # This is likely what you want to send if you're invoking this class for each new website visitor.
248
+ # You can override or add to these with set_detect_var(key, value)
249
+ #
250
+ # +param+ env A hash holding web server environment variables. In Rails: request.env or request.headers
251
+ # +return+ void
252
+ #
253
+ def set_server_headers(env)
254
+ env.each do |k, v|
255
+ next unless k.match(/^HTTP_/)
256
+ name = k.sub(/^HTTP_/, '').split(/_/).map { |x| x.capitalize }.join('-')
257
+ @detect_request[name] = v
258
+ end
259
+ end
260
+
261
+ # List all known vendors
262
+ #
263
+ # +param+ void
264
+ # +return+ bool true on success, false otherwise. Use getReply to inspect results on success.
265
+ #
266
+ def device_vendors
267
+ return remote("device/vendors", nil) unless @config['use_local']
268
+ @reply = @device.get_reply if @device.local_vendors
269
+ set_error @device.get_status, @device.get_message
270
+ end
271
+
272
+ # List all models for a given vendor
273
+ #
274
+ # +param+ string vendor The device vendor eg Apple
275
+ # +return+ bool true on success, false otherwise. Use getReply to inspect results on success.
276
+ #
277
+ def device_models(vendor)
278
+ unless @config['use_local']
279
+ return remote "device/models/#{vendor}", nil
280
+ end
281
+
282
+ if @device.local_models vendor
283
+ @reply = @device.get_reply
284
+ end
285
+
286
+ set_error @device.get_status, @device.get_message
287
+ end
288
+
289
+ # Find properties for a specific device
290
+ #
291
+ # +param+ string vendor The device vendor eg. Nokia
292
+ # +param+ string model The deviec model eg. N95
293
+ # +return+ bool true on success, false otherwise. Use getReply to inspect results on success.
294
+ #
295
+ def device_view(vendor, model)
296
+ unless @config['use_local']
297
+ return remote "device/view/#{vendor}/#{model}", nil
298
+ end
299
+ if @device.local_view vendor, model
300
+ @reply = @device.get_reply
301
+ end
302
+ set_error @device.get_status, @device.get_message
303
+ end
304
+
305
+ # Find which devices have property 'X'.
306
+ #
307
+ # +param+ string key Property to inquire about eg 'network', 'connectors' etc...
308
+ # +param+ string value Value to inquire about eg 'CDMA', 'USB' etc ...
309
+ # +return+ bool true on success, false otherwise. Use getReply to inspect results on success.
310
+ #
311
+ def device_what_has(key, value)
312
+ unless @config['use_local']
313
+ return remote("device/whathas/#{key}/#{value}", nil)
314
+ end
315
+
316
+ if @device.local_what_has key, value
317
+ @reply = @device.get_reply
318
+ end
319
+
320
+ set_error @device.get_status, @device.get_message
321
+ end
322
+
323
+ # Device Detect
324
+ #
325
+ # +param+ array data : Data for device detection : HTTP Headers usually
326
+ # +return+ bool true on success, false otherwise. Use getReply to inspect results on success.
327
+ #
328
+ def device_detect(data={})
329
+ id = data['id'].blank? ? @config['site_id'] : data['id']
330
+ request_body = @detect_request.merge data
331
+ fast_key = ''
332
+
333
+ # If caching enabled then check cache
334
+ unless @config['cache_requests'].blank?
335
+ request_body.each { |k, v| headers[k.downcase] = v }
336
+ headers = headers.sort.to_h
337
+ fast_key = JSON.generate(headers).gsub(/ /, '')
338
+ if reply = @cache.read(fast_key)
339
+ @reply = reply
340
+ @raw_reply = ''
341
+ return set_error 0, "OK"
342
+ end
343
+ end
344
+
345
+ if @config['use_local']
346
+ # log("Starting Local Detection")
347
+ result = @device.local_detect(request_body)
348
+ set_error @device.get_status, @device.get_message
349
+ set_reply @device.get_reply
350
+ # log"Finishing Local Detection : result is #{result}"
351
+ # Log unknown headers if enabled
352
+ if @config['log_unknown'] == true and not result
353
+ send_remote_syslog request_body
354
+ end
355
+ else
356
+ # log "Starting API Detection"
357
+ result = remote "device/detect/#{id}", request_body
358
+ # log "Finishing API Detection : result is #{result}"
359
+ end
360
+
361
+ # If we got a result then cache it
362
+ if result and not @config['cache_requests'].blank?
363
+ @cache.write fast_key, reply
364
+ end
365
+ result
366
+ end
367
+
368
+ # Fetch an archive from handset detection which contains all the device specs and matching trees as individual json files.
369
+ #
370
+ # +param+ void
371
+ # +return+ hd_specs data on success, false otherwise
372
+ #
373
+ def device_fetch_archive
374
+ path = File.join(@config['filesdir'], "ultimate.zip")
375
+ unless @config['local_archive_source'].blank?
376
+ FileUtils.cp File.join(@config['local_archive_source'], "ultimate.zip"), path
377
+ return install_archive path
378
+ end
379
+
380
+ return false unless remote("device/fetcharchive", '', 'zip')
381
+ data = get_raw_reply
382
+ if data.blank?
383
+ return set_error 299, 'Error : FetchArchive failed. Bad Download. File is zero length'
384
+ elsif data.length < 9000000
385
+ trythis = JSON.parse data
386
+ if trythis and trythis.include?('status') and trythis.include('message')
387
+ return set_error trythis['status'].to_i, trythis['message']
388
+ end
389
+ set_error 299, "Error : FetchArchive failed. Bad Download. File too short at #{data.length} bytes."
390
+ end
391
+
392
+ begin
393
+ File.open(path, 'w') { |f| f.write(get_raw_reply) }
394
+ rescue
395
+ return set_error 299, "Error : FetchArchive failed. Could not write #{path}"
396
+ end
397
+ return install_archive path
398
+ end
399
+
400
+ # Community Fetch Archive - Fetch the community archive version
401
+ #
402
+ # +param+ void
403
+ # +return+ hd_specs data on success, false otherwise
404
+ #
405
+ def community_fetch_archive
406
+ path = File.join(@config['filesdir'], "communityultimate.zip")
407
+ unless @config['local_archive_source'].blank?
408
+ FileUtils.cp File.join(@config['local_archive_source'], "communityultimate.zip"), path
409
+ return install_archive path
410
+ end
411
+
412
+ return false unless remote "community/fetcharchive", '', 'zip', false
413
+ data = get_raw_reply
414
+ if data.blank?
415
+ return set_error 299, 'Error : FetchArchive failed. Bad Download. File is zero length'
416
+ elsif data.length < 900000
417
+ trythis = JSON.parse data
418
+ if not trythis.blank? and trythis.include?('status') and trythis.include?('message')
419
+ return set_error trythis['status'].to_int, trythis['message']
420
+ end
421
+ return set_error 299, "Error : FetchArchive failed. Bad Download. File too short at #{data.length} bytes."
422
+ end
423
+
424
+ begin
425
+ File.open(path, 'w') { |f| f.write data }
426
+ rescue
427
+ return set_error 299, "Error : FetchArchive failed. Could not write #{path}"
428
+ end
429
+ return install_archive path
430
+ end
431
+
432
+ # Install an ultimate archive file
433
+ #
434
+ # +param+ string $file Fully qualified path to file
435
+ # +return+ boolean true on success, false otherwise
436
+ #
437
+ def install_archive(file)
438
+ # Unzip the archive and cache the individual files
439
+ Zip::File.open(file) do |zip_file|
440
+ # return $this->setError(299, "Error : Failed to open ". $file)
441
+ zip_file.each do |entry|
442
+ filename = File.join Dir.tmpdir, entry.name
443
+ entry.extract(filename) {true}
444
+ @store.move_in filename, entry.name
445
+ end
446
+ end
447
+ true
448
+ end
449
+
450
+ # This method can indicate if using the js Helper would yeild more accurate results.
451
+ #
452
+ # +param+ hash headers
453
+ # +return+ true if helpful, false otherwise.
454
+ #
455
+ def is_helper_useful(headers)
456
+ @device.is_helper_useful headers
457
+ end
458
+ end