Passed
Push — develop ( 7cbbb4...22c94d )
by Stephen
02:36
created

Filter   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 475
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 19
Bugs 0 Features 0
Metric Value
wmc 47
eloc 104
c 19
b 0
f 0
dl 0
loc 475
ccs 110
cts 110
cp 1
rs 8.64

22 Methods

Rating   Name   Duplication   Size   Complexity  
A determineRedirect() 0 7 2
A onRedirectPath() 0 4 1
A generateCacheKey() 0 4 1
A needsRedirecting() 0 3 1
A haveRulesForDevice() 0 3 1
A haveVersionsForBrowser() 0 4 2
A isMatchedBrowserVersion() 0 12 2
A handle() 0 30 4
A getCacheTimeout() 0 3 1
A isMatchedDevice() 0 3 1
A isMatchedBrowser() 0 3 1
A isMatched() 0 3 3
A getFilterType() 0 7 3
A getBrowsers() 0 3 2
A getBrowserVersions() 0 7 2
A validateRules() 0 8 3
A getRedirectRoute() 0 3 2
A getRules() 0 3 1
A __construct() 0 12 1
A validateBrowserVersionRules() 0 24 3
A validateBrowserRules() 0 26 5
A validDeviceRule() 0 21 5

How to fix   Complexity   

Complex Class

Complex classes like Filter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Filter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Spinen\BrowserFilter;
4
5
use Closure;
6
use Illuminate\Contracts\Cache\Repository as Cache;
7
use Illuminate\Contracts\Config\Repository as Config;
8
use Illuminate\Http\Request;
9
use Illuminate\Routing\Redirector;
10
use Mobile_Detect;
11
use Spinen\BrowserFilter\Exceptions\FilterTypeNotSetException;
12
use Spinen\BrowserFilter\Exceptions\InvalidRuleDefinitionsException;
13
use Spinen\BrowserFilter\Support\ParserCreator;
14
15
/**
16
 * Class Filter
17
 *
18
 * @package Spinen\BrowserFilter
19
 */
