Completed
Push — master ( f50a57...dc9a8f )
by François
02:44
created

Bouncer::processAnalyzers()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
rs 9.4285
cc 3
eloc 6
nc 2
nop 2
1
<?php
2
3
/*
4
 * This file is part of the Bouncer package.
5
 *
6
 * (c) François Hodierne <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Bouncer;
13
14
class Bouncer
15
{
16
17
    const NICE       = 'nice';
18
    const OK         = 'ok';
19
    const SUSPICIOUS = 'suspicious';
20
    const BAD        = 'bad';
21
22
    const ROBOT      = 'robot';
23
    const BROWSER    = 'browser';
24
    const UNKNOWN    = 'unknown';
25
26
    /**
27
     * @var string|object
28
     */
29
    protected $profile;
30
31
    /**
32
     * @var boolean
33
     */
34
    protected $throwExceptions = false;
35
36
    /**
37
     * @var boolean
38
     */
39
    protected $logErrors = true;
40
41
    /**
42
     * @var boolean
43
     */
44
    protected $cookieName = 'bsid';
45
46
    /**
47
     * @var \Bouncer\Cache\CacheInterface
48
     */
49
    protected $cache;
50
51
    /**
52
     * @var \Bouncer\Logger\LoggerInterface
53
     */
54
    protected $logger;
55
56
    /**
57
     * @var Request
58
     */
59
    protected $request;
60
61
    /**
62
     * @var array
63
     */
64
    protected $analyzers = array();
65
66
    /**
67
     * @var Identity
68
     */
69
    protected $identity;
70
71
    /**
72
     * Store metadata about the handling of the request
73
     *
74
     * @var array
75
     */
76
    protected $connection = array();
77
78
    /**
79
     * @var boolean
80
     */
81
    protected $started = false;
82
83
    /**
84
     * @var boolean
85
     */
86
    protected $ended = false;
87
88
    public function __construct(array $options = array())
89
    {
90
        if (!empty($options)) {
91
            $this->setOptions($options);
92
        }
93
94
        // Load Profile
95
        if (!$this->profile) {
96
            $this->profile = new \Bouncer\Profile\Standard;
97
        }
98
        call_user_func_array(array($this->profile, 'load'), array($this));
99
    }
100
101
    /*
102
     * Set the supported options
103
     */
104
    public function setOptions(array $options = array())
105
    {
106
        if (isset($options['cache'])) {
107
            $this->cache = $options['cache'];
108
        }
109
        if (isset($options['request'])) {
110
            $this->request = $options['request'];
111
        }
112
        if (isset($options['logger'])) {
113
            $this->logger = $options['logger'];
114
        }
115
        if (isset($options['profile'])) {
116
            $this->profile = $options['profile'];
117
        }
118
        if (isset($options['cookieName'])) {
119
            $this->cookieName = $options['cookieName'];
120
        }
121
    }
122
123
    /**
124
     * @throw Exception
125
     */
126
    public function error($message)
127
    {
128
        if ($this->throwExceptions) {
129
            throw new Exception($message);
130
        }
131
        if ($this->logErrors) {
132
            error_log("Bouncer: {$message}");
133
        }
134
    }
135
136
    /**
137
     * @return \Bouncer\Cache\CacheInterface
138
     */
139
    public function getCache($reportError = false)
140
    {
141
        if (empty($this->cache)) {
142
            if ($reportError) {
143
                $this->error('No cache available.');
144
            }
145
            return;
146
        }
147
148
        return $this->cache;
149
    }
150
151
    /**
152
     * @return \Bouncer\Logger\LoggerInterface
153
     */
154
    public function getLogger($reportError = false)
155
    {
156
        if (empty($this->logger)) {
157
            if ($reportError) {
158
                $this->error('No logger available.');
159
            }
160
            return;
161
        }
162
163
        return $this->logger;
164
    }
165
166
    /**
167
     * @return Request
168
     */
169
    public function getRequest()
170
    {
171
        if (isset($this->request)) {
172
            return $this->request;
173
        }
174
175
        $request = Request::createFromGlobals();
176
177
        return $this->request = $request;
178
    }
179
180
    /**
181
     * @return string
182
     */
183
    public function getUserAgent()
184
    {
185
        return $this->getRequest()->getUserAgent();
186
    }
187
188
    /**
189
     * @return string
190
     */
191
    public function getAddr()
192
    {
193
        return $this->getRequest()->getAddr();
194
    }
195
196
    /**
197
     * @return array
198
     */
199
    public function getHeaders()
