Completed
Push — master ( a8cb62...764ba2 )
by François
03:52
created

Bouncer::getAddress()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

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 4
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 $response;
70
71
    /**
72
     * @var array
73
     */
74
    protected $analyzers = array();
75
76
    /**
77
     * @var Identity
78
     */
79
    protected $identity;
80
81
    /**
82
     * Store internal metadata
83
     *
84
     * @var array
85
     */
86
    protected $context = array();
87
88
    /**
89
     * @var boolean
90
     */
91
    protected $started = false;
92
93
    /**
94
     * @var boolean
95
     */
96
    protected $ended = false;
97
98
    public function __construct(array $options = array())
99
    {
100
        if (!empty($options)) {
101
            $this->setOptions($options);
102
        }
103
104
        // Load Profile
105
        if (!$this->profile) {
106
            $this->profile = new \Bouncer\Profile\Standard;
107
        }
108
        call_user_func_array(array($this->profile, 'load'), array($this));
109
    }
110
111
    /*
112
     * Set the supported options
113
     */
114
    public function setOptions(array $options = array())
115
    {
116
        if (isset($options['cache'])) {
117
            $this->cache = $options['cache'];
118
        }
119
        if (isset($options['request'])) {
120
            $this->request = $options['request'];
121
        }
122
        if (isset($options['logger'])) {
123
            $this->logger = $options['logger'];
124
        }
125
        if (isset($options['profile'])) {
126
            $this->profile = $options['profile'];
127
        }
128
        if (isset($options['cookieName'])) {
129
            $this->cookieName = $options['cookieName'];
130
        }
131
        if (isset($options['cookiePath'])) {
132
            $this->cookiePath = $options['cookiePath'];
133
        }
134
    }
135
136
    /**
137
     * @throw Exception
138
     */
139
    public function error($message)
140
    {
141
        if ($this->throwExceptions) {
142
            throw new Exception($message);
143
        }
144
        if ($this->logErrors) {
145
            error_log("Bouncer: {$message}");
146
        }
147
    }
148
149
    /**
150
     * @return \Bouncer\Cache\CacheInterface
151
     */
152
    public function getCache($reportError = false)
153
    {
154
        if (empty($this->cache)) {
155
            if ($reportError) {
156
                $this->error('No cache available.');
157
            }
158
            return;
159
        }
160
161
        return $this->cache;
162
    }
163
164
    /**
165
     * @return \Bouncer\Logger\LoggerInterface
166
     */
167
    public function getLogger($reportError = false)
168
    {
169
        if (empty($this->logger)) {
170
            if ($reportError) {
171
                $this->error('No logger available.');
172
            }
173
            return;
174
        }
175
176
        return $this->logger;
177
    }
178
179
    /**
180
     * @return Request
181
     */
182
    public function getRequest()
183
    {
184
        if (isset($this->request)) {
185
            return $this->request;
186
        }
187
188
        $request = Request::createFromGlobals();
189
190
        return $this->request = $request;
191
    }
192
193
    /**
194
     * @return array
195
     */
196
    public function getResponse()
197
    {
198
        return $this->response;
199
    }
200
201
    /**
202
     * @return string
203
     */
204
    public function getUserAgent()
205
    {
206
        return $this->getRequest()->getUserAgent();
207
    }
208
209
    /**
210
     * @return string
211
     */
212
    public function getAddr()
213
    {
214
        return $this->getRequest()->getAddr();
215
    }
216
217
    /**
218
     * @return Address
219
     */
220
    public function getAddress()
221
    {
222
        $addr = $this->getRequest()->getAddr();
223
224
        $address = new Address($addr);
225
226
        return $address;
227
    }
228
229
    /**
230
     * @return array
231
     */
232
    public function getHeaders()
233
    {
234
        $request = $this->getRequest();
235
236
        $headers = $request->getHeaders();
237
238
        return $headers;
239
    }
240
241
    /**
242
     * @return Signature
243
     */
244
    public function getSignature()
245
    {
246
        $headers = $this->getHeaders();
247
248
        $signature = new Signature(array('headers' => $headers));
249
250
        return $signature;
251
    }
252
253
    /**
254
     * @return array
255
     */
256
    public function getCookies()
