Antibot   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 377
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 91.43%

Importance

Changes 0
Metric Value
wmc 40
lcom 1
cbo 10
dl 0
loc 377
ccs 96
cts 105
cp 0.9143
rs 9.2
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getUnique() 0 4 1
A getPrefix() 0 4 1
A getData() 0 6 1
A getParameterPrefix() 0 6 1
A addValidator() 0 5 1
B validateRequest() 0 40 7
A armorInputs() 0 16 3
A sortValidators() 0 10 3
A initialize() 0 10 2
A calculateSignature() 0 6 1
A extractData() 0 6 3
A extractScopedData() 0 13 3
A checkImmutable() 0 9 2
A checkInitialized() 0 10 2
A getLogger() 0 4 1
A setLogger() 0 4 1
A setParameterScope() 0 14 4
A getScopedParameters() 0 10 2

How to fix   Complexity   

Complex Class

Complex classes like Antibot 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Antibot, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * antibot
5
 *
6
 * @category   Jkphl
7
 * @package    Jkphl\Antibot
8
 * @subpackage Jkphl\Antibot\Domain
9
 * @author     Joschi Kuphal <[email protected]> / @jkphl
10
 * @copyright  Copyright © 2020 Joschi Kuphal <[email protected]> / @jkphl
11
 * @license    http://opensource.org/licenses/MIT The MIT License (MIT)
12
 */
13
14
/***********************************************************************************
15
 *  The MIT License (MIT)
16
 *
17
 *  Copyright © 2020 Joschi Kuphal <[email protected]>
18
 *
19
 *  Permission is hereby granted, free of charge, to any person obtaining a copy of
20
 *  this software and associated documentation files (the "Software"), to deal in
21
 *  the Software without restriction, including without limitation the rights to
22
 *  use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
23
 *  the Software, and to permit persons to whom the Software is furnished to do so,
24
 *  subject to the following conditions:
25
 *
26
 *  The above copyright notice and this permission notice shall be included in all
27
 *  copies or substantial portions of the Software.
28
 *
29
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
31
 *  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
32
 *  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
33
 *  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
34
 *  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35
 ***********************************************************************************/
36
37
namespace Jkphl\Antibot\Domain;
38
39
use Jkphl\Antibot\Domain\Contract\ValidationResultInterface;
40
use Jkphl\Antibot\Domain\Contract\ValidatorInterface;
41
use Jkphl\Antibot\Domain\Exceptions\BlacklistValidationException;
42
use Jkphl\Antibot\Domain\Exceptions\ErrorException;
43
use Jkphl\Antibot\Domain\Exceptions\InvalidArgumentException;
44
use Jkphl\Antibot\Domain\Exceptions\RuntimeException;
45
use Jkphl\Antibot\Domain\Exceptions\SkippedValidationException;
46
use Jkphl\Antibot\Domain\Exceptions\WhitelistValidationException;
47
use Jkphl\Antibot\Infrastructure\Model\InputElement;
48
use Psr\Http\Message\ServerRequestInterface;
49
use Psr\Log\LoggerAwareInterface;
50
use Psr\Log\LoggerInterface;
51
use Psr\Log\NullLogger;
52
53
/**
54
 * Antibot core
55
 *
56
 * @package    Jkphl\Antibot
57
 * @subpackage Jkphl\Antibot\Domain
58
 */