200
    {
201
        $request = $this->getRequest();
202
203
        $headers = $request->getHeaders();
204
205
        // TODO: this should be deprecated
206
        $protocol = $this->getProtocol();
207
        if ($protocol) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $protocol 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...
208
            $headers['protocol'] = $protocol;
209
        }
210
211
        return $headers;
212
    }
213
214
    /**
215
     * @return array
216
     */
217
    public function getCookies()
218
    {
219
        $names = array($this->cookieName, '__utmz', '__utma');
220
221
        $request = $this->getRequest();
222
223
        return $request->getCookies($names);
224
    }
225
226
    /**
227
     * Return the current session id (from Cookie)
228
     *
229
     * @return string|null
230
     */
231
    public function getSession()
232
    {
233
        $request = $this->getRequest();
234
235
        return $request->getCookie($this->cookieName);
236
    }
237
238
    /**
239
     * Return the protocol of the request: HTTP/1.0 or HTTP/1.1
240
     *
241
     * @return string|null
242
     */
243
    public function getProtocol()
244
    {
245
        $request = $this->getRequest();
246
247
        return $request->getProtocol();
248
    }
249
250
    /**
251
     * @return Identity
252
     */
253
    public function getIdentity()
254
    {
255
        if (isset($this->identity)) {
256
            return $this->identity;
257
        }
258
259
        $cache = $this->getCache();
260
261
        $addr  = $this->getAddr();
262
        $haddr = self::hash($addr);
263
264
        $headers = $this->getHeaders();
265
        $fingerprint = Fingerprint::generate($headers);
266
267
        $id = self::hash($fingerprint . $haddr);
268
269
        // Try to get identity from cache
270
        if ($cache) {
271
            $identity = $cache->getIdentity($id);
272
            if ($identity) {
273
                return $this->identity = new Identity($identity);
274
            }
275
        }
276
277
        // Build base identity
278
        $identity = array(
279
            'id'          => $id,
280
            'addr'        => $addr,
281
            'haddr'       => $haddr,
282
            'headers'     => $headers,
283
            'fingerprint' => $fingerprint
284
        );
285
286
        // Extra identity (optional)
287
        $protocol = $this->getProtocol();
288
        if ($protocol) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $protocol 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...
289
            $identity['protocol'] = $protocol;
290
        }
291
        $cookies = $this->getCookies();
292
        if ($cookies) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cookies of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
293
            $identity['cookies'] = $cookies;
294
        }
295
        $session = $this->getSession();
296
        if ($session) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $session 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...
297
            $identity['session'] = $session;
298
        }
299
300
        // Process Analyzers
301
        $identity = $this->processAnalyzers('identity', $identity);
302
303
        // Store Identity in cache
304
        if ($cache) {
305
            $cache->setIdentity($id, $identity);
306
        }
307
308
        return $this->identity = new Identity($identity);
0 ignored issues
show
Bug introduced by
It seems like $identity defined by $this->processAnalyzers('identity', $identity) on line 301 can also be of type object; however, Bouncer\Identity::__construct() 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...
309
    }
310
311
    public function getConnection()
312
    {
313
        if (!$this->connection) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->connection of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
314
            $this->initConnection();
315
        }
316
317
        return $this->connection;
318
    }
319
320
    /*
321
     * Init the connection with id, time and start.
322
     */
323
    public function initConnection()
324
    {
325
        $this->connection = array();
326
        $this->connection['pid']   = getmypid();
327
        $this->connection['time']  = time();
328
        $this->connection['start'] = microtime(true);
329
    }
330
331
    /*
332
     * Complete the connection with end, exec_time, memory_usage and response_status.
333
     */
334
    public function completeConnection()
335
    {
336
        // Session (from Cookie)
337
        $session = $this->getSession();
338
        if ($session) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $session 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...
339
            $this->connection['session'] = $session;
340
        }
341
342
        // Measure execution time
343
        $this->connection['end'] = microtime(true);
344
        $this->connection['exec_time'] = round($this->connection['end'] - $this->connection['start'], 4);
345
        if (!empty($this->connection['throttle_time'])) {
346
             $this->connection['exec_time'] -= $this->connection['throttle_time'];
347
        }
348
        unset($this->connection['end'], $this->connection['start']);
349
350
        // Report Memory Usage
351
        $this->connection['memory_usage'] = memory_get_peak_usage();
352
353
        // Add response
354
        if (function_exists('http_response_code')) {
355
            $responseStatus = http_response_code();
356
            if ($responseStatus) {
357
                $this->connection['response']['status'] = $responseStatus;
358
            }
359
        }
360
    }
361
362
    /*
363
     * Register an analyzer for a given type.
364
     *
365
     * @param string
366
     * @param callable
367
     * @param int
368
     */
369
    public function registerAnalyzer($type, $callable, $priority = 100)
