Completed
Push — master ( dc9a8f...a8cb62 )
by François
02:21
created

Bouncer::forbidden()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
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 8
rs 9.4285
cc 1
eloc 6
nc 1
nop 0
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 string
43
     */
44
    protected $cookieName = 'bsid';
45
46
    /**
47
     * @var string
48
     */
49
    protected $cookiePath = '/';
50
51
    /**
52
     * @var \Bouncer\Cache\CacheInterface
53
     */
54
    protected $cache;
55
56
    /**
57
     * @var \Bouncer\Logger\LoggerInterface
58
     */
59
    protected $logger;
60
61
    /**
62
     * @var Request
63
     */
64
    protected $request;
65
66
    /**
67
     * @var array
68
     */
69
    protected $analyzers = array();
70
71
    /**
72
     * @var Identity
73
     */
74
    protected $identity;
75
76
    /**
77
     * Store metadata about the handling of the request
78
     *
79
     * @var array
80
     */
81
    protected $connection = array();
82
83
    /**
84
     * @var boolean
85
     */
86
    protected $started = false;
87
88
    /**
89
     * @var boolean
90
     */
91
    protected $ended = false;
92
93
    public function __construct(array $options = array())
94
    {
95
        if (!empty($options)) {
96
            $this->setOptions($options);
97
        }
98
99
        // Load Profile
100
        if (!$this->profile) {
101
            $this->profile = new \Bouncer\Profile\Standard;
102
        }
103
        call_user_func_array(array($this->profile, 'load'), array($this));
104
    }
105
106
    /*
107
     * Set the supported options
108
     */
109
    public function setOptions(array $options = array())
110
    {
111
        if (isset($options['cache'])) {
112
            $this->cache = $options['cache'];
113
        }
114
        if (isset($options['request'])) {
115
            $this->request = $options['request'];
116
        }
117
        if (isset($options['logger'])) {
118
            $this->logger = $options['logger'];
119
        }
120
        if (isset($options['profile'])) {
121
            $this->profile = $options['profile'];
122
        }
123
        if (isset($options['cookieName'])) {
124
            $this->cookieName = $options['cookieName'];
125
        }
126
        if (isset($options['cookiePath'])) {
127
            $this->cookiePath = $options['cookiePath'];
128
        }
129
    }
130
131
    /**
132
     * @throw Exception
133
     */
134
    public function error($message)
135
    {
136
        if ($this->throwExceptions) {
137
            throw new Exception($message);
138
        }
139
        if ($this->logErrors) {
140
            error_log("Bouncer: {$message}");
141
        }
142
    }
143
144
    /**
145
     * @return \Bouncer\Cache\CacheInterface
146
     */
147
    public function getCache($reportError = false)
148
    {
149
        if (empty($this->cache)) {
150
            if ($reportError) {
151
                $this->error('No cache available.');
152
            }
153
            return;
154
        }
155
156
        return $this->cache;
157
    }
158
159
    /**
160
     * @return \Bouncer\Logger\LoggerInterface
161
     */
162
    public function getLogger($reportError = false)
163
    {
164
        if (empty($this->logger)) {
165
            if ($reportError) {
166
                $this->error('No logger available.');
167
            }
168
            return;
169
        }
170
171
        return $this->logger;
172
    }
173
174
    /**
175
     * @return Request
176
     */
177
    public function getRequest()
178
    {
179
        if (isset($this->request)) {
180
            return $this->request;
181
        }
182
183
        $request = Request::createFromGlobals();
184
185
        return $this->request = $request;
186
    }
187
188
    /**
189
     * @return string
190
     */
191
    public function getUserAgent()
192
    {
193
        return $this->getRequest()->getUserAgent();
194
    }
195
196
    /**
197
     * @return string
198
     */
199
    public function getAddr()
200
    {
201
        return $this->getRequest()->getAddr();
202
    }
203
204
    /**
205
     * @return array
206
     */
207
    public function getHeaders()
208
    {
209
        $request = $this->getRequest();
210
211
        $headers = $request->getHeaders();
212
213
        // TODO: this should be deprecated
214
        $protocol = $this->getProtocol();
215
        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...
216
            $headers['protocol'] = $protocol;
217
        }
