Completed
Push — master ( c75a1a...5e3b3c )
by François
02:10
created

Bouncer::getSession()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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