Completed
Pull Request — master (#3)
by ARCANEDEV
04:19
created

Agent::isRobot()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 1
nop 1
crap 2
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
        'UCBrowser' => 'UCBrowser',
57
        'Vivaldi'   => 'Vivaldi',
58
        'Chrome'    => 'Chrome',
59
        'Firefox'   => 'Firefox',
60
        'Safari'    => 'Safari',
61
        'IE'        => 'MSIE|IEMobile|MSIEMobile|Trident/[.0-9]+',
62
        'Netscape'  => 'Netscape',
63
        'Mozilla'   => 'Mozilla',
64
    ];
65
66
    /**
67
     * List of additional properties.
68
     *
69
     * @var array
70
     */
71
    protected static $additionalProperties = [
72
        // Operating systems
73
        'Windows'      => 'Windows NT [VER]',
74
        'Windows NT'   => 'Windows NT [VER]',
75
        'OS X'         => 'OS X [VER]',
76
        'BlackBerryOS' => ['BlackBerry[\w]+/[VER]', 'BlackBerry.*Version/[VER]', 'Version/[VER]'],
77
        'AndroidOS'    => 'Android [VER]',
78
        'ChromeOS'     => 'CrOS x86_64 [VER]',
79
80
        // Browsers
81
        'Opera'    => [' OPR/[VER]', 'Opera Mini/[VER]', 'Version/[VER]', 'Opera [VER]'],
82
        'Netscape' => 'Netscape/[VER]',
83
        'Mozilla'  => 'rv:[VER]',
84
        'IE'       => ['IEMobile/[VER];', 'IEMobile [VER]', 'MSIE [VER];', 'rv:[VER]'],
85
        'Edge'     => 'Edge/[VER]',
86
        'Vivaldi'  => 'Vivaldi/[VER]',
87
    ];
88
89
    /**
90
     * Crawler detector instance.
91
     *
92
     * @var \Arcanedev\Agent\Detectors\CrawlerDetector
93
     */
94
    protected static $crawlerDetector;
95
96
    /* -----------------------------------------------------------------
97
     |  Getters & Setters
98
     | -----------------------------------------------------------------
99
     */
100
101
    /**
102
     * Get the crawler detector.
103
     *
104
     * @return \Arcanedev\Agent\Detectors\CrawlerDetector
105
     */
106 18
    public function getCrawlerDetector()
107
    {
108 18
        if (self::$crawlerDetector === null) {
109 2
            self::$crawlerDetector = new CrawlerDetector;
110
        }
111
112 18
        return self::$crawlerDetector;
113
    }
114
115
    /**
116
     * Get all detection rules. These rules include the additional
117
     * platforms and browsers.
118
     *
119
     * @return array
120
     */
121 42
    public function getDetectionRulesExtended()
122
    {
123 42
        static $rules;
124
125 42
        if ( ! $rules) {
126 2
            $rules = $this->mergeRules(
127 2
                static::$additionalDevices, // NEW
128 2
                static::getPhoneDevices(),
129 2
                static::getTabletDevices(),
130 2
                static::getOperatingSystems(),
131 2
                static::$additionalOperatingSystems, // NEW
132 2
                static::getBrowsers(),
133 2
                static::$additionalBrowsers, // NEW
134 2
                static::getUtilities()
135
            );
136
        }
137
138 42
        return $rules;
139
    }
140
141
    /**
142
     * Retrieve the current set of rules.
143
     *
144
     * @return array
145
     */
146 56
    public function getRules()
147
    {
148 56
        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...
149 42
            ? static::getDetectionRulesExtended()
150 56
            : 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...
151
    }
152
153
    /**
154
     * Get the device name.
155
     *
156
     * @param  string|null  $userAgent
157
     *
158
     * @return string
159
     */
160 4
    public function device($userAgent = null)
161
    {
162
        // Get device rules
163 4
        $rules = $this->mergeRules(
164 4
            static::$additionalDevices, // NEW
165 4
            static::$phoneDevices,
166 4
            static::$tabletDevices,
167 4
            static::$utilities
168
        );
169
170 4
        return $this->findDetectionRulesAgainstUA($rules, $userAgent);
0 ignored issues
show
Bug introduced by
It seems like $userAgent defined by parameter $userAgent on line 160 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...
171
    }
172
173
    /**
174
     * Get the browser name.
175
     *
176
     * @param  string|null  $userAgent
177
     *
178
     * @return string
179
     */
180 26
    public function browser($userAgent = null)
181
    {
182
        // Get browser rules
183 26
        $rules = $this->mergeRules(
184 26
            static::$additionalBrowsers, // NEW
185 26
            static::$browsers
186
        );
187
188 26
        return $this->findDetectionRulesAgainstUA($rules, $userAgent);
0 ignored issues
show
Bug introduced by
It seems like $userAgent defined by parameter $userAgent on line 180 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...
189
    }
190
191
    /**
192
     * Get the robot name.
193
     *
194
     * @param  string|null  $userAgent
195
     *
196
     * @return string
197
     */
198 10
    public function robot($userAgent = null)
199
    {
200 10
        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 200 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...
201 10
            ? ucfirst($this->getCrawlerDetector()->getMatches())
202 10
            : false;
203
    }
204
205
    /**
206
     * Get the platform name.
207
     *
208
     * @param  string|null  $userAgent
209
     *
210
     * @return string
211
     */
212 16
    public function platform($userAgent = null)
