PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.11
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.11
1.10.25 1.10.24 1.10.23 1.10.22 1.10.21 1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 All 169 releases
woocommerce-pos / vendor_prefixed / sentry / sentry / src / ErrorHandler.php

ErrorHandler.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.11, at vendor_prefixed/sentry/sentry/src/ErrorHandler.php

460 lines 19.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare (strict_types=1);
4 namespace WCPOS\Vendor\Sentry;
5
6 use WCPOS\Vendor\Sentry\Exception\FatalErrorException;
7 use WCPOS\Vendor\Sentry\Exception\SilencedErrorException;
8 /**
9 * This class implements a simple error handler that catches all configured
10 * error types and relays them to all configured listeners. Registering this
11 * error handler more than once is not supported and will lead to nasty
12 * problems. The code is based on the Symfony ErrorHandler component.
13 *
14 * @phpstan-import-type StacktraceFrame from FrameBuilder
15 */
16 final class ErrorHandler
17 {
18 /**
19 * The default amount of bytes of memory to reserve for the fatal error handler.
20 *
21 * @internal
22 */
23 public const DEFAULT_RESERVED_MEMORY_SIZE = 16 * 1024;
24 // 16 KiB
25 /**
26 * The regular expression used to match the message of an out of memory error.
27 *
28 * Regex inspired by https://github.com/php/php-src/blob/524b13460752fba908f88e3c4428b91fa66c083a/Zend/tests/new_oom.phpt#L15
29 */
30 private const OOM_MESSAGE_MATCHER = '/^Allowed memory size of (?<memory_limit>\\d+) bytes exhausted[^\\r\\n]* \\(tried to allocate \\d+ bytes\\)/';
31 /**
32 * The fatal error types that cannot be silenced using the @ operator in PHP 8+.
33 */
34 private const PHP8_UNSILENCEABLE_FATAL_ERRORS = \E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_COMPILE_ERROR | \E_USER_ERROR | \E_RECOVERABLE_ERROR;
35 /**
36 * @var self|null The current registered handler (this class is a singleton)
37 */
38 private static $handlerInstance;
39 /**
40 * @var callable[] List of listeners that will act on each captured error
41 *
42 * @phpstan-var (callable(\ErrorException): void)[]
43 */
44 private $errorListeners = [];
45 /**
46 * @var callable[] List of listeners that will act of each captured fatal error
47 *
48 * @phpstan-var (callable(FatalErrorException): void)[]
49 */
50 private $fatalErrorListeners = [];
51 /**
52 * @var callable[] List of listeners that will act on each captured exception
53 *
54 * @phpstan-var (callable(\Throwable): void)[]
55 */
56 private $exceptionListeners = [];
57 /**
58 * @var \ReflectionProperty A reflection cached instance that points to the
59 * trace property of the exception objects
60 */
61 private $exceptionReflection;
62 /**
63 * @var callable|null The previous error handler, if any
64 */
65 private $previousErrorHandler;
66 /**
67 * @var callable|null The previous exception handler, if any
68 *
69 * @phpstan-var (callable(\Throwable): void)|null
70 */
71 private $previousExceptionHandler;
72 /**
73 * @var bool Whether the error handler has been registered
74 */
75 private $isErrorHandlerRegistered = \false;
76 /**
77 * @var bool Whether the exception handler has been registered
78 */
79 private $isExceptionHandlerRegistered = \false;
80 /**
81 * @var bool Whether the fatal error handler has been registered
82 */
83 private $isFatalErrorHandlerRegistered = \false;
84 /**
85 * @var int|null the amount of bytes of memory to increase the memory limit by when we are capturing a out of memory error, set to null to not increase the memory limit
86 */
87 private $memoryLimitIncreaseOnOutOfMemoryErrorValue = 5 * 1024 * 1024;
88 // 5 MiB
89 /**
90 * @var Options|null The SDK options
91 */
92 private $options;
93 /**
94 * @var bool Whether the memory limit has been increased
95 */
96 private static $didIncreaseMemoryLimit = \false;
97 /**
98 * @var string|null A portion of pre-allocated memory data that will be reclaimed in case a fatal error occurs to handle it
99 *
100 * @phpstan-ignore-next-line This property is used to reserve memory for the fatal error handler and is thus never read
101 */
102 private static $reservedMemory;
103 /**
104 * @var int The amount of memory to reserve for the fatal error handler
105 */
106 private static $reservedMemorySize = self::DEFAULT_RESERVED_MEMORY_SIZE;
107 /**
108 * @var bool Whether the fatal error handler should be disabled
109 */
110 private static $disableFatalErrorHandler = \false;
111 /**
112 * @var string[] List of error levels and their description
113 */
114 private const ERROR_LEVELS_DESCRIPTION = [
115 \E_DEPRECATED => 'Deprecated',
116 \E_USER_DEPRECATED => 'User Deprecated',
117 \E_NOTICE => 'Notice',
118 \E_USER_NOTICE => 'User Notice',
119 // This is \E_STRICT which has been deprecated in PHP 8.4 so we should not reference it directly to prevent deprecation notices
120 2048 => 'Runtime Notice',
121 \E_WARNING => 'Warning',
122 \E_USER_WARNING => 'User Warning',
123 \E_COMPILE_WARNING => 'Compile Warning',
124 \E_CORE_WARNING => 'Core Warning',
125 \E_USER_ERROR => 'User Error',
126 \E_RECOVERABLE_ERROR => 'Catchable Fatal Error',
127 \E_COMPILE_ERROR => 'Compile Error',
128 \E_PARSE => 'Parse Error',
129 \E_ERROR => 'Error',
130 \E_CORE_ERROR => 'Core Error',
131 ];
132 /**
133 * Constructor.
134 *
135 * @throws \ReflectionException If hooking into the \Exception class to
136 * make the `trace` property accessible fails
137 */
138 private function __construct()
139 {
140 $this->exceptionReflection = new \ReflectionProperty(\Exception::class, 'trace');
141 if (\PHP_VERSION_ID < 80100) {
142 $this->exceptionReflection->setAccessible(\true);
143 }
144 }
145 /**
146 * Registers the error handler once and returns its instance.
147 */
148 public static function registerOnceErrorHandler(?Options $options = null) : self
149 {
150 if (self::$handlerInstance === null) {
151 self::$handlerInstance = new self();
152 }
153 self::$handlerInstance->options = $options;
154 if (self::$handlerInstance->isErrorHandlerRegistered) {
155 return self::$handlerInstance;
156 }
157 $errorHandlerCallback = \Closure::fromCallable([self::$handlerInstance, 'handleError']);
158 self::$handlerInstance->isErrorHandlerRegistered = \true;
159 self::$handlerInstance->previousErrorHandler = \set_error_handler($errorHandlerCallback);
160 if (self::$handlerInstance->previousErrorHandler === null) {
161 \restore_error_handler();
162 // Specifying the error types caught by the error handler with the
163 // first call to the set_error_handler method would cause the PHP
164 // bug https://bugs.php.net/63206 if the handler is not the first
165 // one in the chain of handlers
166 \set_error_handler($errorHandlerCallback, \E_ALL);
167 }
168 return self::$handlerInstance;
169 }
170 /**
171 * Registers the fatal error handler and reserves a certain amount of memory
172 * that will be reclaimed to handle the errors (to prevent out of memory
173 * issues while handling them) and returns its instance.
174 *
175 * @param int $reservedMemorySize The amount of memory to reserve for the fatal
176 * error handler expressed in bytes
177 */
178 public static function registerOnceFatalErrorHandler(int $reservedMemorySize = self::DEFAULT_RESERVED_MEMORY_SIZE) : self
179 {
180 if ($reservedMemorySize <= 0) {
181 throw new \InvalidArgumentException('The $reservedMemorySize argument must be greater than 0.');
182 }
183 if (self::$handlerInstance === null) {
184 self::$handlerInstance = new self();
185 }
186 if (self::$handlerInstance->isFatalErrorHandlerRegistered) {
187 return self::$handlerInstance;
188 }
189 self::$handlerInstance->isFatalErrorHandlerRegistered = \true;
190 self::$reservedMemorySize = $reservedMemorySize;
191 self::$reservedMemory = \str_repeat('x', $reservedMemorySize);
192 \register_shutdown_function(\Closure::fromCallable([self::$handlerInstance, 'handleFatalError']));
193 return self::$handlerInstance;
194 }
195 /**
196 * Registers the exception handler, effectively replacing the current one
197 * and returns its instance. The previous one will be saved anyway and
198 * called when appropriate.
199 */
200 public static function registerOnceExceptionHandler() : self
201 {
202 if (self::$handlerInstance === null) {
203 self::$handlerInstance = new self();
204 }
205 if (self::$handlerInstance->isExceptionHandlerRegistered) {
206 return self::$handlerInstance;
207 }
208 self::$handlerInstance->isExceptionHandlerRegistered = \true;
209 self::$handlerInstance->previousExceptionHandler = \set_exception_handler(\Closure::fromCallable([self::$handlerInstance, 'handleException']));
210 return self::$handlerInstance;
211 }
212 /**
213 * Adds a listener to the current error handler that will be called every
214 * time an error is captured.
215 *
216 * @param callable $listener A callable that will act as a listener
217 * and that must accept a single argument
218 * of type \ErrorException
219 *
220 * @phpstan-param callable(\ErrorException): void $listener
221 */
222 public function addErrorHandlerListener(callable $listener) : void
223 {
224 $this->errorListeners[] = $listener;
225 }
226 /**
227 * Adds a listener to the current error handler that will be called every
228 * time a fatal error handler is captured.
229 *
230 * @param callable $listener A callable that will act as a listener
231 * and that must accept a single argument
232 * of type \Sentry\Exception\FatalErrorException
233 *
234 * @phpstan-param callable(FatalErrorException): void $listener
235 */
236 public function addFatalErrorHandlerListener(callable $listener) : void
237 {
238 $this->fatalErrorListeners[] = $listener;
239 }
240 /**
241 * Adds a listener to the current error handler that will be called every
242 * time an exception is captured.
243 *
244 * @param callable $listener A callable that will act as a listener
245 * and that must accept a single argument
246 * of type \Throwable
247 *
248 * @phpstan-param callable(\Throwable): void $listener
249 */
250 public function addExceptionHandlerListener(callable $listener) : void
251 {
252 $this->exceptionListeners[] = $listener;
253 }
254 /**
255 * Sets the amount of memory to increase the memory limit by when we are capturing a out of memory error.
256 *
257 * @param int|null $valueInBytes the number of bytes to increase the memory limit by, or null to not increase the memory limit
258 */
259 public function setMemoryLimitIncreaseOnOutOfMemoryErrorInBytes(?int $valueInBytes) : void
260 {
261 if ($valueInBytes !== null && $valueInBytes <= 0) {
262 throw new \InvalidArgumentException('The $valueInBytes argument must be greater than 0 or null.');
263 }
264 $this->memoryLimitIncreaseOnOutOfMemoryErrorValue = $valueInBytes;
265 }
266 /**
267 * @internal
268 */
269 public static function resetFatalErrorHandlerState() : void
270 {
271 self::$disableFatalErrorHandler = \false;
272 self::$didIncreaseMemoryLimit = \false;
273 if (self::$handlerInstance !== null && self::$handlerInstance->isFatalErrorHandlerRegistered) {
274 self::$reservedMemory = \str_repeat('x', self::$reservedMemorySize);
275 }
276 }
277 /**
278 * Handles errors by capturing them through the client according to the
279 * configured bit field.
280 *
281 * @param int $level The level of the error raised, represented by
282 * one of the E_* constants
283 * @param string $message The error message
284 * @param string $file The filename the error was raised in
285 * @param int $line The line number the error was raised at
286 * @param array<string, mixed>|null $errcontext The error context (deprecated since PHP 7.2)
287 *
288 * @return bool If the function returns `false` then the PHP native error
289 * handler will be called
290 *
291 * @throws \Throwable
292 */
293 private function handleError(int $level, string $message, string $file, int $line, ?array $errcontext = []) : bool
294 {
295 $isSilencedError = \error_reporting() === 0;
296 if (\PHP_MAJOR_VERSION >= 8) {
297 // Starting from PHP8, when a silenced error occurs the `error_reporting()`
298 // function will return a bitmask of fatal errors that are unsilenceable.
299 // If by subtracting from this value those errors the result is 0, we can
300 // conclude that the error was silenced.
301 $isSilencedError = 0 === (\error_reporting() & ~self::PHP8_UNSILENCEABLE_FATAL_ERRORS);
302 // However, starting from PHP8 some fatal errors are unsilenceable,
303 // so we have to check for them to avoid reporting any of them as
304 // silenced instead
305 if ($level === (self::PHP8_UNSILENCEABLE_FATAL_ERRORS & $level)) {
306 $isSilencedError = \false;
307 }
308 }
309 if ($this->shouldHandleError($level, $isSilencedError)) {
310 if ($isSilencedError) {
311 $errorAsException = new SilencedErrorException(self::ERROR_LEVELS_DESCRIPTION[$level] . ': ' . $message, 0, $level, $file, $line);
312 } else {
313 $errorAsException = new \ErrorException(self::ERROR_LEVELS_DESCRIPTION[$level] . ': ' . $message, 0, $level, $file, $line);
314 }
315 $backtrace = $this->cleanBacktraceFromErrorHandlerFrames($errorAsException->getTrace(), $errorAsException->getFile(), $errorAsException->getLine());
316 $this->exceptionReflection->setValue($errorAsException, $backtrace);
317 $this->invokeListeners($this->errorListeners, $errorAsException);
318 }
319 if ($this->previousErrorHandler !== null) {
320 return \false !== ($this->previousErrorHandler)($level, $message, $file, $line, $errcontext);
321 }
322 return \false;
323 }
324 private function shouldHandleError(int $level, bool $silenced) : bool
325 {
326 // If we were not given any options, we should handle all errors
327 if ($this->options === null) {
328 return \true;
329 }
330 if ($silenced) {
331 return $this->options->shouldCaptureSilencedErrors();
332 }
333 return ($this->options->getErrorTypes() & $level) !== 0;
334 }
335 /**
336 * Tries to handle a fatal error if any and relay them to the listeners.
337 * It only tries to do this if we still have some reserved memory at
338 * disposal. This method is used as callback of a shutdown function.
339 */
340 private function handleFatalError() : void
341 {
342 if (self::$disableFatalErrorHandler) {
343 return;
344 }
345 // Free the reserved memory that allows us to potentially handle OOM errors
346 self::$reservedMemory = null;
347 $error = \error_get_last();
348 if (!empty($error) && $error['type'] & (\E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_CORE_WARNING | \E_COMPILE_ERROR | \E_COMPILE_WARNING)) {
349 // If we did not do so already and we are allowed to increase the memory limit, we do so when we detect an OOM error
350 if (self::$didIncreaseMemoryLimit === \false && $this->memoryLimitIncreaseOnOutOfMemoryErrorValue !== null && \preg_match(self::OOM_MESSAGE_MATCHER, $error['message'], $matches) === 1) {
351 $currentMemoryLimit = (int) $matches['memory_limit'];
352 $newMemoryLimit = $currentMemoryLimit + $this->memoryLimitIncreaseOnOutOfMemoryErrorValue;
353 // It can happen that the memory limit + increase is still lower than
354 // the memory that is currently being used. This produces warnings
355 // that may end up in Sentry. To prevent this, we can check the real
356 // usage before.
357 if ($newMemoryLimit > \memory_get_usage(\true)) {
358 $this->setMemoryLimitWithoutHandlingWarnings($newMemoryLimit);
359 }
360 self::$didIncreaseMemoryLimit = \true;
361 }
362 $errorAsException = new FatalErrorException(self::ERROR_LEVELS_DESCRIPTION[$error['type']] . ': ' . $error['message'], 0, $error['type'], $error['file'], $error['line']);
363 $this->exceptionReflection->setValue($errorAsException, []);
364 $this->invokeListeners($this->fatalErrorListeners, $errorAsException);
365 }
366 }
367 /**
368 * Handles the given exception by passing it to all the listeners,
369 * then forwarding it to another handler.
370 *
371 * @param \Throwable $exception The exception to handle
372 *
373 * @throws \Throwable
374 */
375 private function handleException(\Throwable $exception) : void
376 {
377 $this->invokeListeners($this->exceptionListeners, $exception);
378 $previousExceptionHandlerException = $exception;
379 // Unset the previous exception handler to prevent infinite loop in case
380 // we need to handle an exception thrown from it
381 $previousExceptionHandler = $this->previousExceptionHandler;
382 $this->previousExceptionHandler = null;
383 try {
384 if ($previousExceptionHandler !== null) {
385 $previousExceptionHandler($exception);
386 return;
387 }
388 } catch (\Throwable $previousExceptionHandlerException) {
389 // This `catch` statement is here to forcefully override the
390 // $previousExceptionHandlerException variable with the exception
391 // we just caught
392 }
393 // If the instance of the exception we're handling is the same as the one
394 // caught from the previous exception handler then we give it back to the
395 // native PHP handler to prevent an infinite loop
396 if ($exception === $previousExceptionHandlerException) {
397 // Disable the fatal error handler or the error will be reported twice
398 self::$disableFatalErrorHandler = \true;
399 throw $exception;
400 }
401 $this->handleException($previousExceptionHandlerException);
402 }
403 /**
404 * Set the memory_limit while having no real error handler so that a warning emitted
405 * will not get reported.
406 */
407 private function setMemoryLimitWithoutHandlingWarnings(int $memoryLimit) : void
408 {
409 \set_error_handler(static function () : bool {
410 return \true;
411 }, \E_WARNING);
412 try {
413 \ini_set('memory_limit', (string) $memoryLimit);
414 } finally {
415 \restore_error_handler();
416 }
417 }
418 /**
419 * Cleans and returns the backtrace without the first frames that belong to
420 * this error handler.
421 *
422 * @param array<int, array<string, mixed>> $backtrace The backtrace to clear
423 * @param string $file The filename the backtrace was raised in
424 * @param int $line The line number the backtrace was raised at
425 *
426 * @phpstan-param list<StacktraceFrame> $backtrace
427 *
428 * @return array<int, mixed>
429 */
430 private function cleanBacktraceFromErrorHandlerFrames(array $backtrace, string $file, int $line) : array
431 {
432 $cleanedBacktrace = $backtrace;
433 $index = 0;
434 while ($index < \count($backtrace)) {
435 if (isset($backtrace[$index]['file'], $backtrace[$index]['line']) && $backtrace[$index]['line'] === $line && $backtrace[$index]['file'] === $file) {
436 $cleanedBacktrace = \array_slice($cleanedBacktrace, 1 + $index);
437 break;
438 }
439 ++$index;
440 }
441 return $cleanedBacktrace;
442 }
443 /**
444 * Invokes all the listeners and pass the exception to all of them.
445 *
446 * @param callable[] $listeners The array of listeners to be called
447 * @param \Throwable $throwable The exception to be passed onto listeners
448 */
449 private function invokeListeners(array $listeners, \Throwable $throwable) : void
450 {
451 foreach ($listeners as $listener) {
452 try {
453 $listener($throwable);
454 } catch (\Throwable $exception) {
455 // Do nothing as this should be as transparent as possible
456 }
457 }
458 }
459 }
460