Completed
Push — master ( db3c82...df4949 )
by Joschi
03:02
created

Antibot   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 262
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 82.35%

Importance

Changes 0
Metric Value
wmc 28
lcom 1
cbo 6
dl 0
loc 262
ccs 56
cts 68
cp 0.8235
rs 10
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getPrefix() 0 4 1
A getParameterPrefix() 0 9 2
A getUnique() 0 4 1
A getData() 0 4 1
B validate() 0 32 6
A armor() 0 16 4
A sortValidators() 0 10 3
A initialize() 0 8 1
A calculateSignature() 0 6 1
A extractData() 0 8 5
A checkImmutable() 0 9 2
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\ActorInterface;
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\RuntimeException;
44
use Jkphl\Antibot\Domain\Exceptions\WhitelistValidationException;
45
use Jkphl\Antibot\Domain\Model\ValidationResult;
46
use Psr\Http\Message\ServerRequestInterface;
47
48
/**
49
 * Antibot core
50
 *
51
 * @package    Jkphl\Antibot
52
 * @subpackage Jkphl\Antibot\Domain
53
 */
54
class Antibot
55
{
56
    /**
57
     * Antibot configuration
58
     *
59
     * @var array
60
     */
61
    protected $config;
62
    /**
63
     * Session persistent, unique token
64
     *
65
     * @var string
66
     */
67
    protected $unique;
68
    /**
69
     * Antibot prefix
70
     *
71
     * @var string
72
     */
73
    protected $prefix;
74
    /**
75
     * Unique signature
76
     *
77
     * @var string
78
     */
79
    protected $signature;
80
    /**
81
     * Parameter prefix
82
     *
83
     * @var string
84
     */
85
    protected $parameterPrefix;
86
    /**
87
     * GET & POST data
88
     *
89
     * @var null|string[]
90
     */
91
    protected $data = null;
92
    /**
93
     * Validators
94
     *
95
     * @var ValidatorInterface[]
96
     */
97
    protected $validators = [];
98
    /**
99
     * Actors
100
     *
101
     * @var ActorInterface[]
102
     */
103
    protected $actors = [];
104
    /**
105
     * Immutable instance
106
     *
107
     * @var bool
108
     */
109
    protected $immutable = false;
110
    /**
111
     * Default antibot prefix
112
     *
113
     * @var string
114
     */
115
    const DEFAULT_PREFIX = 'antibot';
116
117
    /**
118
     * Antibot constructor
119
     *
120
     * @param array $config  Configuration
121
     * @param string $unique Session-persistent, unique key
122
     * @param string $prefix Prefix
123
     */
124 9
    public function __construct($config, string $unique, string $prefix = self::DEFAULT_PREFIX)
125
    {
126 9
        $this->config = $config;
127 9
        $this->unique = $unique;
128 9
        $this->prefix = $prefix;
129 9
    }
130
131
    /**
132
     * Return the prefix
133
     *
134
     * @return string Prefix
135
     */
136 1
    public function getPrefix(): string
137
    {
138 1
        return $this->prefix;
139
    }
140
141
    /**
142
     * Return the parameter prefix
143
     *
144
     * @return string Parameter prefix
145
     * @throws RuntimeException If Antibot needs to be initialized
146
     */
147 3
    public function getParameterPrefix(): string
148
    {
149
        // If Antibot needs to be initialized
150 3
        if (!$this->immutable) {
151
            throw new RuntimeException(RuntimeException::ANTIBOT_INITIALIZE_STR, RuntimeException::ANTIBOT_INITIALIZE);
152
        }
153
154 3
        return $this->parameterPrefix;
155
    }
156
157
    /**
158
     * Return the session persistent, unique token
159
     *
160
     * @return string Session persistent, unique token
161
     */
162 3
    public function getUnique(): string
163
    {
164 3
        return $this->unique;
165
    }
166
167
    /**
168
     * Return the submitted Antibot data
169
     *
170
     * @return string[] Antibot data
171
     */
172 3
    public function getData(): ?array
173
    {
174 3
        return $this->data;
175
    }
176
177
    /**
178
     * Validate a request
179
     *
180
     * @param ServerRequestInterface $request Request
181
     *
182
     * @return ValidationResult Validation result
183
     */
184 3
    public function validate(ServerRequestInterface $request): ValidationResult
185
    {
186 3
        $this->initialize($request);
187 3
        $result = new ValidationResult();
188
189
        // Run through all validators (in order)
190
        /** @var ValidatorInterface $validator */
191 3
        foreach ($this->validators as $validator) {
192
            try {
193 3
                if (!$validator->validate($request, $this)) {
194 2
                    $result->setValid(false);
195
                }
196
197
                // If the request failed a blacklist test
198 2
            } catch (BlacklistValidationException $e) {
199
                $result->addBlacklist($e->getMessage());
200
                $result->setValid(false);
201
202
                // If the request passed a whitelist test
203 2
            } catch (WhitelistValidationException $e) {
204 1
                $result->addWhitelist($e->getMessage());
205 1
                break;
206
207
                // If an error occured
208 1
            } catch (ErrorException $e) {
209 1
                $result->addError($e);
210 2
                $result->setValid(false);
211
            }
212
        }
213
214 3
        return $result;
215
    }
216
217
    /**
218
     * Return the Antibot armor
219
     *
220
     * @param ServerRequestInterface $request Request
221
     * @param bool $raw                       Return input elements
222
     *
223
     * @return string|array Antibot armor
224
     */
225 3
    public function armor(ServerRequestInterface $request, bool $raw = false)
226
    {
227 3
        $this->initialize($request);
228 3
        $armor = [];
229
230
        // Run through all validators (in order)
231
        /** @var ValidatorInterface $validator */
232 3
        foreach ($this->validators as $validator) {
233 3
            $validatorArmor = $validator->armor($request, $this);
234 3
            if (!empty($validatorArmor)) {
235 3
                $armor = array_merge($armor, $validatorArmor);
236
            }
237
        }
238
239 3
        return $raw ? $armor : implode('', array_map('strval', $armor));
240
    }
241
242
    /**
243
     * Compare and sort validators
244
     *
245
     * @param ValidatorInterface $validator1 Validator 1
246
     * @param ValidatorInterface $validator2 Validator 2
247
     *
248
     * @return int Sorting
249
     */
250
    protected function sortValidators(ValidatorInterface $validator1, ValidatorInterface $validator2): int
251
    {
252
        $validatorPos1 = $validator1->getPosition();
253
        $validatorPos2 = $validator2->getPosition();
254
        if ($validatorPos1 == $validatorPos2) {
255
            return 0;
256
        }
257
258
        return ($validatorPos1 > $validatorPos2) ? 1 : -1;
259
    }
260
261
    /**
262
     * Pre-validation initialization
263
     *
264
     * @param ServerRequestInterface $request Request
265
     */
266 4
    protected function initialize(ServerRequestInterface $request): void
267
    {
268 4
        usort($this->validators, [$this, 'sortValidators']);
269 4
        $this->immutable       = true;
270 4
        $this->signature       = $this->calculateSignature();
271 4
        $this->parameterPrefix = $this->prefix.'_'.$this->signature;
272 4
        $this->extractData($request);
273 4
    }
274
275
    /**
276
     * Calculate the unique signature
277
     *
278
     * @return string Signature
279
     */
280 4
    protected function calculateSignature(): string
281
    {
282 4
        $params = [$this->prefix, $this->config, $this->validators, $this->actors];
283
284 4
        return sha1($this->unique.serialize($params));
285
    }
286
287
    /**
288
     * Extract the antibot data from GET and POST parameters
289
     *
290
     * @param ServerRequestInterface $request Request
291
     */
292 4
    protected function extractData(ServerRequestInterface $request): void
293
    {
294 4
        $get        = $request->getQueryParams();
295 4
        $get        = empty($get[$this->parameterPrefix]) ? null : $get[$this->parameterPrefix];
296 4
        $post       = $request->getParsedBody();
297 4
        $post       = empty($post[$this->parameterPrefix]) ? null : $post[$this->parameterPrefix];
298 4
        $this->data = (($get !== null) || ($post !== null)) ? array_merge((array)$get, (array)$post) : null;
0 ignored issues
show
Documentation Bug introduced by
It seems like $get !== null || $post !..., (array) $post) : null can also be of type array. However, the property $data is declared as type null|array<integer,string>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
299 4
    }
300
301
    /**
302
     * Check whether this Antibot instance is immutable
303
     *
304
     * @throws RuntimeException If the Antibot instance is immutable
305
     */
306 4
    protected function checkImmutable(): void
307
    {
308 4
        if ($this->immutable) {
309
            throw new RuntimeException(
310
                RuntimeException::ANTIBOT_IMMUTABLE_STR,
311
                RuntimeException::ANTIBOT_IMMUTABLE
312
            );
313
        }
314 4
    }
315
}
316