218
219
        return $headers;
220
    }
221
222
    /**
223
     * @return array
224
     */
225
    public function getCookies()
226
    {
227
        $names = array($this->cookieName, '__utmz', '__utma');
228
229
        $request = $this->getRequest();
230
231
        return $request->getCookies($names);
232
    }
233
234
    /**
235
     * Return the current session id (from Cookie)
236
     *
237
     * @return string|null
238
     */
239
    public function getSession()
240
    {
241
        $request = $this->getRequest();
242
243
        return $request->getCookie($this->cookieName);
244
    }
245
246
    /**
247
     * Return the protocol of the request: HTTP/1.0 or HTTP/1.1
248
     *
249
     * @return string|null
250
     */
251
    public function getProtocol()
252
    {
253
        $request = $this->getRequest();
254
255
        return $request->getProtocol();
256
    }
257
258
    /**
259
     * @return Identity
260
     */
261
    public function getIdentity()
262
    {
263
        if (isset($this->identity)) {
264
            return $this->identity;
265
        }
266
267
        $cache = $this->getCache();
268
269
        $addr  = $this->getAddr();
270
        $haddr = self::hash($addr);
271
272
        $headers = $this->getHeaders();
273
        $fingerprint = Fingerprint::generate($headers);
274
275
        $id = self::hash($fingerprint . $haddr);
276
277
        // Try to get identity from cache
278
        if ($cache) {
279
            $identity = $cache->getIdentity($id);
280
            if ($identity) {
281
                return $this->identity = new Identity($identity);
282
            }
283
        }
284
285
        // Build base identity
286
        $identity = array(
287
            'id'          => $id,
288
            'addr'        => $addr,
289
            'haddr'       => $haddr,
290
            'headers'     => $headers,
291
            'fingerprint' => $fingerprint
292
        );
293
294
        // Extra identity (optional)
295
        $protocol = $this->getProtocol();
296
        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...
297
            $identity['protocol'] = $protocol;
298
        }
299
        $cookies = $this->getCookies();
300
        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...
301
            $identity['cookies'] = $cookies;
302
        }
303
        $session = $this->getSession();
304
        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...
305
            $identity['session'] = $session;
306
        }
307
308
        // Process Analyzers
309
        $identity = $this->processAnalyzers('identity', $identity);
310
311
        // Store Identity in cache
312
        if ($cache) {
313
            $cache->setIdentity($id, $identity);
314
        }
315
316
        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 309 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...
317
    }
318
319
    public function getConnection()
320
    {
321
        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...
322
            $this->initConnection();
323
        }
324
325
        return $this->connection;
326
    }
327
328
    /*
329
     * Init the connection with id, time and start.
330
     */
331
    public function initConnection()
332
    {
333
        $this->connection = array();
334
        $this->connection['pid']   = getmypid();
335
        $this->connection['time']  = time();
336
        $this->connection['start'] = microtime(true);
337
    }
338
339
    /*
340
     * Complete the connection with end, exec_time, memory_usage and response_status.
341
     */
342
    public function completeConnection()
343
    {
344
        // Session (from Cookie)
345
        $session = $this->getSession();
346
        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...
347
            $this->connection['session'] = $session;
348
        }
349
350
        // Measure execution time
351
        $this->connection['end'] = microtime(true);
352
        $this->connection['exec_time'] = round($this->connection['end'] - $this->connection['start'], 4);
353
        if (!empty($this->connection['throttle_time'])) {
354
             $this->connection['exec_time'] -= $this->connection['throttle_time'];
355
        }
356
        unset($this->connection['end'], $this->connection['start']);
357
358
        // Report Memory Usage
359
        $this->connection['memory_usage'] = memory_get_peak_usage();
360
361
        // Add response
362
        if (function_exists('http_response_code')) {
363
            $responseStatus = http_response_code();
364
            if ($responseStatus) {
365
                $this->connection['response']['status'] = $responseStatus;
366
            }
367
        }
368
    }
369
370
    /*
371
     * Register an analyzer for a given type.
372
     *
373
     * @param string
374
     * @param callable
375
     * @param int
376
     */