59
class Antibot implements LoggerAwareInterface
60
{
61
    /**
62
     * Session persistent, unique token
63
     *
64
     * @var string
65
     */
66
    protected $unique;
67
    /**
68
     * Antibot prefix
69
     *
70
     * @var string
71
     */
72
    protected $prefix;
73
    /**
74
     * Parameter scope nodes
75
     *
76
     * @var array
77
     */
78
    protected $scope = [];
79
    /**
80
     * Unique signature
81
     *
82
     * @var string
83
     */
84
    protected $signature;
85
    /**
86
     * Parameter prefix
87
     *
88
     * @var string
89
     */
90
    protected $parameterPrefix;
91
    /**
92
     * GET & POST data
93
     *
94
     * @var null|array
95
     */
96
    protected $data = null;
97
    /**
98
     * Validators
99
     *
100
     * @var ValidatorInterface[]
101
     */
102
    protected $validators = [];
103
    /**
104
     * Immutable instance
105
     *
106
     * @var bool
107
     */
108
    protected $immutable = false;
109
    /**
110
     * Logger
111
     *
112
     * @var LoggerInterface
113
     */
114
    protected $logger = null;
115
    /**
116
     * Default antibot prefix
117
     *
118
     * @var string
119
     */
120
    const DEFAULT_PREFIX = 'antibot';
121
122
    /**
123
     * Antibot constructor
124
     *
125
     * @param string $unique Session-persistent, unique key
126
     * @param string $prefix Prefix
127
     */
128 20
    public function __construct(string $unique, string $prefix = self::DEFAULT_PREFIX)
129
    {
130 20
        $this->unique = $unique;
131 20
        $this->prefix = $prefix;
132 20
        $this->logger = new NullLogger();
133 20
    }
134
135
    /**
136
     * Return the session persistent, unique token
137
     *
138
     * @return string Session persistent, unique token
139
     */
140 5
    public function getUnique(): string
141
    {
142 5
        return $this->unique;
143
    }
144
145
    /**
146
     * Return the prefix
147
     *
148
     * @return string Prefix
149
     */
150 2
    public function getPrefix(): string
151
    {
152 2
        return $this->prefix;
153
    }
154
155
    /**
156
     * Return the submitted Antibot data
157
     *
158
     * @return string[] Antibot data
159
     */
160 6
    public function getData(): ?array
161
    {
162 6
        $this->checkInitialized();
163
164 5
        return $this->data;
165
    }
166
167
    /**
168
     * Return the parameter prefix
169
     *
170
     * @return string Parameter prefix
171
     * @throws RuntimeException If Antibot needs to be initialized
172
     */
173 14
    public function getParameterPrefix(): string
174
    {
175 14
        $this->checkInitialized();
176
177 13
        return $this->parameterPrefix;
178
    }
179
180
    /**
181
     * Add a validator
182
     *
183
     * @param ValidatorInterface $validator Validator
184
     */
185 12
    public function addValidator(ValidatorInterface $validator): void
186
    {
187 12
        $this->checkImmutable();
188 12
        $this->validators[] = $validator;
189 12
    }
190
191
    /**
192
     * Validate a request
193
     *
194
     * @param ServerRequestInterface $request   Request
195
     * @param ValidationResultInterface $result Validation result
196
     *
197
     * @return ValidationResultInterface Validation result
198
     * @internal
199
     */
200 12
    public function validateRequest(
201
        ServerRequestInterface $request,
202
        ValidationResultInterface $result
203
    ): ValidationResultInterface {
204 12
        $this->logger->info('Start validation');
205 12
        $this->initialize($request);
206
207
        // Run through all validators (in order)
208
        /** @var ValidatorInterface $validator */
209 12
        foreach ($this->validators as $validator) {
210
            try {
211 11
                if (!$validator->validate($request, $this)) {
212 6
                    $result->setValid(false);
213
                }
214
215
                // If the validator skipped validation
216 9
            } catch (SkippedValidationException $e) {
217 3
                $result->addSkip($e->getMessage());
218
219
                // If the request failed a blacklist test
220 7
            } catch (BlacklistValidationException $e) {
221 1
                $result->addBlacklist($e->getMessage());
222 1
                $result->setValid(false);
223
224
                // If the request passed a whitelist test
225 6
            } catch (WhitelistValidationException $e) {
226 2
                $result->addWhitelist($e->getMessage());
227 2
                break;
228
229
                // If an error occured
230 4
            } catch (ErrorException $e) {
231 4
                $result->addError($e);
232 4
                $result->setValid(false);
233
            }
234
        }
235
236 12
        $this->logger->info('Finished validation');
237
238 12
        return $result;
239
    }
240
241
    /**
242
     * Create and return the raw armor input elements
243
     *
244
     * @param ServerRequestInterface $request Request
245
     *
246
     * @return InputElement[] Armor input elements
247
     */
248 5
    public function armorInputs(ServerRequestInterface $request): array
249
    {
250 5
        $this->initialize($request);
251 5
        $armor = [];
252
253
        // Run through all validators (in order)
254
        /** @var ValidatorInterface $validator */
255 5
        foreach ($this->validators as $validator) {
256 5
            $validatorArmor = $validator->armor($request, $this);
257 5
            if (!empty($validatorArmor)) {
258 5
                $armor = array_merge($armor, $validatorArmor);
259
            }
260
        }
261
262 5
        return $armor;
263
    }
264
265
    /**
266
     * Compare and sort validators
267
     *
268
     * @param ValidatorInterface $validator1 Validator 1
269
     * @param ValidatorInterface $validator2 Validator 2
270
     *
271
     * @return int Sorting
272
     */
273
    protected function sortValidators(ValidatorInterface $validator1, ValidatorInterface $validator2): int
274
    {
275
        $validatorPos1 = $validator1->getPosition();
276
        $validatorPos2 = $validator2->getPosition();
277
        if ($validatorPos1 == $validatorPos2) {
278
            return 0;
279
        }
280
281
        return ($validatorPos1 > $validatorPos2) ? 1 : -1;
282
    }
283
284
    /**
285
     * Pre-validation initialization
286
     *
287
     * @param ServerRequestInterface $request Request
288
     */
289 13
    protected function initialize(ServerRequestInterface $request): void
290
    {
291 13
        if (!$this->immutable) {
292 13
            $this->immutable = true;
293 13
            usort($this->validators, [$this, 'sortValidators']);
294 13
            $this->signature       = $this->calculateSignature();
295 13
            $this->parameterPrefix = $this->prefix.'_'.$this->signature;
296
        }
297 13
        $this->extractData($request);
298 13
    }
299
300
    /**
301
     * Calculate the unique signature
302
     *
303
     * @return string Signature
304
     */
305 13
    protected function calculateSignature(): string
306
    {
307 13
        $params = [$this->prefix, $this->validators];
308
309 13
        return sha1($this->unique.serialize($params));
310
    }
311
312
    /**
313
     * Extract the antibot data from GET and POST parameters
314
     *
315
     * @param ServerRequestInterface $request Request
316
     */
317 13
    protected function extractData(ServerRequestInterface $request): void
318
    {
319 13
        $get        = $this->extractScopedData($request->getQueryParams() ?? []);
320 13
        $post       = $this->extractScopedData($request->getParsedBody() ?? []);
0 ignored issues
show
Bug introduced by
It seems like $request->getParsedBody() ?? array() can also be of type object; however, Jkphl\Antibot\Domain\Antibot::extractScopedData() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
321 13
        $this->data = (($get !== null) || ($post !== null)) ? array_merge((array)$get, (array)$post) : null;
322 13
    }
323
324
    /**
325
     * Extract scoped data
326
     *
327
     * @param array $data Source data
328
     *
329
     * @return array|null Scoped data
330
     */
331 13
    protected function extractScopedData(array $data): ?array
332
    {
333
        // Run through all scope nodes
334 13
        foreach (array_merge($this->scope, [$this->getParameterPrefix()]) as $node) {
335 13
            if (!isset($data[$node])) {
336 13
                return null;
337
            }
338
339 4
            $data = $data[$node];
340
        }
341
342 4
        return $data;
343
    }
344
345
    /**
346
     * Check whether this Antibot instance is immutable
347
     *
348
     * @throws RuntimeException If the Antibot instance is immutable
349
     */
350 12
    protected function checkImmutable(): void
351
    {
352 12
        if ($this->immutable) {
353
            throw new RuntimeException(
354
                RuntimeException::ANTIBOT_IMMUTABLE_STR,
355
                RuntimeException::ANTIBOT_IMMUTABLE
356
            );
357
        }
358 12
    }
359
360
    /**
361
     * Check whether this Antibot instance is already initialized
362
     *
363
     * @throws RuntimeException If the Antibot instance still needs to be initialized
364
     */
365 15
    protected function checkInitialized(): void
366
    {
367
        // If the Antibot instance still needs to be initialized
368 15
        if (!$this->immutable) {
369 2
            throw new RuntimeException(
370 2
                RuntimeException::ANTIBOT_INITIALIZE_STR,
371 2
                RuntimeException::ANTIBOT_INITIALIZE
372
            );
373
        }
374 13
    }
375
376
    /**
377
     * Return the logger
378
     *
379
     * @return LoggerInterface Logger
380
     */
381 6
    public function getLogger(): LoggerInterface
382
    {
383 6
        return $this->logger;
384
    }
385
386
    /**
387
     * Sets a logger instance on the object
388
     *
389
     * @param LoggerInterface $logger Logger
390
     *
391
     * @return void
392
     */
393 12
    public function setLogger(LoggerInterface $logger): void
394
    {
395 12
        $this->logger = $logger;
396 12
    }
397
398
    /**
399
     * Set the parameter scope
400
     *
401
     * @param string[] ...$scope Parameter scope
402
     */
403 1
    public function setParameterScope(...$scope): void
404
    {
405
        // Run through all scope nodes
406 1
        foreach ($scope as $node) {
407 1
            if (!is_string($node) || empty($node)) {
408 1
                throw new InvalidArgumentException(
409 1
                    sprintf(InvalidArgumentException::INVALID_SCOPE_NODE_STR, $node),
410 1
                    InvalidArgumentException::INVALID_SCOPE_NODE
411
                );
412
            }
413
414 1
            $this->scope[] = $node;
415
        }
416 1
    }
417
418
    /**
419
     * Scope a set of parameters
420
     *
421
     * @param array $params Parameters
422
     *
423
     * @return array Scoped parameters
424
     */
425 1
    public function getScopedParameters(array $params): array
426
    {
427 1
        $params = [$this->getParameterPrefix() => $params];
428 1
        $scope  = $this->scope;
429 1
        while ($node = array_pop($scope)) {
430 1
            $params = [$node => $params];
431
        }
432
433 1
        return $params;
434
    }
435
}
436