Completed
Pull Request — master (#2)
by ARCANEDEV
03:23
created

Agent::isDesktop()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 3
eloc 2
nc 3
nop 0
crap 3
1
<?php namespace Arcanedev\Agent;
2
3
use Arcanedev\Agent\Contracts\Agent as AgentContract;
4
use Arcanedev\Agent\Detectors\CrawlerDetector;
5
use Illuminate\Support\Str;
6
use Mobile_Detect;
7
8
/**
9
 * Class     Agent
10
 *
11
 * @package  Arcanedev\Agent
12
 * @author   ARCANEDEV <[email protected]>
13
 */
14
class Agent extends Mobile_Detect implements AgentContract
15
{
16
    /* -----------------------------------------------------------------
17
     |  Properties
18
     | -----------------------------------------------------------------
19
     */
20
21
    /**
22
     * List of desktop devices.
23
     *
24
     * @var array
25
     */
26
    protected static $additionalDevices = [
27
        'Macintosh' => 'Macintosh',
28
    ];
29
30
    /**
31
     * List of additional operating systems.
32
     *
33
     * @var array
34
     */
35
    protected static $additionalOperatingSystems = [
36
        'Windows'    => 'Windows',
37
        'Windows NT' => 'Windows NT',
38
        'OS X'       => 'Mac OS X',
39
        'Debian'     => 'Debian',
40
        'Ubuntu'     => 'Ubuntu',
41
        'Macintosh'  => 'PPC',
42
        'OpenBSD'    => 'OpenBSD',
43
        'Linux'      => 'Linux',
44
        'ChromeOS'   => 'CrOS',
45
    ];
46
47
    /**
48
     * List of additional browsers.
49
     * Note: 'Vivaldi' must be above Chrome, otherwise it'll fail.
50
     *
51
     * @var array
52
     */
53
    protected static $additionalBrowsers = [
54
        'Opera'    => 'Opera|OPR',
55
        'Edge'     => 'Edge',
56
        'Vivaldi'  => 'Vivaldi',
57
        'Chrome'   => 'Chrome',
58
        'Firefox'  => 'Firefox',
59
        'Safari'   => 'Safari',
60
        'IE'       => 'MSIE|IEMobile|MSIEMobile|Trident/[.0-9]+',
61
        'Netscape' => 'Netscape',
62
        'Mozilla'  => 'Mozilla',
63
    ];
64
65
    /**
66
     * List of additional properties.
67
     *
68
     * @var array
69
     */
70
    protected static $additionalProperties = [
71
        // Operating systems
72
        'Windows'      => 'Windows NT [VER]',
73
        'Windows NT'   => 'Windows NT [VER]',
74
        'OS X'         => 'OS X [VER]',
75
        'BlackBerryOS' => ['BlackBerry[\w]+/[VER]', 'BlackBerry.*Version/[VER]', 'Version/[VER]'],
76
        'AndroidOS'    => 'Android [VER]',
77
        'ChromeOS'     => 'CrOS x86_64 [VER]',
78
79
        // Browsers
80
        'Opera'    => [' OPR/[VER]', 'Opera Mini/[VER]', 'Version/[VER]', 'Opera [VER]'],
81
        'Netscape' => 'Netscape/[VER]',
82
        'Mozilla'  => 'rv:[VER]',
83
        'IE'       => ['IEMobile/[VER];', 'IEMobile [VER]', 'MSIE [VER];', 'rv:[VER]'],
84
        'Edge'     => 'Edge/[VER]',
85
        'Vivaldi'  => 'Vivaldi/[VER]',
86
    ];
87
88
    /**
89
     * Crawler detector instance.
90
     *
91
     * @var \Arcanedev\Agent\Detectors\CrawlerDetector
92
     */
93
    protected static $crawlerDetector;
94
95
    /* -----------------------------------------------------------------
96
     |  Getters & Setters
97
     | -----------------------------------------------------------------
98
     */
99
100
    /**
101
     * Get the crawler detector.
102
     *
103
     * @return \Arcanedev\Agent\Detectors\CrawlerDetector
104
     */
105 27
    public function getCrawlerDetector()
106
    {
107 27
        if (self::$crawlerDetector === null) {
108 3
            self::$crawlerDetector = new CrawlerDetector;
109 1
        }
110
111 27
        return self::$crawlerDetector;
112
    }
113
114
    /**
115
     * Get all detection rules. These rules include the additional
116
     * platforms and browsers.
117
     *
118
     * @return array
119
     */
120 60
    public function getDetectionRulesExtended()
121
    {
122 60
        static $rules;
123
124 60
        if ( ! $rules) {
125 3
            $rules = $this->mergeRules(
126 3
                static::$additionalDevices, // NEW
127 3
                static::getPhoneDevices(),
128 3
                static::getTabletDevices(),
129 3
                static::getOperatingSystems(),
130 3
                static::$additionalOperatingSystems, // NEW
131 3
                static::getBrowsers(),
132 3
                static::$additionalBrowsers, // NEW
133 3
                static::getUtilities()
134 1
            );
135 1
        }
136
137 60
        return $rules;
138
    }
139
140
    /**
141
     * Retrieve the current set of rules.
142
     *
143
     * @return array
144
     */
145 81
    public function getRules()
146
    {
147 81
        return $this->detectionType == static::DETECTION_TYPE_EXTENDED
0 ignored issues
show
Deprecated Code introduced by
The property Mobile_Detect::$detectionType has been deprecated with message: since version 2.6.9

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
Deprecated Code introduced by
The constant Mobile_Detect::DETECTION_TYPE_EXTENDED has been deprecated with message: since version 2.6.9

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
148 67
            ? static::getDetectionRulesExtended()
149 81
            : parent::getRules();
0 ignored issues
show
Deprecated Code introduced by
The method Mobile_Detect::getRules() has been deprecated with message: since version 2.6.9

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
150
    }
151
152
    /**
153
     * Get the device name.
154
     *
155
     * @param  string|null  $userAgent
156
     *
157
     * @return string
158
     */
159 6
    public function device($userAgent = null)
160
    {
161
        // Get device rules
162 6
        $rules = $this->mergeRules(
163 6
            static::$additionalDevices, // NEW
164 6
            static::$phoneDevices,
165 6
            static::$tabletDevices,
166 4
            static::$utilities
167 2
        );
168
169 6
        return $this->findDetectionRulesAgainstUA($rules, $userAgent);
0 ignored issues
show
Bug introduced by
It seems like $userAgent defined by parameter $userAgent on line 159 can also be of type string; however, Arcanedev\Agent\Agent::f...tectionRulesAgainstUA() does only seem to accept null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
170
    }
171
172
    /**
173
     * Get the browser name.
174
     *
175
     * @param  string|null  $userAgent
176
     *
177
     * @return string
178
     */
179 36
    public function browser($userAgent = null)
180
    {
181
        // Get browser rules
182 36
        $rules = $this->mergeRules(
183 36
            static::$additionalBrowsers, // NEW
184 24
            static::$browsers
185 12
        );
186
187 36
        return $this->findDetectionRulesAgainstUA($rules, $userAgent);
0 ignored issues
show
Bug introduced by
It seems like $userAgent defined by parameter $userAgent on line 179 can also be of type string; however, Arcanedev\Agent\Agent::f...tectionRulesAgainstUA() does only seem to accept null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
188
    }
189
190
    /**
191
     * Get the robot name.
192
     *
193
     * @param  string|null  $userAgent
194
     *
195
     * @return string
196
     */
197 15
    public function robot($userAgent = null)
198
    {
199 15
        return $this->isRobot($userAgent)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->isRobot($userAgen...>getMatches()) : false; of type string|false adds false to the return on line 199 which is incompatible with the return type declared by the interface Arcanedev\Agent\Contracts\Agent::robot of type string. It seems like you forgot to handle an error condition.
Loading history...
200 15
            ? ucfirst($this->getCrawlerDetector()->getMatches())
201 15
            : false;
202
    }
203
204
    /**
205
     * Get the platform name.
206
     *
207
     * @param  string|null  $userAgent
208
     *
209
     * @return string
210
     */
211 24
    public function platform($userAgent = null)
212
    {
213
        // Get platform rules
214 24
        $rules = $this->mergeRules(
215 24
            static::$operatingSystems,
216 16
            static::$additionalOperatingSystems // NEW
217 8
        );
218
219 24
        return $this->findDetectionRulesAgainstUA($rules, $userAgent);
0 ignored issues
show
Bug introduced by
It seems like $userAgent defined by parameter $userAgent on line 211 can also be of type string; however, Arcanedev\Agent\Agent::f...tectionRulesAgainstUA() does only seem to accept null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
220
    }
221
222
    /**
223
     * Get the languages.
224
     *
225
     * @param  string|null  $acceptLanguage
226
     *
227
     * @return array
228
     */
229 6
    public function languages($acceptLanguage = null)
230
    {
231 6
        if ( ! $acceptLanguage) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $acceptLanguage of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
232 6
            $acceptLanguage = $this->getHttpHeader('HTTP_ACCEPT_LANGUAGE');
233 2
        }
234
235 6
        $languages = [];
236
237 6
        if ($acceptLanguage) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $acceptLanguage of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
238
            // Parse accept language string.
239 6
            foreach (explode(',', $acceptLanguage) as $piece) {
240 6
                $parts = explode(';', $piece);
241 6
                $language = strtolower($parts[0]);
242 6
                $priority = empty($parts[1]) ? 1. : floatval(str_replace('q=', '', $parts[1]));
243 6
                $languages[$language] = $priority;
244 2
            }
245
246
            // Sort languages by priority.
247 6
            arsort($languages);
248
249 6
            $languages = array_keys($languages);
250 2
        }
251
252 6
        return $languages;
253
    }
