1
|
|
|
<?php declare(strict_types=1); |
2
|
|
|
|
3
|
|
|
namespace Neomerx\Cors; |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* Copyright 2015-2019 [email protected] |
7
|
|
|
* |
8
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
9
|
|
|
* you may not use this file except in compliance with the License. |
10
|
|
|
* You may obtain a copy of the License at |
11
|
|
|
* |
12
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0 |
13
|
|
|
* |
14
|
|
|
* Unless required by applicable law or agreed to in writing, software |
15
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS, |
16
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
17
|
|
|
* See the License for the specific language governing permissions and |
18
|
|
|
* limitations under the License. |
19
|
|
|
*/ |
20
|
|
|
|
21
|
|
|
use Neomerx\Cors\Contracts\AnalysisResultInterface; |
22
|
|
|
use Neomerx\Cors\Contracts\AnalysisStrategyInterface; |
23
|
|
|
use Neomerx\Cors\Contracts\AnalyzerInterface; |
24
|
|
|
use Neomerx\Cors\Contracts\Constants\CorsRequestHeaders; |
25
|
|
|
use Neomerx\Cors\Contracts\Constants\CorsResponseHeaders; |
26
|
|
|
use Neomerx\Cors\Contracts\Constants\SimpleRequestHeaders; |
27
|
|
|
use Neomerx\Cors\Contracts\Constants\SimpleRequestMethods; |
28
|
|
|
use Neomerx\Cors\Contracts\Factory\FactoryInterface; |
29
|
|
|
use Neomerx\Cors\Log\LoggerAwareTrait; |
30
|
|
|
use Psr\Http\Message\RequestInterface; |
31
|
|
|
use Psr\Log\LoggerInterface; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @package Neomerx\Cors |
35
|
|
|
* |
36
|
|
|
* @SuppressWarnings(PHPMD.CouplingBetweenObjects) |
37
|
|
|
*/ |
38
|
|
|
class Analyzer implements AnalyzerInterface |
39
|
|
|
{ |
40
|
|
|
use LoggerAwareTrait { |
41
|
|
|
LoggerAwareTrait::setLogger as psrSetLogger; |
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
/** HTTP method for pre-flight request */ |
45
|
|
|
const PRE_FLIGHT_METHOD = 'OPTIONS'; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @var array |
49
|
|
|
*/ |
50
|
|
|
private const SIMPLE_METHODS = [ |
51
|
|
|
SimpleRequestMethods::GET => true, |
52
|
|
|
SimpleRequestMethods::HEAD => true, |
53
|
|
|
SimpleRequestMethods::POST => true, |
54
|
|
|
]; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @var string[] |
58
|
|
|
*/ |
59
|
|
|
private const SIMPLE_LC_HEADERS_EXCLUDING_CONTENT_TYPE = [ |
60
|
|
|
SimpleRequestHeaders::LC_ACCEPT, |
61
|
|
|
SimpleRequestHeaders::LC_ACCEPT_LANGUAGE, |
62
|
|
|
SimpleRequestHeaders::LC_CONTENT_LANGUAGE, |
63
|
|
|
]; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @var AnalysisStrategyInterface |
67
|
|
|
*/ |
68
|
|
|
private $strategy; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* @var FactoryInterface |
72
|
|
|
*/ |
73
|
|
|
private $factory; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @param AnalysisStrategyInterface $strategy |
77
|
|
|
* @param FactoryInterface $factory |
78
|
|
|
*/ |
79
|
16 |
|
public function __construct(AnalysisStrategyInterface $strategy, FactoryInterface $factory) |
80
|
|
|
{ |
81
|
16 |
|
$this->factory = $factory; |
82
|
16 |
|
$this->strategy = $strategy; |
83
|
16 |
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Create analyzer instance. |
87
|
|
|
* |
88
|
|
|
* @param AnalysisStrategyInterface $strategy |
89
|
|
|
* |
90
|
|
|
* @return AnalyzerInterface |
91
|
|
|
*/ |
92
|
16 |
|
public static function instance(AnalysisStrategyInterface $strategy): AnalyzerInterface |
93
|
|
|
{ |
94
|
16 |
|
return static::getFactory()->createAnalyzer($strategy); |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* @inheritdoc |
99
|
|
|
*/ |
100
|
1 |
|
public function setLogger(LoggerInterface $logger) |
101
|
|
|
{ |
102
|
1 |
|
$this->psrSetLogger($logger); |
103
|
1 |
|
$this->strategy->setLogger($logger); |
104
|
1 |
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* @inheritdoc |
108
|
|
|
* |
109
|
|
|
* @see http://www.w3.org/TR/cors/#resource-processing-model |
110
|
|
|
*/ |
111
|
14 |
|
public function analyze(RequestInterface $request): AnalysisResultInterface |
112
|
|
|
{ |
113
|
14 |
|
$this->logDebug('CORS analysis for request started.'); |
114
|
|
|
|
115
|
14 |
|
$result = $this->analyzeImplementation($request); |
116
|
|
|
|
117
|
14 |
|
$this->logDebug('CORS analysis for request completed.'); |
118
|
|
|
|
119
|
14 |
|
return $result; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* @param RequestInterface $request |
124
|
|
|
* |
125
|
|
|
* @return AnalysisResultInterface |
126
|
|
|
*/ |
127
|
14 |
|
protected function analyzeImplementation(RequestInterface $request): AnalysisResultInterface |
128
|
|
|
{ |
129
|
|
|
// check 'Host' request |
130
|
14 |
|
if ($this->strategy->isCheckHost() === true && $this->checkIsSameHost($request) === false) { |
131
|
1 |
|
return $this->createResult(AnalysisResultInterface::ERR_NO_HOST_HEADER); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
// Request handlers have common part (#6.1.1 - #6.1.2 and #6.2.1 - #6.2.2) |
135
|
|
|
|
136
|
|
|
// #6.1.1 and #6.2.1 |
137
|
13 |
|
if (empty($requestOrigin = $this->getOriginHeader($request)) === true) { |
138
|
1 |
|
$this->logInfo('Request is not CORS (request origin is empty).'); |
139
|
1 |
|
return $this->createResult(AnalysisResultInterface::TYPE_REQUEST_OUT_OF_CORS_SCOPE); |
140
|
|
|
} |
141
|
12 |
|
if ($this->checkIsCrossOrigin($requestOrigin) === false) { |
142
|
2 |
|
return $this->createResult(AnalysisResultInterface::TYPE_REQUEST_OUT_OF_CORS_SCOPE); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
// #6.1.2 and #6.2.2 |
146
|
10 |
|
if ($this->strategy->isRequestOriginAllowed($requestOrigin) === false) { |
147
|
3 |
|
$this->logInfo( |
148
|
3 |
|
'Request origin is not allowed. Check config settings for Allowed Origins.', |
149
|
3 |
|
['origin' => $requestOrigin] |
150
|
|
|
); |
151
|
3 |
|
return $this->createResult(AnalysisResultInterface::ERR_ORIGIN_NOT_ALLOWED); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
// Since this point handlers have their own path for |
155
|
|
|
// - simple CORS and actual CORS request (#6.1.3 - #6.1.4) |
156
|
|
|
// - pre-flight request (#6.2.3 - #6.2.10) |
157
|
|
|
|
158
|
7 |
|
if ($request->getMethod() === self::PRE_FLIGHT_METHOD) { |
159
|
5 |
|
return $this->analyzeAsPreFlight($request, $requestOrigin); |
160
|
|
|
} |
161
|
|
|
|
162
|
2 |
|
return $this->analyzeAsRequest($request, $requestOrigin); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Analyze request as simple CORS or/and actual CORS request (#6.1.3 - #6.1.4). |
167
|
|
|
* |
168
|
|
|
* @param RequestInterface $request |
169
|
|
|
* @param string $requestOrigin |
170
|
|
|
* |
171
|
|
|
* @return AnalysisResultInterface |
172
|
|
|
*/ |
173
|
2 |
|
protected function analyzeAsRequest(RequestInterface $request, string $requestOrigin): AnalysisResultInterface |
174
|
|
|
{ |
175
|
2 |
|
$this->logDebug('Request is identified as an actual CORS request.'); |
176
|
|
|
|
177
|
2 |
|
$headers = []; |
178
|
|
|
|
179
|
|
|
// #6.1.3 |
180
|
2 |
|
$headers[CorsResponseHeaders::ALLOW_ORIGIN] = $requestOrigin; |
181
|
2 |
|
if ($this->strategy->isRequestCredentialsSupported($request) === true) { |
182
|
1 |
|
$headers[CorsResponseHeaders::ALLOW_CREDENTIALS] = CorsResponseHeaders::VALUE_ALLOW_CREDENTIALS_TRUE; |
183
|
|
|
} |
184
|
|
|
// #6.4 |
185
|
2 |
|
$headers[CorsResponseHeaders::VARY] = CorsRequestHeaders::ORIGIN; |
186
|
|
|
|
187
|
|
|
// #6.1.4 |
188
|
2 |
|
$exposedHeaders = $this->strategy->getResponseExposedHeaders($request); |
189
|
2 |
|
if (empty($exposedHeaders) === false) { |
190
|
1 |
|
$headers[CorsResponseHeaders::EXPOSE_HEADERS] = $exposedHeaders; |
191
|
|
|
} |
192
|
|
|
|
193
|
2 |
|
return $this->createResult(AnalysisResultInterface::TYPE_ACTUAL_REQUEST, $headers); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Analyze request as CORS pre-flight request (#6.2.3 - #6.2.10). |
198
|
|
|
* |
199
|
|
|
* @param RequestInterface $request |
200
|
|
|
* @param string $requestOrigin |
201
|
|
|
* |
202
|
|
|
* @return AnalysisResultInterface |
203
|
|
|
* |
204
|
|
|
* @SuppressWarnings(PHPMD.NPathComplexity) |
205
|
|
|
* @SuppressWarnings(PHPMD.CyclomaticComplexity) |
206
|
|
|
*/ |
207
|
5 |
|
protected function analyzeAsPreFlight(RequestInterface $request, string $requestOrigin): AnalysisResultInterface |
208
|
|
|
{ |
209
|
|
|
// #6.2.3 |
210
|
5 |
|
$requestMethod = $request->getHeader(CorsRequestHeaders::METHOD); |
211
|
5 |
|
if (empty($requestMethod) === true) { |
212
|
1 |
|
$this->logDebug('Request is not CORS (header ' . CorsRequestHeaders::METHOD . ' is not specified).'); |
213
|
|
|
|
214
|
1 |
|
return $this->createResult(AnalysisResultInterface::TYPE_REQUEST_OUT_OF_CORS_SCOPE); |
215
|
|
|
} |
216
|
4 |
|
$requestMethod = reset($requestMethod); |
217
|
|
|
|
218
|
|
|
// OK now we are sure it's a pre-flight request |
219
|
4 |
|
$this->logDebug('Request is identified as a pre-flight CORS request.'); |
220
|
|
|
|
221
|
|
|
/** @var string $requestMethod */ |
222
|
|
|
|
223
|
|
|
// #6.2.4 |
224
|
4 |
|
$lcRequestHeaders = $this->getRequestedHeadersInLowerCase($request); |
225
|
|
|
|
226
|
|
|
// #6.2.5 |
227
|
4 |
|
if ($this->strategy->isRequestMethodSupported($requestMethod) === false) { |
|
|
|
|
228
|
1 |
|
$this->logInfo( |
229
|
1 |
|
'Request method is not supported. Check config settings for Allowed Methods.', |
230
|
1 |
|
['method' => $requestMethod] |
231
|
|
|
); |
232
|
1 |
|
return $this->createResult(AnalysisResultInterface::ERR_METHOD_NOT_SUPPORTED); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
// #6.2.6 |
236
|
3 |
|
if ($this->strategy->isRequestAllHeadersSupported($lcRequestHeaders) === false) { |
237
|
1 |
|
return $this->createResult(AnalysisResultInterface::ERR_HEADERS_NOT_SUPPORTED); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
// pre-flight response headers |
241
|
2 |
|
$headers = []; |
242
|
|
|
|
243
|
|
|
// #6.2.7 |
244
|
2 |
|
$headers[CorsResponseHeaders::ALLOW_ORIGIN] = $requestOrigin; |
245
|
2 |
|
if ($this->strategy->isRequestCredentialsSupported($request) === true) { |
246
|
2 |
|
$headers[CorsResponseHeaders::ALLOW_CREDENTIALS] = CorsResponseHeaders::VALUE_ALLOW_CREDENTIALS_TRUE; |
247
|
|
|
} |
248
|
|
|
// #6.4 |
249
|
2 |
|
$headers[CorsResponseHeaders::VARY] = CorsRequestHeaders::ORIGIN; |
250
|
|
|
|
251
|
|
|
// #6.2.8 |
252
|
2 |
|
if ($this->strategy->isPreFlightCanBeCached($request) === true) { |
253
|
2 |
|
$headers[CorsResponseHeaders::MAX_AGE] = $this->strategy->getPreFlightCacheMaxAge($request); |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
// #6.2.9 |
257
|
2 |
|
$isSimpleMethod = isset(static::SIMPLE_METHODS[$requestMethod]); |
258
|
2 |
|
if ($isSimpleMethod === false || $this->strategy->isForceAddAllowedMethodsToPreFlightResponse() === true) { |
259
|
2 |
|
$headers[CorsResponseHeaders::ALLOW_METHODS] = $this->strategy->getRequestAllowedMethods($request); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
// #6.2.10 |
263
|
|
|
// Has only 'simple' headers excluding Content-Type |
264
|
2 |
|
$isSimpleExclCT = empty(array_diff($lcRequestHeaders, static::SIMPLE_LC_HEADERS_EXCLUDING_CONTENT_TYPE)); |
265
|
2 |
|
if ($isSimpleExclCT === false || $this->strategy->isForceAddAllowedHeadersToPreFlightResponse() === true) { |
266
|
2 |
|
$headers[CorsResponseHeaders::ALLOW_HEADERS] = $this->strategy->getRequestAllowedHeaders($request); |
267
|
|
|
} |
268
|
|
|
|
269
|
2 |
|
return $this->createResult(AnalysisResultInterface::TYPE_PRE_FLIGHT_REQUEST, $headers); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* @param RequestInterface $request |
274
|
|
|
* |
275
|
|
|
* @return string[] |
276
|
|
|
*/ |
277
|
4 |
|
protected function getRequestedHeadersInLowerCase(RequestInterface $request): array |
278
|
|
|
{ |
279
|
4 |
|
$requestHeaders = []; |
280
|
|
|
|
281
|
4 |
|
foreach ($request->getHeader(CorsRequestHeaders::HEADERS) as $headersList) { |
282
|
3 |
|
$headersList = strtolower($headersList); |
283
|
3 |
|
foreach (explode(CorsRequestHeaders::HEADERS_SEPARATOR, $headersList) as $header) { |
284
|
|
|
// after explode header names might have spaces in the beginnings and ends so trim them |
285
|
3 |
|
$header = trim($header); |
286
|
3 |
|
if (empty($header) === false) { |
287
|
3 |
|
$requestHeaders[] = $header; |
288
|
|
|
} |
289
|
|
|
} |
290
|
|
|
} |
291
|
|
|
|
292
|
4 |
|
return $requestHeaders; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* @param RequestInterface $request |
297
|
|
|
* |
298
|
|
|
* @return string |
299
|
|
|
*/ |
300
|
13 |
|
protected function getOriginHeader(RequestInterface $request): string |
301
|
|
|
{ |
302
|
13 |
|
if ($request->hasHeader(CorsRequestHeaders::ORIGIN) === true) { |
303
|
13 |
|
$header = $request->getHeader(CorsRequestHeaders::ORIGIN); |
304
|
13 |
|
if (empty($header) === false) { |
305
|
12 |
|
return reset($header); |
306
|
|
|
} |
307
|
|
|
} |
308
|
|
|
|
309
|
1 |
|
return ''; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* @param RequestInterface $request |
314
|
|
|
* |
315
|
|
|
* @return bool |
316
|
|
|
*/ |
317
|
14 |
|
protected function checkIsSameHost(RequestInterface $request): bool |
318
|
|
|
{ |
319
|
14 |
|
$serverOriginHost = $this->strategy->getServerOriginHost(); |
320
|
14 |
|
$serverOriginPort = $this->strategy->getServerOriginPort(); |
321
|
|
|
|
322
|
14 |
|
$host = $this->getRequestHostHeader($request); |
323
|
|
|
|
324
|
|
|
// parse `Host` header |
325
|
|
|
// |
326
|
|
|
// According to https://tools.ietf.org/html/rfc7230#section-5.4 `Host` header could be |
327
|
|
|
// |
328
|
|
|
// "uri-host" OR "uri-host:port" |
329
|
|
|
// |
330
|
|
|
// `parse_url` function thinks the first value is `path` and the second is `host` with `port` |
331
|
|
|
// which is a bit annoying so... |
332
|
14 |
|
$portOrNull = parse_url($host, PHP_URL_PORT); |
333
|
14 |
|
$hostUrl = $portOrNull === null ? $host : parse_url($host, PHP_URL_HOST); |
334
|
|
|
|
335
|
|
|
// Neither MDN, nor RFC tell anything definitive about Host header comparison. |
336
|
|
|
// Browsers such as Firefox and Chrome do not add the optional port for |
337
|
|
|
// HTTP (80) and HTTPS (443). |
338
|
|
|
// So we require port match only if it specified in settings. |
339
|
|
|
|
340
|
14 |
|
$isHostUrlMatch = strcasecmp($serverOriginHost, $hostUrl) === 0; |
341
|
|
|
$isSameHost = |
342
|
14 |
|
$isHostUrlMatch === true && |
343
|
14 |
|
($serverOriginPort === null || $serverOriginPort === $portOrNull); |
344
|
|
|
|
345
|
14 |
|
if ($isSameHost === false) { |
346
|
1 |
|
$this->logInfo( |
347
|
|
|
'Host header in request either absent or do not match server origin. ' . |
348
|
1 |
|
'Check config settings for Server Origin and Host Check.', |
349
|
1 |
|
['host' => $host, 'server_origin_host' => $serverOriginHost, 'server_origin_port' => $serverOriginPort] |
350
|
|
|
); |
351
|
|
|
} |
352
|
|
|
|
353
|
14 |
|
return $isSameHost; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
/** |
357
|
|
|
* @param string $requestOrigin |
358
|
|
|
* |
359
|
|
|
* @return bool |
360
|
|
|
* |
361
|
|
|
* @see http://tools.ietf.org/html/rfc6454#section-5 |
362
|
|
|
*/ |
363
|
12 |
|
protected function checkIsCrossOrigin(string $requestOrigin): bool |
364
|
|
|
{ |
365
|
12 |
|
$parsedUrl = parse_url($requestOrigin); |
366
|
12 |
|
if ($parsedUrl === false) { |
367
|
1 |
|
$this->logWarning('Request origin header URL cannot be parsed.', ['url' => $requestOrigin]); |
368
|
|
|
|
369
|
1 |
|
return false; |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
// check `host` parts |
373
|
11 |
|
$requestOriginHost = $parsedUrl['host'] ?? ''; |
374
|
11 |
|
$serverOriginHost = $this->strategy->getServerOriginHost(); |
375
|
11 |
|
if (strcasecmp($requestOriginHost, $serverOriginHost) !== 0) { |
376
|
8 |
|
return true; |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
// check `port` parts |
380
|
3 |
|
$requestOriginPort = array_key_exists('port', $parsedUrl) === true ? (int)$parsedUrl['port'] : null; |
381
|
3 |
|
$serverOriginPort = $this->strategy->getServerOriginPort(); |
382
|
3 |
|
if ($requestOriginPort !== $serverOriginPort) { |
383
|
1 |
|
return true; |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
// check `scheme` parts |
387
|
2 |
|
$requestOriginScheme = $parsedUrl['scheme'] ?? ''; |
388
|
2 |
|
$serverOriginScheme = $this->strategy->getServerOriginScheme(); |
389
|
2 |
|
if (strcasecmp($requestOriginScheme, $serverOriginScheme) !== 0) { |
390
|
1 |
|
return true; |
391
|
|
|
} |
392
|
|
|
|
393
|
1 |
|
$this->logInfo( |
394
|
1 |
|
'Request is not CORS (request origin equals to server one).', |
395
|
|
|
[ |
396
|
1 |
|
'request_origin' => $requestOrigin, |
397
|
1 |
|
'server_origin_scheme' => $serverOriginScheme, |
398
|
1 |
|
'server_origin_host' => $serverOriginHost, |
399
|
1 |
|
'server_origin_port' => $serverOriginPort |
400
|
|
|
] |
401
|
|
|
); |
402
|
|
|
|
403
|
1 |
|
return false; |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
/** |
407
|
|
|
* @param int $type |
408
|
|
|
* @param array $headers |
409
|
|
|
* |
410
|
|
|
* @return AnalysisResultInterface |
411
|
|
|
*/ |
412
|
14 |
|
protected function createResult($type, array $headers = []): AnalysisResultInterface |
413
|
|
|
{ |
414
|
14 |
|
return $this->factory->createAnalysisResult($type, $headers); |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
/** |
418
|
|
|
* @return FactoryInterface |
419
|
|
|
*/ |
420
|
16 |
|
protected static function getFactory(): FactoryInterface |
421
|
|
|
{ |
422
|
|
|
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ |
423
|
16 |
|
return new \Neomerx\Cors\Factory\Factory(); |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
/** |
427
|
|
|
* @param RequestInterface $request |
428
|
|
|
* |
429
|
|
|
* @return null|string |
|
|
|
|
430
|
|
|
*/ |
431
|
14 |
|
private function getRequestHostHeader(RequestInterface $request): ?string |
432
|
|
|
{ |
433
|
14 |
|
$hostHeaderValue = $request->getHeader(CorsRequestHeaders::HOST); |
434
|
14 |
|
$host = empty($hostHeaderValue) === true ? null : reset($hostHeaderValue); |
435
|
|
|
|
436
|
14 |
|
return $host; |
437
|
|
|
} |
438
|
|
|
} |
439
|
|
|
|
This check looks for type mismatches where the missing type is
false
. This is usually indicative of an error condtion.Consider the follow example
This function either returns a new
DateTime
object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returnedfalse
before passing on the value to another function or method that may not be able to handle afalse
.