370
    {
371
        $this->analyzers[$type][] = array($callable, $priority);
372
    }
373
374
    /*
375
     * Process Analyzers for a given type. Return the modified array or object.
376
     *
377
     * @param string
378
     * @param array|object
379
     *
380
     * @return array|object
381
     */
382
    protected function processAnalyzers($type, $value)
383
    {
384
        if (isset($this->analyzers[$type])) {
385
            // TODO: order analyzers by priority
386
            foreach ($this->analyzers[$type] as $array) {
387
                list($callable, $priority) = $array;
0 ignored issues
show
Unused Code introduced by
The assignment to $priority is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
388
                $value = call_user_func_array($callable, array($value));
389
            }
390
        }
391
        return $value;
392
    }
393
394
    /*
395
     * Start a Bouncer session
396
     */
397
    public function start()
398
    {
399
        // Already started, skip
400
        if ($this->started === true) {
401
            return;
402
        }
403
404
        $this->initConnection();
405
406
        $this->initSession();
407
408
        register_shutdown_function(array($this, 'end'));
409
410
        $this->started = true;
411
    }
412
413
    /*
414
     * Set a cookie containing the session id
415
     */
416
    public function initSession()
417
    {
418
        $identity = $this->getIdentity();
419
420
        if ($identity->hasAttribute('session')) {
421
            $curentSession = $this->getSession();
422
            $identitySession = $identity->getAttribute('session');
423
            if (empty($curentSession) || $curentSession !== $identitySession) {
424
                setcookie($this->cookieName, $identitySession, time() + (60 * 60 * 24 * 365 * 2), '/');
425
            }
426
        }
427
    }
428
429
    /*
430
     * Sleep if Identity status is of a certain value.
431
     *
432
     * @param array
433
     * @param int
434
     * @param int
435
     *
436
     */
437
    public function sleep($statuses = array(), $minimum = 1000, $maximum = 2500)
438
    {
439
        $identity = $this->getIdentity();
440
441
        if (in_array($identity->getStatus(), $statuses)) {
0 ignored issues
show
Documentation Bug introduced by
The method getStatus does not exist on object<Bouncer\Identity>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
442
            $throttle_time = rand($minimum * 1000, $maximum * 1000);
443
            usleep($throttle_time);
444
            $this->connection['throttle_time'] = round($throttle_time / 1000 / 1000, 3);
445
        }
446
    }
447
448
    /*
449
     * Ban if Identity status is of a certain value.
450
     */
451
    public function ban($statuses = array())
452
    {
453
        $identity = $this->getIdentity();
454
455
        if (in_array($identity->getStatus(), $statuses)) {
0 ignored issues
show
Documentation Bug introduced by
The method getStatus does not exist on object<Bouncer\Identity>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
456
            $this->connection['banned'] = true;
457
            $this->forbidden();
458
        }
459
    }
460
461
    /*
462
     * Complete the connection then attempt to log.
463
     */
464
    public function end()
465
    {
466
        // Already ended, skip
467
        if ($this->ended === true) {
468
            return;
469
        }
470
471
        $this->completeConnection();
472
473
        // We really want to avoid throwing exceptions there
474
        try {
475
            $this->log();
476
        } catch (Exception $e) {
477
            error_log($e->getMessage());
478
        }
479
480
        $this->ended = true;
481
    }
482
483
    /*
484
     * Log the connection to the logging backend.
485
     */
486
    public function log()
487
    {
488
        $connection = $this->getConnection();
489
        $identity = $this->getIdentity();
490
        $request = $this->getRequest();
491
492
        $logger = $this->getLogger();
493
        if ($logger) {
494
            $logger->log($connection, $identity, $request);
495
        }
496
    }
497
498
    // Static
499
500
    public static function forbidden()
501
    {
502
        $code = '403';
503
        $message = 'Forbidden';
504
        self::response_status($code, $message);
505
        echo $message;
506
        exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method forbidden() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
507
    }
508
509
    public static function unavailable()
510
    {
511
        $code = '503';
512
        $message = 'Service Unavailable';
513
        self::response_status($code, $message);
514
        echo $message;
515
        exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method unavailable() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
516
    }
517
518
    public static function response_status($code, $message)
0 ignored issues
show
Coding Style introduced by
Method name "Bouncer::response_status" is not in camel caps format
Loading history...
519
    {
520
        if (function_exists('http_response_code')) {
521
            http_response_code($code);
522
        } else {
523
            header("HTTP/1.0 $code $message");
524
            header("Status: $code $message");
525
        }
526
    }
527
528
    public static function hash($string)
529
    {
530
        return md5($string);
531
    }
532
533
}
534