377
    public function registerAnalyzer($type, $callable, $priority = 100)
378
    {
379
        $this->analyzers[$type][] = array($callable, $priority);
380
    }
381
382
    /*
383
     * Process Analyzers for a given type. Return the modified array or object.
384
     *
385
     * @param string
386
     * @param array|object
387
     *
388
     * @return array|object
389
     */
390
    protected function processAnalyzers($type, $value)
391
    {
392
        if (isset($this->analyzers[$type])) {
393
            // TODO: order analyzers by priority
394
            foreach ($this->analyzers[$type] as $array) {
395
                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...
396
                $value = call_user_func_array($callable, array($value));
397
            }
398
        }
399
        return $value;
400
    }
401
402
    /*
403
     * Start a Bouncer session
404
     */
405
    public function start()
406
    {
407
        // Already started, skip
408
        if ($this->started === true) {
409
            return;
410
        }
411
412
        $this->initConnection();
413
414
        $this->initSession();
415
416
        register_shutdown_function(array($this, 'end'));
417
418
        $this->started = true;
419
    }
420
421
    /*
422
     * Set a cookie containing the session id
423
     */
424
    public function initSession()
425
    {
426
        $identity = $this->getIdentity();
427
428
        if ($identity->hasAttribute('session')) {
429
            $curentSession = $this->getSession();
430
            $identitySession = $identity->getAttribute('session');
431
            if (empty($curentSession) || $curentSession !== $identitySession) {
432
                setcookie($this->cookieName, $identitySession, time() + (60 * 60 * 24 * 365 * 2), $this->cookiePath);
433
            }
434
        }
435
    }
436
437
    /*
438
     * Sleep if Identity status is of a certain value.
439
     *
440
     * @param array
441
     * @param int
442
     * @param int
443
     *
444
     */
445
    public function sleep($statuses = array(), $minimum = 1000, $maximum = 2500)
446
    {
447
        $identity = $this->getIdentity();
448
449
        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...
450
            $throttle_time = rand($minimum * 1000, $maximum * 1000);
451
            usleep($throttle_time);
452
            $this->connection['throttle_time'] = round($throttle_time / 1000 / 1000, 3);
453
        }
454
    }
455
456
    /*
457
     * Ban if Identity status is of a certain value.
458
     */
459
    public function ban($statuses = array())
460
    {
461
        $identity = $this->getIdentity();
462
463
        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...
464
            $this->connection['banned'] = true;
465
            $this->forbidden();
466
        }
467
    }
468
469
    /*
470
     * Complete the connection then attempt to log.
471
     */
472
    public function end()
473
    {
474
        // Already ended, skip
475
        if ($this->ended === true) {
476
            return;
477
        }
478
479
        $this->completeConnection();
480
481
        // We really want to avoid throwing exceptions there
482
        try {
483
            $this->log();
484
        } catch (Exception $e) {
485
            error_log($e->getMessage());
486
        }
487
488
        $this->ended = true;
489
    }
490
491
    /*
492
     * Log the connection to the logging backend.
493
     */
494
    public function log()
495
    {
496
        $connection = $this->getConnection();
497
        $identity = $this->getIdentity();
498
        $request = $this->getRequest();
499
500
        $logger = $this->getLogger();
501
        if ($logger) {
502
            $logger->log($connection, $identity, $request);
503
        }
504
    }
505
506
    // Static
507
508
    public static function forbidden()
509
    {
510
        $code = '403';
511
        $message = 'Forbidden';
512
        self::responseStatus($code, $message);
513
        echo $message;
514
        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...
515
    }
516
517
    public static function unavailable()
518
    {
519
        $code = '503';
520
        $message = 'Service Unavailable';
521
        self::responseStatus($code, $message);
522
        echo $message;
523
        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...
524
    }
525
526
    public static function responseStatus($code, $message)
527
    {
528
        if (function_exists('http_response_code')) {
529
            http_response_code($code);
530
        } else {
531
            header("HTTP/1.0 $code $message");
532
            header("Status: $code $message");
533
        }
534
    }
535
536
    public static function hash($string)
537
    {
538
        return md5($string);
539
    }
540
541
}
542