213
    {
214
        // Get platform rules
215 16
        $rules = $this->mergeRules(
216 16
            static::$operatingSystems,
217 16
            static::$additionalOperatingSystems // NEW
218
        );
219
220 16
        return $this->findDetectionRulesAgainstUA($rules, $userAgent);
0 ignored issues
show
Bug introduced by
It seems like $userAgent defined by parameter $userAgent on line 212 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...
221
    }
222
223
    /**
224
     * Get the languages.
225
     *
226
     * @param  string|null  $acceptLanguage
227
     *
228
     * @return array
229
     */
230 4
    public function languages($acceptLanguage = null)
231
    {
232 4
        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...
233 4
            $acceptLanguage = $this->getHttpHeader('HTTP_ACCEPT_LANGUAGE');
234
        }
235
236 4
        $languages = [];
237
238 4
        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...
239
            // Parse accept language string.
240 4
            foreach (explode(',', $acceptLanguage) as $piece) {
241 4
                $parts = explode(';', $piece);
242 4
                $language = strtolower($parts[0]);
243 4
                $priority = empty($parts[1]) ? 1. : floatval(str_replace('q=', '', $parts[1]));
244 4
                $languages[$language] = $priority;
245
            }
246
247
            // Sort languages by priority.
248 4
            arsort($languages);
249
250 4
            $languages = array_keys($languages);
251
        }
252
253 4
        return $languages;
254
    }
255
256
    /**
257
     * Match a detection rule and return the matched key.
258
     *
259
     * @param  array  $rules
260
     * @param  null   $userAgent
261
     *
262
     * @return string
263
     */
264 46
    protected function findDetectionRulesAgainstUA(array $rules, $userAgent = null)
265
    {
266
        // Loop given rules
267 46
        foreach ($rules as $key => $regex) {
268
            // Check match
269 46
            if ( ! empty($regex) && $this->match($regex, $userAgent))
270 46
                return $key ?: reset($this->matchesArray);
271
        }
272
273
        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...
274
    }
275
276
    /* -----------------------------------------------------------------
277
     |  Main Methods
278
     | -----------------------------------------------------------------
279
     */
280
281
    /**
282
     * Check the version of the given property in the User-Agent.
283
     * Will return a float number. (eg. 2_0 will return 2.0, 4.3.1 will return 4.31)
284
     *
285
     * @param  string  $propertyName  The name of the property. See self::getProperties() array
286
     *                                keys for all possible properties.
287
     * @param  string  $type          Either self::VERSION_TYPE_STRING to get a string value or
288
     *                                self::VERSION_TYPE_FLOAT indicating a float value. This parameter is optional
289
     *                                and defaults to self::VERSION_TYPE_STRING. Passing an invalid parameter will
290
     *                                default to the this type as well.
291
     *
292
     * @return string|float The version of the property we are trying to extract.
293
     */
294 4
    public function version($propertyName, $type = self::VERSION_TYPE_STRING)
295
    {
296 4
        $check = key(static::$additionalProperties);
297
298
        // Check if the additional properties have been added already
299 4
        if ( ! array_key_exists($check, static::$properties)) {
300
            // TODO: why is mergeRules not working here?
301 2
            static::$properties = array_merge(
302 2
                static::$properties,
303 2
                static::$additionalProperties
304
            );
305
        }
306
307 4
        return parent::version($propertyName, $type);
308
    }
309
310
    /* -----------------------------------------------------------------
311
     |  Check Methods
312
     | -----------------------------------------------------------------
313
     */
314
315
    /**
316
     * Check if the device is a desktop computer.
317
     *
318
     * @return bool
319
     */
320 18
    public function isDesktop()
321
    {
322 18
        return ! ($this->isMobile() || $this->isTablet() || $this->isRobot());
323
    }
324
325
    /**
326
     * Check if device is a robot.
327
     *
328
     * @param  string|null  $userAgent
329
     *
330
     * @return bool
331
     */
332 18
    public function isRobot($userAgent = null)
333
    {
334 18
        return $this->getCrawlerDetector()->isCrawler($userAgent ?: $this->userAgent);
335
    }
336
337
    /**
338
     * Check if the device is a mobile phone.
339
     *
340
     * @return bool
341
     */
342 16
    public function isPhone()
343
    {
344 16
        return $this->isMobile() && ! $this->isTablet();
345
    }
346
347
    /* -----------------------------------------------------------------
348
     |  Other Methods
349
     | -----------------------------------------------------------------
350
     */
351
352
    /**
353
     * Merge multiple rules into one array.
354
     *
355
     * @param  array  $rulesGroups
356
     *
357
     * @return array
358
     */
359 46
    protected function mergeRules(...$rulesGroups)
360
    {
361 46
        $merged = [];
362
363 46
        foreach ($rulesGroups as $rules) {
364 46
            foreach ($rules as $key => $value) {
365 46
                if (empty($merged[$key]))
366 46
                    $merged[$key] = $value;
367 26
                elseif (is_array($merged[$key]))
368
                    $merged[$key][] = $value;
369
                else
370 46
                    $merged[$key] .= '|' . $value;
371
            }
372
        }
373
374 46
        return $merged;
375
    }
376
377
    /**
378
     * Changing detection type to extended.
379
     *
380
     * @inherit
381
     *
382
     * @param  string  $name
383
     * @param  array   $arguments
384
     *
385
     * @return bool|mixed
386
     */
387 42
    public function __call($name, $arguments)
388
    {
389
        // Make sure the name starts with 'is', otherwise
390 42
        if ( ! Str::startsWith($name, ['is'])) {
391 2
            throw new \BadMethodCallException("No such method exists: $name");
392
        }
393
394 40
        $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...
395
396 40
        return $this->matchUAAgainstKey(substr($name, 2));
397
    }
398
}
399