|
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; |
|
|
|
|
|
|
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) { |
|
|
|
|
|
|
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; |
|
|
|
|
|
|
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; |
|
|
|
|
|
|
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; |
|
|
|
|
|
|
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
|
|
|
|
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
$accountIdthat can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theidproperty of an instance of theAccountclass. 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.