1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* BrowsCap.php |
4
|
|
|
* Author: Federico Mestrone |
5
|
|
|
* Created: 17/12/2012 12:16 |
6
|
|
|
* Copyright: 2012, Moodsdesign Ltd |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
namespace phpbrowscap; |
10
|
|
|
|
11
|
|
|
use \Exception as BaseException; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* Browscap.ini parsing class with caching and update capabilities |
15
|
|
|
* |
16
|
|
|
* PHP version 5 |
17
|
|
|
* |
18
|
|
|
* Copyright (c) 2006-2012 Jonathan Stoppani |
19
|
|
|
* |
20
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a |
21
|
|
|
* copy of this software and associated documentation files (the "Software"), |
22
|
|
|
* to deal in the Software without restriction, including without limitation |
23
|
|
|
* the rights to use, copy, modify, merge, publish, distribute, sublicense, |
24
|
|
|
* and/or sell copies of the Software, and to permit persons to whom the |
25
|
|
|
* Software is furnished to do so, subject to the following conditions: |
26
|
|
|
* |
27
|
|
|
* The above copyright notice and this permission notice shall be included |
28
|
|
|
* in all copies or substantial portions of the Software. |
29
|
|
|
* |
30
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
31
|
|
|
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
32
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
33
|
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
34
|
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
35
|
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
36
|
|
|
* THE SOFTWARE. |
37
|
|
|
* |
38
|
|
|
* @package Browscap |
39
|
|
|
* @author Jonathan Stoppani <[email protected]> |
40
|
|
|
* @author Vítor Brandão <[email protected]> |
41
|
|
|
* @copyright Copyright (c) 2006-2012 Jonathan Stoppani |
42
|
|
|
* @version 1.0 |
43
|
|
|
* @license http://www.opensource.org/licenses/MIT MIT License |
44
|
|
|
* @link https://github.com/GaretJax/phpbrowscap/ |
45
|
|
|
*/ |
46
|
|
|
class Browscap |
47
|
|
|
{ |
48
|
|
|
/** |
49
|
|
|
* Current version of the class. |
50
|
|
|
*/ |
51
|
|
|
const VERSION = '1.0'; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Different ways to access remote and local files. |
55
|
|
|
* |
56
|
|
|
* UPDATE_FOPEN: Uses the fopen url wrapper (use file_get_contents). |
57
|
|
|
* UPDATE_FSOCKOPEN: Uses the socket functions (fsockopen). |
58
|
|
|
* UPDATE_CURL: Uses the cURL extension. |
59
|
|
|
* UPDATE_LOCAL: Updates from a local file (file_get_contents). |
60
|
|
|
*/ |
61
|
|
|
const UPDATE_FOPEN = 'URL-wrapper'; |
62
|
|
|
const UPDATE_FSOCKOPEN = 'socket'; |
63
|
|
|
const UPDATE_CURL = 'cURL'; |
64
|
|
|
const UPDATE_LOCAL = 'local'; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Options for regex patterns. |
68
|
|
|
* |
69
|
|
|
* REGEX_DELIMITER: Delimiter of all the regex patterns in the whole class. |
70
|
|
|
* REGEX_MODIFIERS: Regex modifiers. |
71
|
|
|
*/ |
72
|
|
|
const REGEX_DELIMITER = '@'; |
73
|
|
|
const REGEX_MODIFIERS = 'i'; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* The values to quote in the ini file |
77
|
|
|
*/ |
78
|
|
|
const VALUES_TO_QUOTE = 'Browser|Parent'; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Definitions of the function used by the uasort() function to order the |
82
|
|
|
* userAgents array. |
83
|
|
|
* |
84
|
|
|
* ORDER_FUNC_ARGS: Arguments that the function will take. |
85
|
|
|
* ORDER_FUNC_LOGIC: Internal logic of the function. |
86
|
|
|
*/ |
87
|
|
|
const ORDER_FUNC_ARGS = '$a, $b'; |
88
|
|
|
const ORDER_FUNC_LOGIC = '$a=strlen($a);$b=strlen($b);return$a==$b?0:($a<$b?1:-1);'; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* The headers to be sent for checking the version and requesting the file. |
92
|
|
|
*/ |
93
|
|
|
const REQUEST_HEADERS = "GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: %s\r\nConnection: Close\r\n\r\n"; |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Options for auto update capabilities |
97
|
|
|
* |
98
|
|
|
* $remoteVerUrl: The location to use to check out if a new version of the |
99
|
|
|
* browscap.ini file is available. |
100
|
|
|
* $remoteIniUrl: The location from which download the ini file. |
101
|
|
|
* The placeholder for the file should be represented by a %s. |
102
|
|
|
* $timeout: The timeout for the requests. |
103
|
|
|
* $updateInterval: The update interval in seconds. |
104
|
|
|
* $errorInterval: The next update interval in seconds in case of an error. |
105
|
|
|
* $doAutoUpdate: Flag to disable the automatic interval based update. |
106
|
|
|
* $updateMethod: The method to use to update the file, has to be a value of |
107
|
|
|
* an UPDATE_* constant, null or false. |
108
|
|
|
*/ |
109
|
|
|
public $remoteIniUrl = 'http://tempdownloads.browserscap.com/stream.php?Full_PHP_BrowscapINI'; |
110
|
|
|
public $remoteVerUrl = 'http://tempdownloads.browserscap.com/versions/version-date.php'; |
111
|
|
|
public $timeout = 5; |
112
|
|
|
public $updateInterval = 432000; // 5 days |
113
|
|
|
public $errorInterval = 7200; // 2 hours |
114
|
|
|
public $doAutoUpdate = true; |
115
|
|
|
public $updateMethod = null; |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* The path of the local version of the browscap.ini file from which to |
119
|
|
|
* update (to be set only if used). |
120
|
|
|
* |
121
|
|
|
* @var string |
122
|
|
|
*/ |
123
|
|
|
public $localFile = null; |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* The useragent to include in the requests made by the class during the |
127
|
|
|
* update process. |
128
|
|
|
* |
129
|
|
|
* @var string |
130
|
|
|
*/ |
131
|
|
|
public $userAgent = 'Browser Capabilities Project - PHP Browscap/%v %m'; |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Flag to enable only lowercase indexes in the result. |
135
|
|
|
* The cache has to be rebuilt in order to apply this option. |
136
|
|
|
* |
137
|
|
|
* @var bool |
138
|
|
|
*/ |
139
|
|
|
public $lowercase = false; |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* Flag to enable/disable silent error management. |
143
|
|
|
* In case of an error during the update process the class returns an empty |
144
|
|
|
* array/object if the update process can't take place and the browscap.ini |
145
|
|
|
* file does not exist. |
146
|
|
|
* |
147
|
|
|
* @var bool |
148
|
|
|
*/ |
149
|
|
|
public $silent = false; |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* Where to store the cached PHP arrays. |
153
|
|
|
* |
154
|
|
|
* @var string |
155
|
|
|
*/ |
156
|
|
|
public $cacheFilename = 'cache.php'; |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* Where to store the downloaded ini file. |
160
|
|
|
* |
161
|
|
|
* @var string |
162
|
|
|
*/ |
163
|
|
|
public $iniFilename = 'browscap.ini'; |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Path to the cache directory |
167
|
|
|
* |
168
|
|
|
* @var string |
169
|
|
|
*/ |
170
|
|
|
public $cacheDir = null; |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Flag to be set to true after loading the cache |
174
|
|
|
* |
175
|
|
|
* @var bool |
176
|
|
|
*/ |
177
|
|
|
protected $_cacheLoaded = false; |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* Where to store the value of the included PHP cache file |
181
|
|
|
* |
182
|
|
|
* @var array |
183
|
|
|
*/ |
184
|
|
|
protected $_userAgents = array(); |
185
|
|
|
protected $_browsers = array(); |
186
|
|
|
protected $_patterns = array(); |
187
|
|
|
protected $_properties = array(); |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* An associative array of associative arrays in the format |
191
|
|
|
* `$arr['wrapper']['option'] = $value` passed to stream_context_create() |
192
|
|
|
* when building a stream resource. |
193
|
|
|
* |
194
|
|
|
* Proxy settings are stored in this variable. |
195
|
|
|
* |
196
|
|
|
* @see http://www.php.net/manual/en/function.stream-context-create.php |
197
|
|
|
* |
198
|
|
|
* @var array |
199
|
|
|
*/ |
200
|
|
|
protected $_streamContextOptions = array(); |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* A valid context resource created with stream_context_create(). |
204
|
|
|
* |
205
|
|
|
* @see http://www.php.net/manual/en/function.stream-context-create.php |
206
|
|
|
* |
207
|
|
|
* @var resource |
208
|
|
|
*/ |
209
|
|
|
protected $_streamContext = null; |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* Constructor class, checks for the existence of (and loads) the cache and |
213
|
|
|
* if needed updated the definitions |
214
|
|
|
* |
215
|
|
|
* @param string $cache_dir |
216
|
|
|
*/ |
217
|
|
|
public function __construct($cache_dir) |
218
|
|
|
{ |
219
|
|
|
// has to be set to reach E_STRICT compatibility, does not affect system/app settings |
220
|
|
|
date_default_timezone_set(date_default_timezone_get()); |
221
|
|
|
|
222
|
|
|
if (!isset($cache_dir)) { |
223
|
|
|
throw new Exception( |
224
|
|
|
'You have to provide a path to read/store the browscap cache file' |
225
|
|
|
); |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
$old_cache_dir = $cache_dir; |
229
|
|
|
$cache_dir = realpath($cache_dir); |
230
|
|
|
|
231
|
|
|
if (false === $cache_dir) { |
232
|
|
|
throw new Exception( |
233
|
|
|
sprintf('The cache path %s is invalid. Are you sure that it exists and that you have permission to access it?', $old_cache_dir) |
234
|
|
|
); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
// Is the cache dir really the directory or is it directly the file? |
238
|
|
|
if (substr($cache_dir, -4) === '.php') { |
239
|
|
|
$this->cacheFilename = basename($cache_dir); |
240
|
|
|
$this->cacheDir = dirname($cache_dir); |
241
|
|
|
} else { |
242
|
|
|
$this->cacheDir = $cache_dir; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
$this->cacheDir .= DIRECTORY_SEPARATOR; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Gets the information about the browser by User Agent |
250
|
|
|
* |
251
|
|
|
* @param string $user_agent the user agent string |
252
|
|
|
* @param bool $return_array whether return an array or an object |
253
|
|
|
* @throws Exception |
254
|
|
|
* @return stdObject the object containing the browsers details. Array if |
255
|
|
|
* $return_array is set to true. |
256
|
|
|
*/ |
257
|
|
|
public function getBrowser($user_agent = null, $return_array = false) |
258
|
|
|
{ |
259
|
|
|
// Load the cache at the first request |
260
|
|
|
if (!$this->_cacheLoaded) { |
261
|
|
|
$cache_file = $this->cacheDir . $this->cacheFilename; |
262
|
|
|
$ini_file = $this->cacheDir . $this->iniFilename; |
263
|
|
|
|
264
|
|
|
// Set the interval only if needed |
265
|
|
|
if ($this->doAutoUpdate && file_exists($ini_file)) { |
266
|
|
|
$interval = time() - filemtime($ini_file); |
267
|
|
|
} else { |
268
|
|
|
$interval = 0; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
// Find out if the cache needs to be updated |
272
|
|
|
if (!file_exists($cache_file) || !file_exists($ini_file) || ($interval > $this->updateInterval)) { |
273
|
|
|
try { |
274
|
|
|
$this->updateCache(); |
275
|
|
|
} catch (Exception $e) { |
276
|
|
|
if (file_exists($ini_file)) { |
277
|
|
|
// Adjust the filemtime to the $errorInterval |
278
|
|
|
touch($ini_file, time() - $this->updateInterval + $this->errorInterval); |
279
|
|
|
} elseif ($this->silent) { |
280
|
|
|
// Return an array if silent mode is active and the ini db doesn't exsist |
281
|
|
|
return array(); |
|
|
|
|
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
if (!$this->silent) { |
285
|
|
|
throw $e; |
286
|
|
|
} |
287
|
|
|
} |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
$this->_loadCache($cache_file); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
// Automatically detect the useragent |
294
|
|
|
if (!isset($user_agent)) { |
295
|
|
|
if (isset($_SERVER['HTTP_USER_AGENT'])) { |
296
|
|
|
$user_agent = $_SERVER['HTTP_USER_AGENT']; |
297
|
|
|
} else { |
298
|
|
|
$user_agent = ''; |
299
|
|
|
} |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
$browser = array(); |
303
|
|
|
foreach ($this->_patterns as $key => $pattern) { |
304
|
|
|
if (preg_match($pattern . 'i', $user_agent)) { |
305
|
|
|
$browser = array( |
306
|
|
|
$user_agent, // Original useragent |
307
|
|
|
trim(strtolower($pattern), self::REGEX_DELIMITER), |
308
|
|
|
$this->_userAgents[$key] |
309
|
|
|
); |
310
|
|
|
|
311
|
|
|
$browser = $value = $browser + $this->_browsers[$key]; |
312
|
|
|
|
313
|
|
|
while (array_key_exists(3, $value) && $value[3]) { |
314
|
|
|
$value = $this->_browsers[$value[3]]; |
315
|
|
|
$browser += $value; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
if (!empty($browser[3])) { |
319
|
|
|
$browser[3] = $this->_userAgents[$browser[3]]; |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
break; |
323
|
|
|
} |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
// Add the keys for each property |
327
|
|
|
$array = array(); |
328
|
|
|
foreach ($browser as $key => $value) { |
329
|
|
|
if ($value === 'true') { |
330
|
|
|
$value = true; |
331
|
|
|
} elseif ($value === 'false') { |
332
|
|
|
$value = false; |
333
|
|
|
} |
334
|
|
|
$array[$this->_properties[$key]] = $value; |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
return $return_array ? $array : (object) $array; |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
/** |
341
|
|
|
* Load (auto-set) proxy settings from environment variables. |
342
|
|
|
*/ |
343
|
|
|
public function autodetectProxySettings() |
344
|
|
|
{ |
345
|
|
|
$wrappers = array('http', 'https', 'ftp'); |
346
|
|
|
|
347
|
|
|
foreach ($wrappers as $wrapper) { |
348
|
|
|
$url = getenv($wrapper.'_proxy'); |
349
|
|
|
if (!empty($url)) { |
350
|
|
|
$params = array_merge(array( |
351
|
|
|
'port' => null, |
352
|
|
|
'user' => null, |
353
|
|
|
'pass' => null, |
354
|
|
|
), parse_url($url)); |
355
|
|
|
$this->addProxySettings($params['host'], $params['port'], $wrapper, $params['user'], $params['pass']); |
356
|
|
|
} |
357
|
|
|
} |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* Add proxy settings to the stream context array. |
362
|
|
|
* |
363
|
|
|
* @param string $server Proxy server/host |
364
|
|
|
* @param int $port Port |
365
|
|
|
* @param string $wrapper Wrapper: "http", "https", "ftp", others... |
366
|
|
|
* @param string $username Username (when requiring authentication) |
367
|
|
|
* @param string $password Password (when requiring authentication) |
368
|
|
|
* |
369
|
|
|
* @return Browscap |
370
|
|
|
*/ |
371
|
|
|
public function addProxySettings($server, $port = 3128, $wrapper = 'http', $username = null, $password = null) |
372
|
|
|
{ |
373
|
|
|
$settings = array($wrapper => array( |
374
|
|
|
'proxy' => sprintf('tcp://%s:%d', $server, $port), |
375
|
|
|
'request_fulluri' => true, |
376
|
|
|
)); |
377
|
|
|
|
378
|
|
|
// Proxy authentication (optional) |
379
|
|
|
if (isset($username) && isset($password)) { |
380
|
|
|
$settings[$wrapper]['header'] = 'Proxy-Authorization: Basic '.base64_encode($username.':'.$password); |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
// Add these new settings to the stream context options array |
384
|
|
|
$this->_streamContextOptions = array_merge( |
385
|
|
|
$this->_streamContextOptions, |
386
|
|
|
$settings |
387
|
|
|
); |
388
|
|
|
|
389
|
|
|
/* Return $this so we can chain addProxySettings() calls like this: |
390
|
|
|
* $browscap-> |
391
|
|
|
* addProxySettings('http')-> |
392
|
|
|
* addProxySettings('https')-> |
393
|
|
|
* addProxySettings('ftp'); |
394
|
|
|
*/ |
395
|
|
|
return $this; |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* Clear proxy settings from the stream context options array. |
400
|
|
|
* |
401
|
|
|
* @param string $wrapper Remove settings from this wrapper only |
402
|
|
|
* |
403
|
|
|
* @return array Wrappers cleared |
404
|
|
|
*/ |
405
|
|
|
public function clearProxySettings($wrapper = null) |
406
|
|
|
{ |
407
|
|
|
$wrappers = isset($wrapper) ? array($wrappers) : array_keys($this->_streamContextOptions); |
|
|
|
|
408
|
|
|
|
409
|
|
|
$affectedProtocols = array(); |
410
|
|
|
$options = array('proxy', 'request_fulluri', 'header'); |
411
|
|
|
foreach ($wrappers as $wrapper) { |
412
|
|
|
|
413
|
|
|
// remove wrapper options related to proxy settings |
414
|
|
|
if (isset($this->_streamContextOptions[$wrapper]['proxy'])) { |
415
|
|
|
foreach ($options as $option){ |
416
|
|
|
unset($this->_streamContextOptions[$wrapper][$option]); |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
// remove wrapper entry if there are no other options left |
420
|
|
|
if (empty($this->_streamContextOptions[$wrapper])) { |
421
|
|
|
unset($this->_streamContextOptions[$wrapper]); |
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
$clearedWrappers[] = $wrapper; |
|
|
|
|
425
|
|
|
} |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
return $clearedWrappers; |
|
|
|
|
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
/** |
432
|
|
|
* Returns the array of stream context options. |
433
|
|
|
* |
434
|
|
|
* @return array |
435
|
|
|
*/ |
436
|
|
|
public function getStreamContextOptions() |
437
|
|
|
{ |
438
|
|
|
return $this->_streamContextOptions; |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
/** |
442
|
|
|
* Parses the ini file and updates the cache files |
443
|
|
|
* |
444
|
|
|
* @return bool whether the file was correctly written to the disk |
445
|
|
|
*/ |
446
|
|
|
public function updateCache() |
447
|
|
|
{ |
448
|
|
|
$ini_path = $this->cacheDir . $this->iniFilename; |
449
|
|
|
$cache_path = $this->cacheDir . $this->cacheFilename; |
450
|
|
|
|
451
|
|
|
// Choose the right url |
452
|
|
|
if ($this->_getUpdateMethod() == self::UPDATE_LOCAL) { |
453
|
|
|
$url = $this->localFile; |
454
|
|
|
} else { |
455
|
|
|
$url = $this->remoteIniUrl; |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
$this->_getRemoteIniFile($url, $ini_path); |
459
|
|
|
|
460
|
|
|
if (version_compare(PHP_VERSION, '5.3.0', '>=')) { |
461
|
|
|
$browsers = parse_ini_file($ini_path, true, INI_SCANNER_RAW); |
462
|
|
|
} else { |
463
|
|
|
$browsers = parse_ini_file($ini_path, true); |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
array_shift($browsers); |
467
|
|
|
|
468
|
|
|
$this->_properties = array_keys($browsers['DefaultProperties']); |
469
|
|
|
array_unshift( |
470
|
|
|
$this->_properties, |
471
|
|
|
'browser_name', |
472
|
|
|
'browser_name_regex', |
473
|
|
|
'browser_name_pattern', |
474
|
|
|
'Parent' |
475
|
|
|
); |
476
|
|
|
|
477
|
|
|
$this->_userAgents = array_keys($browsers); |
478
|
|
|
usort( |
479
|
|
|
$this->_userAgents, |
480
|
|
|
create_function(self::ORDER_FUNC_ARGS, self::ORDER_FUNC_LOGIC) |
481
|
|
|
); |
482
|
|
|
|
483
|
|
|
$user_agents_keys = array_flip($this->_userAgents); |
484
|
|
|
$properties_keys = array_flip($this->_properties); |
485
|
|
|
|
486
|
|
|
$search = array('\*', '\?'); |
487
|
|
|
$replace = array('.*', '.'); |
488
|
|
|
|
489
|
|
|
foreach ($this->_userAgents as $user_agent) { |
490
|
|
|
$pattern = preg_quote($user_agent, self::REGEX_DELIMITER); |
491
|
|
|
$this->_patterns[] = self::REGEX_DELIMITER |
492
|
|
|
. '^' |
493
|
|
|
. str_replace($search, $replace, $pattern) |
494
|
|
|
. '$' |
495
|
|
|
. self::REGEX_DELIMITER; |
496
|
|
|
|
497
|
|
|
if (!empty($browsers[$user_agent]['Parent'])) { |
498
|
|
|
$parent = $browsers[$user_agent]['Parent']; |
499
|
|
|
$browsers[$user_agent]['Parent'] = $user_agents_keys[$parent]; |
500
|
|
|
} |
501
|
|
|
|
502
|
|
|
foreach ($browsers[$user_agent] as $key => $value) { |
503
|
|
|
$key = $properties_keys[$key] . ".0"; |
504
|
|
|
$browser[$key] = $value; |
|
|
|
|
505
|
|
|
} |
506
|
|
|
|
507
|
|
|
$this->_browsers[] = $browser; |
|
|
|
|
508
|
|
|
unset($browser); |
509
|
|
|
} |
510
|
|
|
unset($user_agents_keys, $properties_keys, $browsers); |
511
|
|
|
|
512
|
|
|
// Save the keys lowercased if needed |
513
|
|
|
if ($this->lowercase) { |
514
|
|
|
$this->_properties = array_map('strtolower', $this->_properties); |
515
|
|
|
} |
516
|
|
|
|
517
|
|
|
// Get the whole PHP code |
518
|
|
|
$cache = $this->_buildCache(); |
519
|
|
|
|
520
|
|
|
// Save and return |
521
|
|
|
return (bool) file_put_contents($cache_path, $cache, LOCK_EX); |
522
|
|
|
} |
523
|
|
|
|
524
|
|
|
/** |
525
|
|
|
* Loads the cache into object's properties |
526
|
|
|
* |
527
|
|
|
* @return void |
528
|
|
|
*/ |
529
|
|
|
protected function _loadCache($cache_file) |
530
|
|
|
{ |
531
|
|
|
require $cache_file; |
532
|
|
|
|
533
|
|
|
$this->_browsers = $browsers; |
|
|
|
|
534
|
|
|
$this->_userAgents = $userAgents; |
|
|
|
|
535
|
|
|
$this->_patterns = $patterns; |
|
|
|
|
536
|
|
|
$this->_properties = $properties; |
|
|
|
|
537
|
|
|
|
538
|
|
|
$this->_cacheLoaded = true; |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
/** |
542
|
|
|
* Parses the array to cache and creates the PHP string to write to disk |
543
|
|
|
* |
544
|
|
|
* @return string the PHP string to save into the cache file |
545
|
|
|
*/ |
546
|
|
|
protected function _buildCache() |
547
|
|
|
{ |
548
|
|
|
$cacheTpl = "<?php\n\$properties=%s;\n\$browsers=%s;\n\$userAgents=%s;\n\$patterns=%s;\n"; |
549
|
|
|
|
550
|
|
|
$propertiesArray = $this->_array2string($this->_properties); |
551
|
|
|
$patternsArray = $this->_array2string($this->_patterns); |
552
|
|
|
$userAgentsArray = $this->_array2string($this->_userAgents); |
553
|
|
|
$browsersArray = $this->_array2string($this->_browsers); |
554
|
|
|
|
555
|
|
|
return sprintf( |
556
|
|
|
$cacheTpl, |
557
|
|
|
$propertiesArray, |
558
|
|
|
$browsersArray, |
559
|
|
|
$userAgentsArray, |
560
|
|
|
$patternsArray |
561
|
|
|
); |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
/** |
565
|
|
|
* Lazy getter for the stream context resource. |
566
|
|
|
* |
567
|
|
|
* @return resource |
568
|
|
|
*/ |
569
|
|
|
protected function _getStreamContext($recreate = false) |
570
|
|
|
{ |
571
|
|
|
if (!isset($this->_streamContext) || true === $recreate) { |
572
|
|
|
$this->_streamContext = stream_context_create($this->_streamContextOptions); |
573
|
|
|
} |
574
|
|
|
|
575
|
|
|
return $this->_streamContext; |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
/** |
579
|
|
|
* Updates the local copy of the ini file (by version checking) and adapts |
580
|
|
|
* his syntax to the PHP ini parser |
581
|
|
|
* |
582
|
|
|
* @param string $url the url of the remote server |
583
|
|
|
* @param string $path the path of the ini file to update |
584
|
|
|
* @throws Exception |
585
|
|
|
* @return bool if the ini file was updated |
586
|
|
|
*/ |
587
|
|
|
protected function _getRemoteIniFile($url, $path) |
588
|
|
|
{ |
589
|
|
|
// Check version |
590
|
|
|
if (file_exists($path) && filesize($path)) { |
591
|
|
|
$local_tmstp = filemtime($path); |
592
|
|
|
|
593
|
|
|
if ($this->_getUpdateMethod() == self::UPDATE_LOCAL) { |
594
|
|
|
$remote_tmstp = $this->_getLocalMTime(); |
595
|
|
|
} else { |
596
|
|
|
$remote_tmstp = $this->_getRemoteMTime(); |
597
|
|
|
} |
598
|
|
|
|
599
|
|
|
if ($remote_tmstp < $local_tmstp) { |
600
|
|
|
// No update needed, return |
601
|
|
|
touch($path); |
602
|
|
|
|
603
|
|
|
return false; |
604
|
|
|
} |
605
|
|
|
} |
606
|
|
|
|
607
|
|
|
// Get updated .ini file |
608
|
|
|
$browscap = $this->_getRemoteData($url); |
609
|
|
|
|
610
|
|
|
|
611
|
|
|
$browscap = explode("\n", $browscap); |
612
|
|
|
|
613
|
|
|
$pattern = self::REGEX_DELIMITER |
614
|
|
|
. '(' |
615
|
|
|
. self::VALUES_TO_QUOTE |
616
|
|
|
. ')="?([^"]*)"?$' |
617
|
|
|
. self::REGEX_DELIMITER; |
618
|
|
|
|
619
|
|
|
|
620
|
|
|
// Ok, lets read the file |
621
|
|
|
$content = ''; |
622
|
|
|
foreach ($browscap as $subject) { |
623
|
|
|
$subject = trim($subject); |
624
|
|
|
$content .= preg_replace($pattern, '$1="$2"', $subject) . "\n"; |
625
|
|
|
} |
626
|
|
|
|
627
|
|
|
if ($url != $path) { |
628
|
|
|
if (!file_put_contents($path, $content)) { |
629
|
|
|
throw new Exception("Could not write .ini content to $path"); |
630
|
|
|
} |
631
|
|
|
} |
632
|
|
|
|
633
|
|
|
return true; |
634
|
|
|
} |
635
|
|
|
|
636
|
|
|
/** |
637
|
|
|
* Gets the remote ini file update timestamp |
638
|
|
|
* |
639
|
|
|
* @throws Exception |
640
|
|
|
* @return int the remote modification timestamp |
641
|
|
|
*/ |
642
|
|
|
protected function _getRemoteMTime() |
643
|
|
|
{ |
644
|
|
|
$remote_datetime = $this->_getRemoteData($this->remoteVerUrl); |
645
|
|
|
$remote_tmstp = strtotime($remote_datetime); |
646
|
|
|
|
647
|
|
|
if (!$remote_tmstp) { |
648
|
|
|
throw new Exception("Bad datetime format from {$this->remoteVerUrl}"); |
649
|
|
|
} |
650
|
|
|
|
651
|
|
|
return $remote_tmstp; |
652
|
|
|
} |
653
|
|
|
|
654
|
|
|
/** |
655
|
|
|
* Gets the local ini file update timestamp |
656
|
|
|
* |
657
|
|
|
* @throws Exception |
658
|
|
|
* @return int the local modification timestamp |
659
|
|
|
*/ |
660
|
|
|
protected function _getLocalMTime() |
661
|
|
|
{ |
662
|
|
|
if (!is_readable($this->localFile) || !is_file($this->localFile)) { |
663
|
|
|
throw new Exception("Local file is not readable"); |
664
|
|
|
} |
665
|
|
|
|
666
|
|
|
return filemtime($this->localFile); |
667
|
|
|
} |
668
|
|
|
|
669
|
|
|
/** |
670
|
|
|
* Converts the given array to the PHP string which represent it. |
671
|
|
|
* This method optimizes the PHP code and the output differs form the |
672
|
|
|
* var_export one as the internal PHP function does not strip whitespace or |
673
|
|
|
* convert strings to numbers. |
674
|
|
|
* |
675
|
|
|
* @param array $array the array to parse and convert |
676
|
|
|
* @return string the array parsed into a PHP string |
677
|
|
|
*/ |
678
|
|
|
protected function _array2string($array) |
679
|
|
|
{ |
680
|
|
|
$strings = array(); |
681
|
|
|
|
682
|
|
|
foreach ($array as $key => $value) { |
683
|
|
|
if (is_int($key)) { |
684
|
|
|
$key = ''; |
685
|
|
|
} elseif (ctype_digit((string) $key) || strpos($key, '.0')) { |
686
|
|
|
$key = intval($key) . '=>' ; |
687
|
|
|
} else { |
688
|
|
|
$key = "'" . str_replace("'", "\'", $key) . "'=>" ; |
689
|
|
|
} |
690
|
|
|
|
691
|
|
|
if (is_array($value)) { |
692
|
|
|
$value = $this->_array2string($value); |
693
|
|
|
} elseif (ctype_digit((string) $value)) { |
694
|
|
|
$value = intval($value); |
695
|
|
|
} else { |
696
|
|
|
$value = "'" . str_replace("'", "\'", $value) . "'"; |
697
|
|
|
} |
698
|
|
|
|
699
|
|
|
$strings[] = $key . $value; |
700
|
|
|
} |
701
|
|
|
|
702
|
|
|
return 'array(' . implode(',', $strings) . ')'; |
703
|
|
|
} |
704
|
|
|
|
705
|
|
|
/** |
706
|
|
|
* Checks for the various possibilities offered by the current configuration |
707
|
|
|
* of PHP to retrieve external HTTP data |
708
|
|
|
* |
709
|
|
|
* @return string the name of function to use to retrieve the file |
710
|
|
|
*/ |
711
|
|
|
protected function _getUpdateMethod() |
712
|
|
|
{ |
713
|
|
|
// Caches the result |
714
|
|
|
if ($this->updateMethod === null) { |
715
|
|
|
if ($this->localFile !== null) { |
716
|
|
|
$this->updateMethod = self::UPDATE_LOCAL; |
717
|
|
|
} elseif (ini_get('allow_url_fopen') && function_exists('file_get_contents')) { |
718
|
|
|
$this->updateMethod = self::UPDATE_FOPEN; |
719
|
|
|
} elseif (function_exists('fsockopen')) { |
720
|
|
|
$this->updateMethod = self::UPDATE_FSOCKOPEN; |
721
|
|
|
} elseif (extension_loaded('curl')) { |
722
|
|
|
$this->updateMethod = self::UPDATE_CURL; |
723
|
|
|
} else { |
724
|
|
|
$this->updateMethod = false; |
725
|
|
|
} |
726
|
|
|
} |
727
|
|
|
|
728
|
|
|
return $this->updateMethod; |
729
|
|
|
} |
730
|
|
|
|
731
|
|
|
/** |
732
|
|
|
* Retrieve the data identified by the URL |
733
|
|
|
* |
734
|
|
|
* @param string $url the url of the data |
735
|
|
|
* @throws Exception |
736
|
|
|
* @return string the retrieved data |
737
|
|
|
*/ |
738
|
|
|
protected function _getRemoteData($url) |
739
|
|
|
{ |
740
|
|
|
ini_set('user_agent', $this->_getUserAgent()); |
741
|
|
|
|
742
|
|
|
switch ($this->_getUpdateMethod()) { |
743
|
|
|
case self::UPDATE_LOCAL: |
744
|
|
|
$file = file_get_contents($url); |
745
|
|
|
|
746
|
|
|
if ($file !== false) { |
747
|
|
|
return $file; |
748
|
|
|
} else { |
749
|
|
|
throw new Exception('Cannot open the local file'); |
750
|
|
|
} |
751
|
|
|
case self::UPDATE_FOPEN: |
752
|
|
|
// include proxy settings in the file_get_contents() call |
753
|
|
|
$context = $this->_getStreamContext(); |
754
|
|
|
$file = file_get_contents($url, false, $context); |
755
|
|
|
|
756
|
|
|
if ($file !== false) { |
757
|
|
|
return $file; |
758
|
|
|
} // else try with the next possibility (break omitted) |
759
|
|
|
case self::UPDATE_FSOCKOPEN: |
760
|
|
|
$remote_url = parse_url($url); |
761
|
|
|
$remote_handler = fsockopen($remote_url['host'], 80, $c, $e, $this->timeout); |
762
|
|
|
|
763
|
|
|
if ($remote_handler) { |
764
|
|
|
stream_set_timeout($remote_handler, $this->timeout); |
765
|
|
|
|
766
|
|
|
if (isset($remote_url['query'])) { |
767
|
|
|
$remote_url['path'] .= '?' . $remote_url['query']; |
768
|
|
|
} |
769
|
|
|
|
770
|
|
|
$out = sprintf( |
771
|
|
|
self::REQUEST_HEADERS, |
772
|
|
|
$remote_url['path'], |
773
|
|
|
$remote_url['host'], |
774
|
|
|
$this->_getUserAgent() |
775
|
|
|
); |
776
|
|
|
|
777
|
|
|
fwrite($remote_handler, $out); |
778
|
|
|
|
779
|
|
|
$response = fgets($remote_handler); |
780
|
|
|
if (strpos($response, '200 OK') !== false) { |
781
|
|
|
$file = ''; |
782
|
|
|
while (!feof($remote_handler)) { |
783
|
|
|
$file .= fgets($remote_handler); |
784
|
|
|
} |
785
|
|
|
|
786
|
|
|
$file = str_replace("\r\n", "\n", $file); |
787
|
|
|
$file = explode("\n\n", $file); |
788
|
|
|
array_shift($file); |
789
|
|
|
|
790
|
|
|
$file = implode("\n\n", $file); |
791
|
|
|
|
792
|
|
|
fclose($remote_handler); |
793
|
|
|
|
794
|
|
|
return $file; |
795
|
|
|
} |
796
|
|
|
} // else try with the next possibility |
797
|
|
|
case self::UPDATE_CURL: |
798
|
|
|
$ch = curl_init($url); |
799
|
|
|
|
800
|
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); |
801
|
|
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout); |
802
|
|
|
curl_setopt($ch, CURLOPT_USERAGENT, $this->_getUserAgent()); |
803
|
|
|
|
804
|
|
|
$file = curl_exec($ch); |
805
|
|
|
|
806
|
|
|
curl_close($ch); |
807
|
|
|
|
808
|
|
|
if ($file !== false) { |
809
|
|
|
return $file; |
810
|
|
|
} // else try with the next possibility |
811
|
|
|
case false: |
|
|
|
|
812
|
|
|
throw new Exception('Your server can\'t connect to external resources. Please update the file manually.'); |
813
|
|
|
} |
814
|
|
|
} |
815
|
|
|
|
816
|
|
|
/** |
817
|
|
|
* Format the useragent string to be used in the remote requests made by the |
818
|
|
|
* class during the update process. |
819
|
|
|
* |
820
|
|
|
* @return string the formatted user agent |
821
|
|
|
*/ |
822
|
|
|
protected function _getUserAgent() |
823
|
|
|
{ |
824
|
|
|
$ua = str_replace('%v', self::VERSION, $this->userAgent); |
825
|
|
|
$ua = str_replace('%m', $this->_getUpdateMethod(), $ua); |
826
|
|
|
|
827
|
|
|
return $ua; |
828
|
|
|
} |
829
|
|
|
} |
830
|
|
|
|
831
|
|
|
/** |
832
|
|
|
* Browscap.ini parsing class exception |
833
|
|
|
* |
834
|
|
|
* @package Browscap |
835
|
|
|
* @author Jonathan Stoppani <[email protected]> |
836
|
|
|
* @copyright Copyright (c) 2006-2012 Jonathan Stoppani |
837
|
|
|
* @version 1.0 |
838
|
|
|
* @license http://www.opensource.org/licenses/MIT MIT License |
839
|
|
|
* @link https://github.com/GaretJax/phpbrowscap/*/ |
840
|
|
|
class Exception extends BaseException |
841
|
|
|
{} |
842
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.