257
    {
258
        $names = array($this->cookieName, '__utmz', '__utma');
259
260
        $request = $this->getRequest();
261
262
        return $request->getCookies($names);
263
    }
264
265
    /**
266
     * Return the current session id (from Cookie)
267
     *
268
     * @return string|null
269
     */
270
    public function getSession()
271
    {
272
        $request = $this->getRequest();
273
274
        return $request->getCookie($this->cookieName);
275
    }
276
277
    /**
278
     * Return the protocol of the request: HTTP/1.0 or HTTP/1.1
279
     *
280
     * @return string|null
281
     */
282
    public function getProtocol()
283
    {
284
        $request = $this->getRequest();
285
286
        return $request->getProtocol();
287
    }
288
289
    /**
290
     * @return Identity
291
     */
292
    public function getIdentity()
293
    {
294
        if (isset($this->identity)) {
295
            return $this->identity;
296
        }
297
298
        $cache = $this->getCache();
299
300
        $identity = new Identity(array(
301
            'address' => $this->getAddress(),
302
            'headers' => $this->getHeaders(),
303
        ));
304
305
306
        $id = $identity->getId();
307
308
        // Try to get identity from cache
309
        if ($cache) {
310
            $cacheIdentity = $cache->getIdentity($id);
311
            if ($cacheIdentity) {
312
                return $this->identity = $cacheIdentity;
313
                // return $this->identity = new Identity($cacheIdentity);
314
            }
315
        }
316
317
        // Process Analyzers
318
        $identity = $this->processAnalyzers('identity', $identity);
319
320
        // Store Identity in cache
321
        if ($cache) {
322
            $cache->setIdentity($id, $identity);
323
        }
324
325
        return $this->identity = $identity;
0 ignored issues
show
Documentation Bug introduced by
It seems like $identity can also be of type array. However, the property $identity is declared as type object<Bouncer\Identity>. 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...
Bug Compatibility introduced by
The expression $this->identity = $identity; of type array|object adds the type array to the return on line 325 which is incompatible with the return type documented by Bouncer\Bouncer::getIdentity of type Bouncer\Identity.
Loading history...
326
    }
327
328
    public function getContext()
329
    {
330
        if (!$this->context) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->context 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...
331
            $this->initContext();
332
        }
333
334
        return $this->context;
335
    }
336
337
    /*
338
     * Init the context with id, time and start.
339
     */
340
    public function initContext()
341
    {
342
        $this->context = array();
343
        $this->context['pid']   = getmypid();
344
        $this->context['time']  = time();
345
        $this->context['start'] = microtime(true);
346
    }
347
348
    /*
349
     * Complete the context with end, exec_time and memory_usage.
350
     */
351
    public function completeContext()
352
    {
353
        // Session (from Cookie)
354
        $session = $this->getSession();
355
        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...
356
            $this->context['session'] = $session;
357
        }
358
359
        // Measure execution time
360
        $this->context['end'] = microtime(true);
361
        $this->context['exec_time'] = round($this->context['end'] - $this->context['start'], 4);
362
        if (!empty($this->context['throttle_time'])) {
363
             $this->context['exec_time'] -= $this->ccontext['throttle_time'];
0 ignored issues
show
Bug introduced by
The property ccontext does not seem to exist. Did you mean context?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
364
        }
365
        unset($this->context['end'], $this->context['start']);
366
367
        // Report Memory Usage
368
        $this->context['memory_usage'] = memory_get_peak_usage();
369
    }
370
371
    /*
372
     * Complete the response with status
373
     */
374
    public function completeResponse()
375
    {
376
        if (!isset($this->response)) {
377
            $this->response = array();
378
        }
379
380
        if (function_exists('http_response_code')) {
381
            $responseStatus = http_response_code();
382
            if ($responseStatus) {
383
                $this->response['status'] = $responseStatus;
384
            }
385
        }
386
    }
387
    /*
388
     * Register an analyzer for a given type.
389
     *
390
     * @param string
391
     * @param callable
392
     * @param int
393
     */
394
    public function registerAnalyzer($type, $callable, $priority = 100)
395
    {
396
        $this->analyzers[$type][] = array($callable, $priority);
397
    }