254
255
    /**
256
     * Match a detection rule and return the matched key.
257
     *
258
     * @param  array  $rules
259
     * @param  null   $userAgent
260
     *
261
     * @return string
262
     */
263 66
    protected function findDetectionRulesAgainstUA(array $rules, $userAgent = null)
264
    {
265
        // Loop given rules
266 66
        foreach ($rules as $key => $regex) {
267
            // Check match
268 66
            if ( ! empty($regex) && $this->match($regex, $userAgent))
269 66
                return $key ?: reset($this->matchesArray);
270 18
        }
271
272
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Arcanedev\Agent\Agent::findDetectionRulesAgainstUA of type string.

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:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
273
    }
274
275
    /* -----------------------------------------------------------------
276
     |  Main Methods
277
     | -----------------------------------------------------------------
278
     */
279
280
    /**
281
     * Check the version of the given property in the User-Agent.
282
     * Will return a float number. (eg. 2_0 will return 2.0, 4.3.1 will return 4.31)
283
     *
284
     * @param  string  $propertyName  The name of the property. See self::getProperties() array
285
     *                                keys for all possible properties.
286
     * @param  string  $type          Either self::VERSION_TYPE_STRING to get a string value or
287
     *                                self::VERSION_TYPE_FLOAT indicating a float value. This parameter is optional
288
     *                                and defaults to self::VERSION_TYPE_STRING. Passing an invalid parameter will
289
     *                                default to the this type as well.
290
     *
291
     * @return string|float The version of the property we are trying to extract.
292
     */
