Completed
Push — master ( 7d4b0d...855588 )
by Joschi
03:39
created

Antibot   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 311
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 89.77%

Importance

Changes 0
Metric Value
wmc 33
lcom 1
cbo 9
dl 0
loc 311
ccs 79
cts 88
cp 0.8977
rs 9.76
c 0
b 0
f 0

16 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 validate() 0 39 7
A armorInputs() 0 16 3
A sortValidators() 0 10 3
A initialize() 0 10 2
A calculateSignature() 0 6 1
A extractData() 0 8 5
A checkImmutable() 0 9 2
A checkInitialized() 0 10 2
A getLogger() 0 4 1
A setLogger() 0 4 1
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 © 2018 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 © 2018 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\ValidatorInterface;
40
use Jkphl\Antibot\Domain\Exceptions\BlacklistValidationException;
41
use Jkphl\Antibot\Domain\Exceptions\ErrorException;
42
use Jkphl\Antibot\Domain\Exceptions\RuntimeException;
43
use Jkphl\Antibot\Domain\Exceptions\SkippedValidationException;
44
use Jkphl\Antibot\Domain\Exceptions\WhitelistValidationException;
45
use Jkphl\Antibot\Domain\Model\ValidationResult;
46
use Jkphl\Antibot\Infrastructure\Model\InputElement;
47
use Psr\Http\Message\ServerRequestInterface;
48
use Psr\Log\LoggerAwareInterface;
49
use Psr\Log\LoggerInterface;
50
use Psr\Log\NullLogger;
51
52
/**
53
 * Antibot core
54
 *
55
 * @package    Jkphl\Antibot
56
 * @subpackage Jkphl\Antibot\Domain
57
 */