20
abstract class Filter
21
{
22
    /**
23
     * Is this a block or allow filter?
24
     *
25
     * @var bool
26
     */
27
    protected $block_filter = null;
28
29
    /**
30
     * The cache repository instance.
31
     *
32
     * @var Cache
33
     */
34
    protected $cache;
35
36
    /**
37
     * The client instance.
38
     *
39
     * @var \UAParser\Result\Client
40
     */
41
    protected $client;
42
43
    /**
44
     * The config repository instance.
45
     *
46
     * @var Config
47
     */
48
    protected $config;
49
50
    /**
51
     * Location of the config file.
52
     *
53
     * @var string
54
     */
55
    protected $config_path = 'browserfilter.';
56
57
    /**
58
     * The mobile detector instance.
59
     *
60
     * @var Mobile_Detect
61
     */
62
    protected $detector;
63
64
    /**
65
     * The path to redirect the user if client is blocked.
66
     *
67
     * @var string
68
     */
69
    protected $redirect_route;
70
71
    /**
72
     * The redirector instance.
73
     *
74
     * @var Redirector
75
     */
76
    protected $redirector;
77
78
    /**
79
     * The array of rules
80
     *
81
     * @var array
82
     */
83
    protected $rules = [];
84
85
    /**
86
     * Create a new browser filter middleware instance.
87
     *
88
     * @param Cache $cache Cache
89
     * @param Config $config Config
90
     * @param Mobile_Detect $detector Mobile_Detect
91
     * @param ParserCreator $parser ParserCreator
92
     * @param Redirector $redirector Redirector
93
     */
94 65
    public function __construct(
95
        Cache $cache,
96
        Config $config,
97
        Mobile_Detect $detector,
98
        ParserCreator $parser,
99
        Redirector $redirector
100
    ) {
101 65
        $this->cache = $cache;
102 65
        $this->config = $config;
103 65
        $this->detector = $detector;
104 65
        $this->client = $parser->parseAgent($this->detector->getUserAgent());
105 65
        $this->redirector = $redirector;
106 65
    }
107
108
    /**
109
     * Determines if the client needs to be redirected.
110
     *
111
     * @return string|bool
112
     */
113 6
    public function determineRedirect()
114
    {
115 6
        if ($this->needsRedirecting()) {
116 3
            return $this->getRedirectRoute();
117
        }
118
119 3
        return false;
120
    }
121
122
    /**
123
     * Generate the key to use to cache the determination.
124
     *
125
     * @param Request $request
126
     *
127
     * @return string
128
     */
129 9
    public function generateCacheKey(Request $request)
130
    {
131
        // NOTE: $request is an unused variable here, but needed in a class that extends this one
132 9
        return $this->client->device->family . ':' . $this->client->ua->family . ':' . $this->client->ua->toVersion();
133
    }
134
135
    /**
136
     * Get the browsers being filtered.
137
     *
138
     * @return string|array
139
     */
140 16
    public function getBrowsers()
141
    {
142 16
        return $this->haveRulesForDevice() ? $this->getRules()[$this->client->device->family] : null;
143
    }
144
145
    /**
146
     * Get the versions of the browsers being filtered.
147
     *
148
     * @return string|array|null
149
     */
150 22
    public function getBrowserVersions()
151
    {
152 22
        if ($this->haveVersionsForBrowser()) {
153 12
            return $this->getRules()[$this->client->device->family][$this->client->ua->family];
154
        }
155
156 11
        return null;
157
    }
158
159
    /**
160
     * Get the timeout of the cached value.
161
     *
162
     * @return mixed
163
     */
164 9
    public function getCacheTimeout()
165
    {
166 9
        return $this->config->get($this->config_path . 'timeout');
167
    }
168
169
    /**
170
     * Return the filter type.
171
     *
172
     * @return string
173
     *
174
     * @throws FilterTypeNotSetException
175
     */
176 7
    public function getFilterType()
177
    {
178 7
        if (is_bool($this->block_filter)) {
179 6
            return $this->block_filter ? 'block' : 'allow';
180
        }
181
182 1
        throw new FilterTypeNotSetException();
183
    }
184
185
    /**
186
     * Get the route to the redirect path.
187
     *
188
     * @return string|null
189
     */
190 5
    public function getRedirectRoute()
191
    {
192 5
        return $this->redirect_route ?: $this->config->get($this->config_path . 'route');
193
    }
194
195
    /**
196
     * Return the array of rules.
197
     *
198
     * @return array
199
     */
200 43
    public function getRules()
201
    {
202 43
        return $this->rules;
203
    }
204
205
    /**
206
     * Handle an incoming request.
207
     *
208
     * @param Request $request Request
209
     * @param Closure $next Closure
210
     * @param string|null $filter_string Filter in string format
211
     * @param string|null $redirect_route Named route to redirect blocked client
212
     *
213
     * @return mixed
214
     */
215 7
    public function handle(Request $request, Closure $next, $filter_string = null, $redirect_route = null)
216
    {
217 7
        $this->redirect_route = $redirect_route;
218
219 7
        if ($this->onRedirectPath($request)) {
220 1
            return $next($request);
221
        }
222
223 6
        $cache_key = $this->generateCacheKey($request);
224
225 6
        $redirect = $this->cache->get($cache_key);
226
227 6
        if (is_null($redirect)) {
228 4
            $this->parseFilterString($filter_string);
229
230 4
            $this->validateRules();
231
232 4
            $redirect = $this->determineRedirect();
233
234 4
            $this->cache->put($cache_key, $redirect, $this->getCacheTimeout());
235
        }
236
237 6
        if ($redirect) {
238 3
            $request->session()
239 3
                    ->flash('redirected', true);
240
241 3
            return $this->redirector->route($redirect);
242
        }
243
244 3
        return $next($request);
245
    }
246
247
    /**
248
     * Check to see if there are defined rules for the device.
249
     *
250
     * @return bool
251
     */
252 18
    public function haveRulesForDevice()
253
    {
254 18
        return array_key_exists($this->client->device->family, $this->getRules());
255
    }
256
257
    /**
258
     * Check to see if there are defined versions for the browser for the device.
259
     *
260
     * @return bool
261
     */
262 22
    public function haveVersionsForBrowser()
263
    {
264 22
        return array_key_exists($this->client->device->family, $this->getRules()) &&
265 22
            array_key_exists($this->client->ua->family, $this->getRules()[$this->client->device->family]);
266
    }
267
268
    /**
269
     * Checks to see if the browser/client is blocked.
270
     *
271
     * @return bool
272
     */
273 13
    public function isMatched()
274
    {
275 13
        return $this->isMatchedDevice() || $this->isMatchedBrowser() || $this->isMatchedBrowserVersion();
276
    }
277
278
    /**
279
     * Checks to see if all versions of the browser is blocked.
280
     *
281
     * @return bool
282
     */
283 14
    public function isMatchedBrowser()
284
    {
285 14
        return '*' === $this->getBrowserVersions();
286
    }
287
288
    /**
289
     * Checks to see if the version of the browser is blocked.
290
     *
291
     * Uses the php version_compare function to decide if there is a match.
292
     *
293
     * @link http://php.net/manual/en/function.version-compare.php
294
     *
295
     * @return bool
296
     */
297 19
    public function isMatchedBrowserVersion()
298
    {
299 19
        $denied = false;
300
301
        // cache it, so that we don't have to keep asking for it
302 19
        $client_version = $this->client->ua->toVersion();
303
304 19
        foreach ((array) $this->getBrowserVersions() as $operator => $version) {
305 9
            $denied |= (bool) version_compare($client_version, $version, $operator);
306
        }
307
308 19
        return (bool) $denied;
309
    }
310
311
    /**
312
     * Checks to see if all browsers of the device family is blocked.
313
     *
314
     * @return bool
315
     */
316 14
    public function isMatchedDevice()
317
    {
318 14
        return '*' === $this->getBrowsers();
319
    }
320
321
    /**
322
     * Decide if the client needs to be redirected.
323
     *
324
     * Here is the logic:
325
     *
326
     *   blockedFilter  true       true    false   false
327
     *   isMatched()    true       false   true    false
328
     *                  redirect   no      no      redirect
329
     *
330
     * so you can see this is a negative xor
331
     *
332
     * @return bool
333
     */
334 10
    public function needsRedirecting()
335
    {
336 10
        return !$this->block_filter xor $this->isMatched();
337
    }
338
339
    /**
340
     * Check to see if we are on the redirect page.
341
     *
342
     * If we did not test for this, then we would get into a redirect loop.
343
     *
344
     * @param Request $request Request
345
     *
346
     * @return bool
347
     */
348 8
    public function onRedirectPath(Request $request)
349
    {
350 8
        return $request->session()
351 8
                       ->get('redirected', false);
352
    }
353
354
    /**
355
     * Delegate setting the rules from the passed in filter string.
356
     *
357
     * The the filter string will always be null on the stack filter.
358
     *
359
     * @param string $filter_string The filter(s)
360
     *
361
     * @return void
362
     */
363
    abstract public function parseFilterString($filter_string);
364
365
    /**
366
     * Validate a device browser stanza in the rules.
367
     *
368
     * @param string $device Device name
369
     * @param string $browser Browser name
370
     * @param array|string $versions Array of browser versions or '*' for all versions
371
     *
372
     * @return void
373
     *
374
     * @throws InvalidRuleDefinitionsException
375
     */
376 6
    protected function validateBrowserRules($device, $browser, $versions)
377
    {
378 6
        if (!is_string($browser)) {
379 1
            throw new InvalidRuleDefinitionsException(
380 1
                sprintf(
381 1
                    "Device [%s] browsers must be a string form of the name.",
382
                    $device
383
                )
384
            );
385
        }
386
387 5
        if ('*' === $versions) {
388 1
            return;
389
        }
390
391 5
        if (!is_array($versions)) {
392 1
            throw new InvalidRuleDefinitionsException(
393 1
                sprintf(
394 1
                    "The value for [%s] must be either an array of browsers or an asterisk (*) for all browsers.",
395
                    $browser
396
                )
397
            );
398
        }
399
400 4
        foreach ($versions as $operator => $version) {
401 4
            $this->validateBrowserVersionRules($device, $browser, $operator, $version);
402
        }
403 1
    }
404
405
    /**
406
     * Validate a browser version stanza in the rules.
407
     *
408
     * @param string $device Device name
409
     * @param string $browser Browser name
410
     * @param string $operator Comparison operator
411
     * @param string $version Version of browser
412
     *
413
     * @return void
414
     *
415
     * @throws InvalidRuleDefinitionsException
416
     */
417 4
    protected function validateBrowserVersionRules($device, $browser, $operator, $version)
418
    {
419 4
        if (!is_string($version)) {
420 1
            throw new InvalidRuleDefinitionsException(
421 1
                sprintf(
422 1
                    "Device [%s] browser [%s] version [%s] must be a string form of the version.",
423
                    $device,
424
                    $browser,
425
                    $version
426
                )
427
            );
428
        }
429
430 3
        if (!in_array(
431
            $operator,
432 3
            ['<', 'lt', '<=', 'le', '>', 'gt', '>=', 'ge', '==', '=', 'eq', '!=', '<>', 'ne'],
433 3
            true
434
        )) {
435 2
            throw new InvalidRuleDefinitionsException(
436 2
                sprintf(
437 2
                    "The comparison operator [%s] for [%s > %s] is invalid.",
438
                    $operator,
439
                    $device,
440
                    $browser
441
                )
442
            );
443
        }
444 1
    }
445
446
    /**
447
     * Validate a device stanza in the rules.
448
     *
449
     * @param string $device Device name
450
     * @param array|string $browsers Array of device browsers or '*' for all versions
451
     *
452
     * @return void
453
     *
454
     * @throws InvalidRuleDefinitionsException
455
     */
456 9
    protected function validDeviceRule($device, $browsers)
457
    {
458 9
        if (!is_string($device)) {
459 1
            throw new InvalidRuleDefinitionsException('Devices must be a string form of the name.');
460
        }
461
462 8
        if ('*' === $browsers) {
463 1
            return;
464
        }
465
466 8
        if (!is_array($browsers)) {
467 1
            throw new InvalidRuleDefinitionsException(
468 1
                sprintf(
469 1
                    "The value for [%s] must be either an array of browsers or an asterisk (*) for all browsers.",
470
                    $device
471
                )
472
            );
473
        }
474
475 7
        foreach ($browsers as $browser => $versions) {
476 6
            $this->validateBrowserRules($device, $browser, $versions);
477
        }
478 2
    }
479
480
    /**
481
     * Validate the rules.
482
     *
483
     * @return void
484
     *
485
     * @throws InvalidRuleDefinitionsException
486
     */
487 12
    public function validateRules()
488
    {
489 12
        if (empty($this->getRules())) {
490 3
            return;
491
        }
492
493 9
        foreach ($this->getRules() as $device => $browsers) {
494 9
            $this->validDeviceRule($device, $browsers);
495
        }
496 2
    }
497
}
498