398
399
    /*
400
     * Process Analyzers for a given type. Return the modified array or object.
401
     *
402
     * @param string
403
     * @param array|object
404
     *
405
     * @return array|object
406
     */
407
    protected function processAnalyzers($type, $value)
408
    {
409
        if (isset($this->analyzers[$type])) {
410
            // TODO: order analyzers by priority
411
            foreach ($this->analyzers[$type] as $array) {
412
                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...
413
                $value = call_user_func_array($callable, array($value));
414
            }
415
        }
416
        return $value;
417
    }
418
419
    /*
420
     * Start a Bouncer session
421
     */
422
    public function start()
423
    {
424
        // Already started, skip
425
        if ($this->started === true) {
426
            return;
427
        }
428
429
        $this->initContext();
430
431
        $this->initSession();
432
433
        register_shutdown_function(array($this, 'end'));
434
435
        $this->started = true;
436
    }
437
438
    /*
439
     * Set a cookie containing the session id
440
     */
441
    public function initSession()
442
    {
443
        $identity = $this->getIdentity();
444
445
        if ($identity->hasAttribute('session')) {
446
            $curentSession = $this->getSession();
447
            $identitySession = $identity->getAttribute('session');
448
            if (empty($curentSession) || $curentSession !== $identitySession) {
449
                setcookie($this->cookieName, $identitySession, time() + (60 * 60 * 24 * 365 * 2), $this->cookiePath);
450
            }
451
        }
452
    }
453
454
    /*
455
     * Sleep if Identity status is of a certain value.
456
     *
457
     * @param array
458
     * @param int
459
     * @param int
460
     *
461
     */
462
    public function sleep($statuses = array(), $minimum = 1000, $maximum = 2500)
463
    {
464
        $identity = $this->getIdentity();
465
466
        if (in_array($identity->getStatus(), $statuses)) {
467
            $throttle_time = rand($minimum * 1000, $maximum * 1000);
468
            usleep($throttle_time);
469
            $this->connection['throttle_time'] = round($throttle_time / 1000 / 1000, 3);
0 ignored issues
show
Bug introduced by
The property connection does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
470
        }
471
    }
472
473
    /*
474
     * Ban if Identity status is of a certain value.
475
     */
476
    public function ban($statuses = array())
477
    {
478
        $identity = $this->getIdentity();
479
480
        if (in_array($identity->getStatus(), $statuses)) {
481
            $this->connection['banned'] = true;
482
            $this->forbidden();
483
        }
484
    }
485
486
    /*
487
     * Complete the connection then attempt to log.
488
     */
489
    public function end()
490
    {
491
        // Already ended, skip
492
        if ($this->ended === true) {
493
            return;
494
        }
495
496
        $this->completeContext();
497
        $this->completeResponse();
498
499
        // We really want to avoid throwing exceptions there
500
        try {
501
            $this->log();
502
        } catch (Exception $e) {
503
            error_log($e->getMessage());
504
        }
505
506
        $this->ended = true;
507
    }
508
509
    /*
510
     * Log the connection to the logging backend.
511
     */
512
    public function log()
513
    {
514
        $logEntry = array(
515
            'address'  => $this->getAddress(),
516
            'request'  => $this->getRequest(),
517
            'response' => $this->getResponse(),
518
            'identity' => $this->getIdentity(),
519
            'context'  => $this->getContext(),
520
        );
521
522
        $logger = $this->getLogger();
523
        if ($logger) {
524
            $logger->log($logEntry);
525
        }
526
    }
527
528
    // Static
529
530
    public static function forbidden()
531
    {
532
        $code = '403';
533
        $message = 'Forbidden';
534
        self::responseStatus($code, $message);
535
        echo $message;
536
        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...
537
    }
538
539
    public static function unavailable()
540
    {
541
        $code = '503';
542
        $message = 'Service Unavailable';
543
        self::responseStatus($code, $message);
544
        echo $message;
545
        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...
546
    }
547
548
    public static function responseStatus($code, $message)
549
    {
550
        if (function_exists('http_response_code')) {
551
            http_response_code($code);
552
        } else {
553
            header("HTTP/1.0 $code $message");
554
            header("Status: $code $message");
555
        }
556
    }
557
558
    public static function hash($string)
559
    {
560
        return md5($string);
561
    }
562
563
}
564