58
class Antibot implements LoggerAwareInterface
59
{
60
    /**
61
     * Session persistent, unique token
62
     *
63
     * @var string
64
     */
65
    protected $unique;
66
    /**
67
     * Antibot prefix
68
     *
69
     * @var string
70
     */
71
    protected $prefix;
72
    /**
73
     * Unique signature
74
     *
75
     * @var string
76
     */
77
    protected $signature;
78
    /**
79
     * Parameter prefix
80
     *
81
     * @var string
82
     */
83
    protected $parameterPrefix;
84
    /**
85
     * GET & POST data
86
     *
87
     * @var null|array
88
     */
89
    protected $data = null;
90
    /**
91
     * Validators
92
     *
93
     * @var ValidatorInterface[]
94
     */
95
    protected $validators = [];
96
    /**
97
     * Immutable instance
98
     *
99
     * @var bool
100
     */
101
    protected $immutable = false;
102
    /**
103
     * Logger
104
     *
105
     * @var LoggerInterface
106
     */
107
    protected $logger = null;
108
    /**
109
     * Default antibot prefix
110
     *
111
     * @var string
112
     */
113
    const DEFAULT_PREFIX = 'antibot';
114
115
    /**
116
     * Antibot constructor
117
     *
118
     * @param string $unique Session-persistent, unique key
119
     * @param string $prefix Prefix
120
     */
121 19
    public function __construct(string $unique, string $prefix = self::DEFAULT_PREFIX)
122
    {
123 19
        $this->unique = $unique;
124 19
        $this->prefix = $prefix;
125 19
        $this->logger = new NullLogger();
126 19
    }
127
128
    /**
129
     * Return the session persistent, unique token
130
     *
131
     * @return string Session persistent, unique token
132
     */
133 5
    public function getUnique(): string
134
    {
135 5
        return $this->unique;
136
    }
137
138
    /**
139
     * Return the prefix
140
     *
141
     * @return string Prefix
142
     */
143 2
    public function getPrefix(): string
144
    {
145 2
        return $this->prefix;
146
    }
147
148
    /**
149
     * Return the submitted Antibot data
150
     *
151
     * @return string[] Antibot data
152
     */
153 5
    public function getData(): ?array
154
    {
155 5
        $this->checkInitialized();
156
157 4
        return $this->data;
158
    }
159
160
    /**
161
     * Return the parameter prefix
162
     *
163
     * @return string Parameter prefix
164
     * @throws RuntimeException If Antibot needs to be initialized
165
     */
166 5
    public function getParameterPrefix(): string
167
    {
168 5
        $this->checkInitialized();
169
170 4
        return $this->parameterPrefix;
171
    }
172
173
    /**
174
     * Add a validator
175
     *
176
     * @param ValidatorInterface $validator Validator
177
     */
178 12
    public function addValidator(ValidatorInterface $validator): void
179
    {
180 12
        $this->checkImmutable();
181 12
        $this->validators[] = $validator;
182 12
    }
183
184
    /**
185
     * Validate a request
186
     *
187
     * @param ServerRequestInterface $request Request
188
     *
189
     * @return ValidationResult Validation result
190
     */
191 11
    public function validate(ServerRequestInterface $request): ValidationResult
192
    {
193 11
        $this->logger->info('Start validation');
194 11
        $this->initialize($request);
195 11
        $result = new ValidationResult();
196
197
        // Run through all validators (in order)
198
        /** @var ValidatorInterface $validator */
199 11
        foreach ($this->validators as $validator) {
200
            try {
201 11
                if (!$validator->validate($request, $this)) {
202 6
                    $result->setValid(false);
203
                }
204
205
                // If the validator skipped validation
206 9
            } catch (SkippedValidationException $e) {
207 3
                $result->addSkip($e->getMessage());
208
209
                // If the request failed a blacklist test
210 7
            } catch (BlacklistValidationException $e) {
211 1
                $result->addBlacklist($e->getMessage());
212 1
                $result->setValid(false);
213
214
                // If the request passed a whitelist test
215 6
            } catch (WhitelistValidationException $e) {
216 2
                $result->addWhitelist($e->getMessage());
217 2
                break;
218
219
                // If an error occured
220 4
            } catch (ErrorException $e) {
221 4
                $result->addError($e);
222 9
                $result->setValid(false);
223
            }
224
        }
225
226 11
        $this->logger->info('Finished validation');
227
228 11
        return $result;
229
    }
230
231
    /**
232
     * Create and return the raw armor input elements
233
     *
234
     * @param ServerRequestInterface $request Request
235
     *
236
     * @return InputElement[] Armor input elements
237
     */
238 5
    public function armorInputs(ServerRequestInterface $request): array
239
    {
240 5
        $this->initialize($request);
241 5
        $armor = [];
242
243
        // Run through all validators (in order)
244
        /** @var ValidatorInterface $validator */
245 5
        foreach ($this->validators as $validator) {
246 5
            $validatorArmor = $validator->armor($request, $this);
247 5
            if (!empty($validatorArmor)) {
248 5
                $armor = array_merge($armor, $validatorArmor);
249
            }
250
        }
251
252 5
        return $armor;
253
    }
254
255
    /**
256
     * Compare and sort validators
257
     *
258
     * @param ValidatorInterface $validator1 Validator 1
259
     * @param ValidatorInterface $validator2 Validator 2
260
     *
261
     * @return int Sorting
262
     */
263
    protected function sortValidators(ValidatorInterface $validator1, ValidatorInterface $validator2): int
264
    {
265
        $validatorPos1 = $validator1->getPosition();
266
        $validatorPos2 = $validator2->getPosition();
267
        if ($validatorPos1 == $validatorPos2) {
268
            return 0;
269
        }
270
271
        return ($validatorPos1 > $validatorPos2) ? 1 : -1;
272
    }
273
274
    /**
275
     * Pre-validation initialization
276
     *
277
     * @param ServerRequestInterface $request Request
278
     */
279 12
    protected function initialize(ServerRequestInterface $request): void
280
    {
281 12
        if (!$this->immutable) {
282 12
            $this->immutable = true;
283 12
            usort($this->validators, [$this, 'sortValidators']);
284 12
            $this->signature       = $this->calculateSignature();
285 12
            $this->parameterPrefix = $this->prefix.'_'.$this->signature;
286
        }
287 12
        $this->extractData($request);
288 12
    }
289
290
    /**
291
     * Calculate the unique signature
292
     *
293
     * @return string Signature
294
     */
295 12
    protected function calculateSignature(): string
296
    {
297 12
        $params = [$this->prefix, $this->validators];
298
299 12
        return sha1($this->unique.serialize($params));
300
    }
301
302
    /**
303
     * Extract the antibot data from GET and POST parameters
304
     *
305
     * @param ServerRequestInterface $request Request
306
     */
307 12
    protected function extractData(ServerRequestInterface $request): void
308
    {
309 12
        $get        = $request->getQueryParams();
310 12
        $get        = empty($get[$this->parameterPrefix]) ? null : $get[$this->parameterPrefix];
311 12
        $post       = $request->getParsedBody();
312 12
        $post       = empty($post[$this->parameterPrefix]) ? null : $post[$this->parameterPrefix];
313 12
        $this->data = (($get !== null) || ($post !== null)) ? array_merge((array)$get, (array)$post) : null;
314 12
    }
315
316
    /**
317
     * Check whether this Antibot instance is immutable
318
     *
319
     * @throws RuntimeException If the Antibot instance is immutable
320
     */
321 12
    protected function checkImmutable(): void
322
    {
323 12
        if ($this->immutable) {
324
            throw new RuntimeException(
325
                RuntimeException::ANTIBOT_IMMUTABLE_STR,
326
                RuntimeException::ANTIBOT_IMMUTABLE
327
            );
328
        }
329 12
    }
330
331
    /**
332
     * Check whether this Antibot instance is already initialized
333
     *
334
     * @throws RuntimeException If the Antibot instance still needs to be initialized
335
     */
336 6
    protected function checkInitialized(): void
337
    {
338
        // If the Antibot instance still needs to be initialized
339 6
        if (!$this->immutable) {
340 2
            throw new RuntimeException(
341 2
                RuntimeException::ANTIBOT_INITIALIZE_STR,
342 2
                RuntimeException::ANTIBOT_INITIALIZE
343
            );
344
        }
345 4
    }
346
347
    /**
348
     * Return the logger
349
     *
350
     * @return LoggerInterface Logger
351
     */
352 6
    public function getLogger(): LoggerInterface
353
    {
354 6
        return $this->logger;
355
    }
356
357
    /**
358
     * Sets a logger instance on the object
359
     *
360
     * @param LoggerInterface $logger Logger
361
     *
362
     * @return void
363
     */
364 12
    public function setLogger(LoggerInterface $logger): void
365
    {
366 12
        $this->logger = $logger;
367 12
    }
368
}
369