Completed
Pull Request — development (#2329)
by Joshua
09:15
created

Browser_Detector::detectBrowser()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 38
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 6
Metric Value
dl 0
loc 38
ccs 21
cts 21
cp 1
rs 8.439
cc 6
eloc 20
nc 10
nop 0
crap 6
1
<?php
2
3
/**
4
 * Used to detect user browsers, evil but sometimes necessary
5
 *
6
 * @name      ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
9
 *
10
 * This software is a derived product, based on:
11
 *
12
 * Simple Machines Forum (SMF)
13
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
14
 * license:		BSD, See included LICENSE.TXT for terms and conditions.
15
 *
16
 * @version 1.1 dev
17
 *
18
 */
19
20
if (!defined('ELK'))
21
	die('No access...');
22
23
/**
24
 * This class is an experiment for the job of correctly detecting browsers and
25
 * settings needed for them.
26
 *
27
 * What it does:
28
 * - Detects the following browsers
29
 * - Opera, Webkit, Firefox, Web_tv, Konqueror, IE, Gecko
30
 * - Webkit variants: Chrome, iphone, blackberry, android, safari, ipad, ipod
31
 * - Opera Versions: 6, 7, 8 ... 10 ... and mobile mini and mobi
32
 * - Firefox Versions: 1, 2, 3 .... 11 ...
33
 * - Chrome Versions: 1 ... 18 ...
34
 * - IE Versions: 4, 5, 5.5, 6, 7, 8, 9, 10 ... mobile and Mac
35
 * - Nokia
36
 * - Basic mobile and tablet (ipad, android and tablet PC)
37
 */
38
class Browser_Detector
39
{
40
	/**
41
	 * Holds all browsers information. Its contents will be placed into $context['browser'].
42
	 *
43
	 * @var array
44
	 */
45
	private $_browsers = null;
46
47
	/**
48
	 * Holds if the detected device may be a mobile one
49
	 *
50
	 * @var boolean
51
	 */
52
	private $_is_mobile = null;
53
54
	/**
55
	 * Holds if the detected device may be a tablet
56
	 *
57
	 * @var boolean
58
	 */
59
	private $_is_tablet = null;
60
61
	/**
62
	 * User agent
63
	 *
64
	 * @var string
65
	 */
66
	protected $_ua = null;
67
68
	/**
69
	 * The main method of this class, you know the one that does the job: detect the thing.
70
	 *
71
	 * What it does:
72
	 * - Determines the user agent (browser) as best it can.
73
	 * - The method fills the instance variables _is_mobile and _is_tablet,
74
	 * and the _browsers array.
75
	 * - When it returns, the Browser_Detector can be queried for information on client browser.
76
	 * - It also attempts to detect if the client is a robot.
77
	 */
78 2
	public function detectBrowser()
79
	{
80 2
		global $context, $user_info;
81
82
		// Init
83 2
		$this->_browsers = array();
84 2
		$this->_is_mobile = false;
85 2
		$this->_is_tablet = false;
86
87
		// Saves us many many calls
88 2
		$req = request();
89 2
		$this->_ua = empty($this->_ua) ? $req->user_agent() : $this->_ua;
90
91
		// One at a time, one at a time, and in this order too
92 2
		if ($this->isOpera())
93 2
			$this->_setupOpera();
94
		// Them webkits need to be set up too
95 2
		elseif ($this->isWebkit())
96 1
			$this->_setupWebkit();
97
		// We may have work to do on Firefox...
98 2
		elseif ($this->isFirefox())
99 1
			$this->_setupFirefox();
100
		// Old friend, old frenemy
101 2
		elseif ($this->isIe())
102 1
			$this->_setupIe();
103
104
		// Just a few mobile checks
105 2
		$this->isOperaMini();
106 2
		$this->isOperaMobi();
107
108 2
		$this->isPossibleRobot();
109
110
		// Last step ...
111 2
		$this->_setupBrowserPriority();
112
113
		// Now see what you've done!
114 2
		$context['browser'] = $this->_browsers;
115 2
	}
116
117
	/**
118
	 * Determine if the browser is Opera or not (for Opera < 15)
119
	 *
120
	 * @return boolean true if the browser is Opera otherwise false
121
	 */
122 2
	public function isOpera()
123
	{
124 2
		if (!isset($this->_browsers['is_opera']))
125 2
			$this->_browsers['is_opera'] = strpos($this->_ua, 'Opera') !== false;
126
127 2
		return $this->_browsers['is_opera'];
128
	}
129
130
	/**
131
	 * Determine if the browser is IE or not
132
	 *
133
	 * @return boolean true if the browser is IE otherwise false
134
	 */
135 2
	public function isIe()
136
	{
137
		// I'm IE, Yes I'm the real IE; All you other IEs are just imitating.
138 2
		if (!isset($this->_browsers['is_ie']))
139 2
			$this->_browsers['is_ie'] = !$this->isOpera() && !$this->isGecko() && !$this->isWebTv() && (preg_match('~Trident/\d+~', $this->_ua) === 1 || preg_match('~MSIE \d+~', $this->_ua) === 1);
140
141 2
		return $this->_browsers['is_ie'];
142
	}
143
144
	/**
145
	 * Determine if the browser is a Webkit based one or not
146
	 *
147
	 * @return boolean true if the browser is Webkit based otherwise false
148
	 */
149 2
	public function isWebkit()
150
	{
151 2
		if (!isset($this->_browsers['is_webkit']))
152 2
			$this->_browsers['is_webkit'] = strpos($this->_ua, 'AppleWebKit') !== false;
153
154 2
		return $this->_browsers['is_webkit'];
155
	}
156
157
	/**
158
	 * Determine if the browser is Firefox or one of its variants
159
	 *
160
	 * @return boolean true if the browser is Firefox otherwise false
161
	 */
162 2
	public function isFirefox()
163
	{
164 2
		if (!isset($this->_browsers['is_firefox']))
165 2
			$this->_browsers['is_firefox'] = preg_match('~(?:Firefox|Ice[wW]easel|IceCat|Shiretoko|Minefield)/~', $this->_ua) === 1 && $this->isGecko();
166
167 2
		return $this->_browsers['is_firefox'];
168
	}
169
170
	/**
171
	 * Determine if the browser is WebTv or not
172
	 *
173
	 * @return boolean true if the browser is WebTv otherwise false
174
	 */
175 2
	public function isWebTv()
176
	{
177 2
		if (!isset($this->_browsers['is_web_tv']))
178 2
			$this->_browsers['is_web_tv'] = strpos($this->_ua, 'WebTV') !== false;
179
180 2
		return $this->_browsers['is_web_tv'];
181
	}
182
183
	/**
184
	 * Determine if the browser is konqueror or not
185
	 *
186
	 * @return boolean true if the browser is konqueror otherwise false
187
	 */
188 1
	public function isKonqueror()
189
	{
190 1
		if (!isset($this->_browsers['is_konqueror']))
191 1
			$this->_browsers['is_konqueror'] = strpos($this->_ua, 'Konqueror') !== false;
192
193 1
		return $this->_browsers['is_konqueror'];
194
	}
195
196
	/**
197
	 * Determine if the browser is Gecko or not
198
	 *
199
	 * @return boolean true if the browser is Gecko otherwise false
200
	 */
201 2
	public function isGecko()
202
	{
203 2
		if (!isset($this->_browsers['is_gecko']))
204 2
			$this->_browsers['is_gecko'] = strpos($this->_ua, 'Gecko') !== false && strpos($this->_ua, 'like Gecko') === false && !$this->isWebkit() && !$this->isKonqueror();
205
206 2
		return $this->_browsers['is_gecko'];
207
	}
208
209
	/**
210
	 * Determine if the browser is OperaMini or not
211
	 *
212
	 * @return boolean true if the browser is OperaMini otherwise false
213
	 */
214 2
	public function isOperaMini()
0 ignored issues
show
Coding Style introduced by
isOperaMini uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
215
	{
216 2
		if (!isset($this->_browsers['is_opera_mini']))
217 2
			$this->_browsers['is_opera_mini'] = (isset($_SERVER['HTTP_X_OPERAMINI_PHONE_UA']) || stripos($this->_ua, 'opera mini') !== false);
218 2
		if ($this->_browsers['is_opera_mini'])
219 2
			$this->_is_mobile = true;
220
221 2
		return $this->_browsers['is_opera_mini'];
222
	}
223
224
	/**
225
	 * Determine if the browser is OperaMobi or not
226
	 *
227
	 * @return boolean true if the browser is OperaMobi otherwise false
228
	 */
229 2
	public function isOperaMobi()
230
	{
231 2
		if (!isset($this->_browsers['is_opera_mobi']))
232 2
			$this->_browsers['is_opera_mobi'] = stripos($this->_ua, 'opera mobi') !== false;
233 2
		if ($this->_browsers['is_opera_mobi'])
234 2
			$this->_is_mobile = true;
235
236 2
		return $this->_browsers['is_opera_mini'];
237
	}
238
239
	/**
240
	 * Determine if the browser is possibly a robot or not
241
	 *
242
	 * @return boolean true if the browser is possibly a robot otherwise false
243
	 */
244 2
	public function isPossibleRobot()
0 ignored issues
show
Coding Style introduced by
isPossibleRobot uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
245
	{
246 2
		global $user_info;
247
248
		// Be you robot or human?
249 2
		if (!isset($this->_browsers['possibly_robot']))
250 2
		{
251 2
			if ($user_info['possibly_robot']) {
252
				// This isn't meant to be reliable, it's just meant to catch most bots to prevent PHPSESSID from showing up.
253 2
				$this->_browsers['possibly_robot'] = !empty($user_info['possibly_robot']);
254
255
				// Robots shouldn't be logging in or registering.  So, they aren't a bot.  Better to be wrong than sorry (or people won't be able to log in!), anyway.
256 2
				if ((isset($_REQUEST['action']) && in_array($_REQUEST['action'], array('login', 'login2', 'register'))) || !$user_info['is_guest'])
257 2
					$this->_browsers['possibly_robot'] = false;
258 2
			} else
259
				$this->_browsers['possibly_robot'] = false;
260 2
		}
261
262 2
		return $this->_browsers['possibly_robot'];
263
	}
264
265
	/**
266
	 * Detect Safari / Chrome / Opera / iP[ao]d / iPhone / Android / Blackberry from webkit.
267
	 *
268
	 * What it does:
269
	 * - set the browser version for Safari and Chrome
270
	 * - set the mobile flag for mobile based useragents
271
	 */
272 1
	private function _setupWebkit()
273
	{
274
		$this->_browsers += array(
275 1
			'is_iphone' => (strpos($this->_ua, 'iPhone') !== false || strpos($this->_ua, 'iPod') !== false) && strpos($this->_ua, 'iPad') === false,
276 1
			'is_blackberry' => stripos($this->_ua, 'BlackBerry') !== false || strpos($this->_ua, 'PlayBook') !== false,
277 1
			'is_android' => strpos($this->_ua, 'Android') !== false,
278 1
			'is_nokia' => strpos($this->_ua, 'SymbianOS') !== false,
279 1
			'is_ipad' => strpos($this->_ua, 'iPad') !== false,
280
		);
281
282
		// blackberry, playbook, iphone, nokia, android and ipods set a mobile flag
283 1
		if ($this->_browsers['is_iphone'] || $this->_browsers['is_blackberry'] || $this->_browsers['is_android'] || $this->_browsers['is_nokia'])
284 1
			$this->_is_mobile = true;
285
286
		// iPad and droid tablets get a tablet flag
287 1
		$this->_browsers['is_android_tablet'] = $this->_browsers['is_android'] && strpos($this->_ua, 'Mobile') === false;
288 1
		if ($this->_browsers['is_ipad'] || $this->_browsers['is_android_tablet'])
289 1
			$this->_is_tablet = true;
290
291
		// Prevent some repetition here
292 1
		$_chrome = strpos($this->_ua, 'Chrome');
293 1
		$_opera = strpos($this->_ua, ' OPR/');
294 1
		$_safari = strpos($this->_ua, 'Safari');
295
296
		// Chrome's ua contains Safari but Safari's ua doesn't contain Chrome, Operas contains OPR, chome and safari
297 1
		$this->_browsers['is_opera'] = $_opera !== false;
298 1
		$this->_browsers['is_chrome'] = $_opera === false && $_chrome !== false && $_safari !== false;
299 1
		$this->_browsers['is_safari'] = $_safari !== false && $_chrome === false && !$this->_browsers['is_iphone'];
300
301
		// if Chrome, get the major version
302 1
		if ($this->_browsers['is_chrome'])
303 1
		{
304 1
			if (preg_match('~chrome[/]([0-9][0-9]?[.])~i', $this->_ua, $match) === 1)
305 1
				$this->_browsers['is_chrome' . (int) $match[1]] = true;
306 1
		}
307
		// if Safari get its major version
308 1
		elseif ($this->_browsers['is_safari'])
309
		{
310 1
			if (preg_match('~version/?(.*)safari.*~i', $this->_ua, $match) === 1)
311 1
				$this->_browsers['is_safari' . (int) trim($match[1])] = true;
312 1
		}
313
		// if Opera get its major version
314 1
		elseif ($this->_browsers['is_opera'])
315
		{
316
			if (preg_match('~OPR[/]([0-9][0-9]?[.])~i', $this->_ua, $match) === 1)
317
				$this->_browsers['is_opera' . (int) trim($match[1])] = true;
318
319
			// Since opera >= 15 wants to look like chrome, set the body to do just that
320
			if (trim($match[1]) >= 15)
321
				$this->_browsers['is_chrome'] = true;
322
		}
323 1
	}
324
325
	/**
326
	 * Additional IE checks and settings.
327
	 *
328
	 * What it does:
329
	 * - determines the version of the IE browser in use
330
	 * - detects ie4 onward
331
	 * - attempts to distinguish between IE and IE in compatabilty view
332
	 */
333 1
	private function _setupIe()
334
	{
335 1
		$this->_browsers['is_ie_compat_view'] = false;
336
337
		// get the version of the browser from the msie tag
338 1
		if (preg_match('~MSIE\s?([0-9][0-9]?.[0-9])~i', $this->_ua, $msie_match) === 1)
339 1
		{
340 1
			$msie_match[1] = trim($msie_match[1]);
341 1
			$msie_match[1] = (($msie_match[1] - (int) $msie_match[1]) == 0) ? (int) $msie_match[1] : $msie_match[1];
342 1
			$this->_browsers['is_ie' . $msie_match[1]] = true;
343 1
		}
344
345
		// "modern" ie uses trident 4=ie8, 5=ie9, 6=ie10, even in compatability view
346 1
		if (preg_match('~Trident/([0-9.])~i', $this->_ua, $trident_match) === 1)
347 1
		{
348 1
			$this->_browsers['is_ie' . ((int) $trident_match[1] + 4)] = true;
349
350
			// If trident is set, see the (if any) msie tag in the user agent matches ... if not its in some compatablity view
351 1
			if (isset($msie_match[1]) && ($msie_match[1] < $trident_match[1] + 4))
352 1
				$this->_browsers['is_ie_compat_view'] = true;
353 1
		}
354
355
		// IE mobile, ... shucks why not
356 1
		if (preg_match('~IEMobile/[0-9][0-9]?\.[0-9]~i', $this->_ua, $msie_match) === 1)
357 1
		{
358
			$this->_browsers['is_ie_mobi'] = true;
359
			$this->_is_mobile = true;
360
		}
361
362
		// Tablets as well, someone may win one
363 1
		if (strpos($this->_ua, 'Tablet PC') !== false)
364 1
		{
365
			$this->_browsers['is_tablet_pc'] = true;
366
			$this->_is_tablet = true;
367
		}
368 1
	}
369
370
	/**
371
	 * Additional firefox checks.
372
	 *
373
	 * What it does:
374
	 * - Gets the version of the FF browser in use
375
	 * - Considers all FF variants as FF including IceWeasel, IceCat, Shiretoko and Minefiled
376
	 */
377 1
	private function _setupFirefox()
378
	{
379 1
		if (preg_match('~(?:Firefox|Ice[wW]easel|IceCat|Shiretoko|Minefield)[\/ \(]([^ ;\)]+)~', $this->_ua, $match) === 1)
380 1
			$this->_browsers['is_firefox' . (int) $match[1]] = true;
381
382
		// Firefox mobile
383 1
		if (strpos($this->_ua, 'Android; Mobile;') !== false)
384 1
		{
385
			$this->_browsers['is_android'] = true;
386
			$this->_is_mobile = true;
387
		}
388
		// Tablets as well
389 1
		elseif (strpos($this->_ua, 'Android; Tablet;') !== false)
390
		{
391
			$this->_browsers['is_android'] = true;
392
			$this->_browsers['is_android_tablet'] = true;
393
			$this->_is_tablet = true;
394
		}
395 1
	}
396
397
	/**
398
	 * More Opera checks if we are opera <15
399
	 *
400
	 * What it does:
401
	 * - checks for the version of Opera in use
402
	 * - uses checks for 10 first and falls through to <9
403
	 */
404 1
	private function _setupOpera()
405
	{
406
		// Opera 10+ uses the version tag at the end of the string
407 1
		if (preg_match('~\sVersion/([0-9]+)\.[0-9]+(?:\s*|$)~', $this->_ua, $match))
408 1
			$this->_browsers['is_opera' . (int) $match[1]] = true;
409
		// Opera pre 10 is supposed to uses the Opera tag alone, as do some spoofers
410 1
		elseif (preg_match('~Opera[ /]([0-9]+)(?!\\.[89])~', $this->_ua, $match))
411 1
			$this->_browsers['is_opera' . (int) $match[1]] = true;
412 1
	}
413
414
	/**
415
	 * Get the browser name that we will use in the <body id="this_browser">
416
	 *
417
	 * What it does:
418
	 * - The order of each browser in $browser_priority is important
419
	 * - if you want to have id='ie6' and not id='ie' then it must appear first in the list of ie browsers
420
	 * - only sets browsers that may need some help via css for compatablity
421
	 */
422 2
	private function _setupBrowserPriority()
423
	{
424 2
		global $context;
425
426 2
		if ($this->_is_mobile && !$this->_is_tablet)
427 2
			$context['browser_body_id'] = 'mobile';
428 2
		elseif ($this->_is_tablet)
429 1
			$context['browser_body_id'] = 'tablet';
430
		else
431
		{
432
			// Add in any specific detection conversions here if you want a special body id e.g. 'is_opera9' => 'opera9'
433
			$browser_priority = array(
434 2
				'is_ie8' => 'ie8',
435 2
				'is_ie9' => 'ie9',
436 2
				'is_ie' => 'ie',
437 2
				'is_firefox' => 'firefox',
438 2
				'is_chrome' => 'chrome',
439 2
				'is_safari' => 'safari',
440 2
				'is_opera' => 'opera',
441 2
				'is_konqueror' => 'konqueror',
442 2
			);
443
444 2
			$context['browser_body_id'] = 'elkarte';
445 2
			$active = array_reverse(array_keys($this->_browsers, true));
446 2
			foreach ($active as $key => $browser)
447
			{
448 2
				if (array_key_exists($browser, $browser_priority))
449 2
				{
450 1
					$context['browser_body_id'] = $browser_priority[$browser];
451 1
					break;
452
				}
453 2
			}
454
		}
455
	}
456
}