293 6
    public function version($propertyName, $type = self::VERSION_TYPE_STRING)
294
    {
295 6
        $check = key(static::$additionalProperties);
296
297
        // Check if the additional properties have been added already
298 6
        if ( ! array_key_exists($check, static::$properties)) {
299
            // TODO: why is mergeRules not working here?
300 3
            static::$properties = array_merge(
301 3
                static::$properties,
302 2
                static::$additionalProperties
303 1
            );
304 1
        }
305
306 6
        return parent::version($propertyName, $type);
307
    }
308
309
    /* -----------------------------------------------------------------
310
     |  Check Methods
311
     | -----------------------------------------------------------------
312
     */
313
314
    /**
315
     * Check if the device is a desktop computer.
316
     *
317
     * @return bool
318
     */
319 27
    public function isDesktop()
320
    {
321 27
        return ! ($this->isMobile() || $this->isTablet() || $this->isRobot());
322
    }
323
324
    /**
325
     * Check if device is a robot.
326
     *
327
     * @param  string|null  $userAgent
328
     *
329
     * @return bool
330
     */
331 27
    public function isRobot($userAgent = null)
332
    {
333 27
        return $this->getCrawlerDetector()->isCrawler($userAgent ?: $this->userAgent);
334
    }
335
336
    /**
337
     * Check if the device is a mobile phone.
338
     *
339
     * @return bool
340
     */
341 24
    public function isPhone()
342
    {
343 24
        return $this->isMobile() && ! $this->isTablet();
344
    }
345
346
    /* -----------------------------------------------------------------
347
     |  Other Methods
348
     | -----------------------------------------------------------------
349
     */
350
351
    /**
352
     * Merge multiple rules into one array.
353
     *
354
     * @param  array  $rulesGroups
355
     *
356
     * @return array
357
     */
358 66
    protected function mergeRules(...$rulesGroups)
359
    {
360 66
        $merged = [];
361
362 66
        foreach ($rulesGroups as $rules) {
363 66
            foreach ($rules as $key => $value) {
364 66
                if (empty($merged[$key]))
365 66
                    $merged[$key] = $value;
366 36
                elseif (is_array($merged[$key]))
367
                    $merged[$key][] = $value;
368
                else
369 56
                    $merged[$key] .= '|' . $value;
370 22
            }
371 22
        }
372
373 66
        return $merged;
374
    }
375
376
    /**
377
     * Changing detection type to extended.
378
     *
379
     * @inherit
380
     *
381
     * @param  string  $name
382
     * @param  array   $arguments
383
     *
384
     * @return bool|mixed
385
     */
386 60
    public function __call($name, $arguments)
387
    {
388
        // Make sure the name starts with 'is', otherwise
389 60
        if ( ! Str::startsWith($name, ['is'])) {
390 3
            throw new \BadMethodCallException("No such method exists: $name");
391
        }
392
393 57
        $this->setDetectionType(self::DETECTION_TYPE_EXTENDED);
0 ignored issues
show
Deprecated Code introduced by
The constant Mobile_Detect::DETECTION_TYPE_EXTENDED has been deprecated with message: since version 2.6.9

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
Deprecated Code introduced by
The method Mobile_Detect::setDetectionType() has been deprecated with message: since version 2.6.9

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
394
395 57
        return $this->matchUAAgainstKey(substr($name, 2));
396
    }
397
}
398