PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.22
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.22
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 1.10.1 All 168 releases
woocommerce-pos / vendor_prefixed / sentry / sentry / src / ErrorHandler.php

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

458 lines 19.8 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 private static $reservedMemory;
101 /**
102 * @var int The amount of memory to reserve for the fatal error handler
103 */
104 private static $reservedMemorySize = self::DEFAULT_RESERVED_MEMORY_SIZE;
105 /**
106 * @var bool Whether the fatal error handler should be disabled
107 */
108 private static $disableFatalErrorHandler = \false;
109 /**
110 * @var string[] List of error levels and their description
111 */
112 private const ERROR_LEVELS_DESCRIPTION = [
113 \E_DEPRECATED => 'Deprecated',
114 \E_USER_DEPRECATED => 'User Deprecated',
115 \E_NOTICE => 'Notice',
116 \E_USER_NOTICE => 'User Notice',
117 // This is \E_STRICT which has been deprecated in PHP 8.4 so we should not reference it directly to prevent deprecation notices
118 2048 => 'Runtime Notice',
119 \E_WARNING => 'Warning',
120 \E_USER_WARNING => 'User Warning',
121 \E_COMPILE_WARNING => 'Compile Warning',
122 \E_CORE_WARNING => 'Core Warning',
123 \E_USER_ERROR => 'User Error',
124 \E_RECOVERABLE_ERROR => 'Catchable Fatal Error',
125 \E_COMPILE_ERROR => 'Compile Error',
126 \E_PARSE => 'Parse Error',
127 \E_ERROR => 'Error',
128 \E_CORE_ERROR => 'Core Error',
129 ];
130 /**
131 * Constructor.
132 *
133 * @throws \ReflectionException If hooking into the \Exception class to
134 * make the `trace` property accessible fails
135 */
136 private function __construct()
137 {
138 $this->exceptionReflection = new \ReflectionProperty(\Exception::class, 'trace');
139 if (\PHP_VERSION_ID < 80100) {
140 $this->exceptionReflection->setAccessible(\true);
141 }
142 }
143 /**
144 * Registers the error handler once and returns its instance.
145 */
146 public static function registerOnceErrorHandler(?Options $options = null) : self
147 {
148 if (self::$handlerInstance === null) {
149 self::$handlerInstance = new self();
150 }
151 self::$handlerInstance->options = $options;
152 if (self::$handlerInstance->isErrorHandlerRegistered) {
153 return self::$handlerInstance;
154 }
155 $errorHandlerCallback = \Closure::fromCallable([self::$handlerInstance, 'handleError']);
156 self::$handlerInstance->isErrorHandlerRegistered = \true;
157 self::$handlerInstance->previousErrorHandler = \set_error_handler($errorHandlerCallback);
158 if (self::$handlerInstance->previousErrorHandler === null) {
159 \restore_error_handler();
160 // Specifying the error types caught by the error handler with the
161 // first call to the set_error_handler method would cause the PHP
162 // bug https://bugs.php.net/63206 if the handler is not the first
163 // one in the chain of handlers
164 \set_error_handler($errorHandlerCallback, \E_ALL);
165 }
166 return self::$handlerInstance;
167 }
168 /**
169 * Registers the fatal error handler and reserves a certain amount of memory
170 * that will be reclaimed to handle the errors (to prevent out of memory
171 * issues while handling them) and returns its instance.
172 *
173 * @param int $reservedMemorySize The amount of memory to reserve for the fatal
174 * error handler expressed in bytes
175 */
176 public static function registerOnceFatalErrorHandler(int $reservedMemorySize = self::DEFAULT_RESERVED_MEMORY_SIZE) : self
177 {
178 if ($reservedMemorySize <= 0) {
179 throw new \InvalidArgumentException('The $reservedMemorySize argument must be greater than 0.');
180 }
181 if (self::$handlerInstance === null) {
182 self::$handlerInstance = new self();
183 }
184 if (self::$handlerInstance->isFatalErrorHandlerRegistered) {
185 return self::$handlerInstance;
186 }
187 self::$handlerInstance->isFatalErrorHandlerRegistered = \true;
188 self::$reservedMemorySize = $reservedMemorySize;
189 self::$reservedMemory = \str_repeat('x', $reservedMemorySize);
190 \register_shutdown_function(\Closure::fromCallable([self::$handlerInstance, 'handleFatalError']));
191 return self::$handlerInstance;
192 }
193 /**
194 * Registers the exception handler, effectively replacing the current one
195 * and returns its instance. The previous one will be saved anyway and
196 * called when appropriate.
197 */
198 public static function registerOnceExceptionHandler() : self
199 {
200 if (self::$handlerInstance === null) {
201 self::$handlerInstance = new self();
202 }
203 if (self::$handlerInstance->isExceptionHandlerRegistered) {
204 return self::$handlerInstance;
205 }
206 self::$handlerInstance->isExceptionHandlerRegistered = \true;
207 self::$handlerInstance->previousExceptionHandler = \set_exception_handler(\Closure::fromCallable([self::$handlerInstance, 'handleException']));
208 return self::$handlerInstance;
209 }
210 /**
211 * Adds a listener to the current error handler that will be called every
212 * time an error is captured.
213 *
214 * @param callable $listener A callable that will act as a listener
215 * and that must accept a single argument
216 * of type \ErrorException
217 *
218 * @phpstan-param callable(\ErrorException): void $listener
219 */
220 public function addErrorHandlerListener(callable $listener) : void
221 {
222 $this->errorListeners[] = $listener;
223 }
224 /**
225 * Adds a listener to the current error handler that will be called every
226 * time a fatal error handler is captured.
227 *
228 * @param callable $listener A callable that will act as a listener
229 * and that must accept a single argument
230 * of type \Sentry\Exception\FatalErrorException
231 *
232 * @phpstan-param callable(FatalErrorException): void $listener
233 */
234 public function addFatalErrorHandlerListener(callable $listener) : void
235 {
236 $this->fatalErrorListeners[] = $listener;
237 }
238 /**
239 * Adds a listener to the current error handler that will be called every
240 * time an exception is captured.
241 *
242 * @param callable $listener A callable that will act as a listener
243 * and that must accept a single argument
244 * of type \Throwable
245 *
246 * @phpstan-param callable(\Throwable): void $listener
247 */
248 public function addExceptionHandlerListener(callable $listener) : void
249 {
250 $this->exceptionListeners[] = $listener;
251 }
252 /**
253 * Sets the amount of memory to increase the memory limit by when we are capturing a out of memory error.
254 *
255 * @param int|null $valueInBytes the number of bytes to increase the memory limit by, or null to not increase the memory limit
256 */
257 public function setMemoryLimitIncreaseOnOutOfMemoryErrorInBytes(?int $valueInBytes) : void
258 {
259 if ($valueInBytes !== null && $valueInBytes <= 0) {
260 throw new \InvalidArgumentException('The $valueInBytes argument must be greater than 0 or null.');
261 }
262 $this->memoryLimitIncreaseOnOutOfMemoryErrorValue = $valueInBytes;
263 }
264 /**
265 * @internal
266 */
267 public static function resetFatalErrorHandlerState() : void
268 {
269 self::$disableFatalErrorHandler = \false;
270 self::$didIncreaseMemoryLimit = \false;
271 if (self::$handlerInstance !== null && self::$handlerInstance->isFatalErrorHandlerRegistered && self::$reservedMemory === null) {
272 self::$reservedMemory = \str_repeat('x', self::$reservedMemorySize);
273 }
274 }
275 /**
276 * Handles errors by capturing them through the client according to the
277 * configured bit field.
278 *
279 * @param int $level The level of the error raised, represented by
280 * one of the E_* constants
281 * @param string $message The error message
282 * @param string $file The filename the error was raised in
283 * @param int $line The line number the error was raised at
284 * @param array<string, mixed>|null $errcontext The error context (deprecated since PHP 7.2)
285 *
286 * @return bool If the function returns `false` then the PHP native error
287 * handler will be called
288 *
289 * @throws \Throwable
290 */
291 private function handleError(int $level, string $message, string $file, int $line, ?array $errcontext = []) : bool
292 {
293 $isSilencedError = \error_reporting() === 0;
294 if (\PHP_MAJOR_VERSION >= 8) {
295 // Starting from PHP8, when a silenced error occurs the `error_reporting()`
296 // function will return a bitmask of fatal errors that are unsilenceable.
297 // If by subtracting from this value those errors the result is 0, we can
298 // conclude that the error was silenced.
299 $isSilencedError = 0 === (\error_reporting() & ~self::PHP8_UNSILENCEABLE_FATAL_ERRORS);
300 // However, starting from PHP8 some fatal errors are unsilenceable,
301 // so we have to check for them to avoid reporting any of them as
302 // silenced instead
303 if ($level === (self::PHP8_UNSILENCEABLE_FATAL_ERRORS & $level)) {
304 $isSilencedError = \false;
305 }
306 }
307 if ($this->shouldHandleError($level, $isSilencedError)) {
308 if ($isSilencedError) {
309 $errorAsException = new SilencedErrorException(self::ERROR_LEVELS_DESCRIPTION[$level] . ': ' . $message, 0, $level, $file, $line);
310 } else {
311 $errorAsException = new \ErrorException(self::ERROR_LEVELS_DESCRIPTION[$level] . ': ' . $message, 0, $level, $file, $line);
312 }
313 $backtrace = $this->cleanBacktraceFromErrorHandlerFrames($errorAsException->getTrace(), $errorAsException->getFile(), $errorAsException->getLine());
314 $this->exceptionReflection->setValue($errorAsException, $backtrace);
315 $this->invokeListeners($this->errorListeners, $errorAsException);
316 }
317 if ($this->previousErrorHandler !== null) {
318 return \false !== ($this->previousErrorHandler)($level, $message, $file, $line, $errcontext);
319 }
320 return \false;
321 }
322 private function shouldHandleError(int $level, bool $silenced) : bool
323 {
324 // If we were not given any options, we should handle all errors
325 if ($this->options === null) {
326 return \true;
327 }
328 if ($silenced) {
329 return $this->options->shouldCaptureSilencedErrors();
330 }
331 return ($this->options->getErrorTypes() & $level) !== 0;
332 }
333 /**
334 * Tries to handle a fatal error if any and relay them to the listeners.
335 * It only tries to do this if we still have some reserved memory at
336 * disposal. This method is used as callback of a shutdown function.
337 */
338 private function handleFatalError() : void
339 {
340 if (self::$disableFatalErrorHandler) {
341 return;
342 }
343 // Free the reserved memory that allows us to potentially handle OOM errors
344 self::$reservedMemory = null;
345 $error = \error_get_last();
346 if (!empty($error) && $error['type'] & (\E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_CORE_WARNING | \E_COMPILE_ERROR | \E_COMPILE_WARNING)) {
347 // 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
348 if (self::$didIncreaseMemoryLimit === \false && $this->memoryLimitIncreaseOnOutOfMemoryErrorValue !== null && \preg_match(self::OOM_MESSAGE_MATCHER, $error['message'], $matches) === 1) {
349 $currentMemoryLimit = (int) $matches['memory_limit'];
350 $newMemoryLimit = $currentMemoryLimit + $this->memoryLimitIncreaseOnOutOfMemoryErrorValue;
351 // It can happen that the memory limit + increase is still lower than
352 // the memory that is currently being used. This produces warnings
353 // that may end up in Sentry. To prevent this, we can check the real
354 // usage before.
355 if ($newMemoryLimit > \memory_get_usage(\true)) {
356 $this->setMemoryLimitWithoutHandlingWarnings($newMemoryLimit);
357 }
358 self::$didIncreaseMemoryLimit = \true;
359 }
360 $errorAsException = new FatalErrorException(self::ERROR_LEVELS_DESCRIPTION[$error['type']] . ': ' . $error['message'], 0, $error['type'], $error['file'], $error['line']);
361 $this->exceptionReflection->setValue($errorAsException, []);
362 $this->invokeListeners($this->fatalErrorListeners, $errorAsException);
363 }
364 }
365 /**
366 * Handles the given exception by passing it to all the listeners,
367 * then forwarding it to another handler.
368 *
369 * @param \Throwable $exception The exception to handle
370 *
371 * @throws \Throwable
372 */
373 private function handleException(\Throwable $exception) : void
374 {
375 $this->invokeListeners($this->exceptionListeners, $exception);
376 $previousExceptionHandlerException = $exception;
377 // Unset the previous exception handler to prevent infinite loop in case
378 // we need to handle an exception thrown from it
379 $previousExceptionHandler = $this->previousExceptionHandler;
380 $this->previousExceptionHandler = null;
381 try {
382 if ($previousExceptionHandler !== null) {
383 $previousExceptionHandler($exception);
384 return;
385 }
386 } catch (\Throwable $previousExceptionHandlerException) {
387 // This `catch` statement is here to forcefully override the
388 // $previousExceptionHandlerException variable with the exception
389 // we just caught
390 }
391 // If the instance of the exception we're handling is the same as the one
392 // caught from the previous exception handler then we give it back to the
393 // native PHP handler to prevent an infinite loop
394 if ($exception === $previousExceptionHandlerException) {
395 // Disable the fatal error handler or the error will be reported twice
396 self::$disableFatalErrorHandler = \true;
397 throw $exception;
398 }
399 $this->handleException($previousExceptionHandlerException);
400 }
401 /**
402 * Set the memory_limit while having no real error handler so that a warning emitted
403 * will not get reported.
404 */
405 private function setMemoryLimitWithoutHandlingWarnings(int $memoryLimit) : void
406 {
407 \set_error_handler(static function () : bool {
408 return \true;
409 }, \E_WARNING);
410 try {
411 \ini_set('memory_limit', (string) $memoryLimit);
412 } finally {
413 \restore_error_handler();
414 }
415 }
416 /**
417 * Cleans and returns the backtrace without the first frames that belong to
418 * this error handler.
419 *
420 * @param array<int, array<string, mixed>> $backtrace The backtrace to clear
421 * @param string $file The filename the backtrace was raised in
422 * @param int $line The line number the backtrace was raised at
423 *
424 * @phpstan-param list<StacktraceFrame> $backtrace
425 *
426 * @return array<int, mixed>
427 */
428 private function cleanBacktraceFromErrorHandlerFrames(array $backtrace, string $file, int $line) : array
429 {
430 $cleanedBacktrace = $backtrace;
431 $index = 0;
432 while ($index < \count($backtrace)) {
433 if (isset($backtrace[$index]['file'], $backtrace[$index]['line']) && $backtrace[$index]['line'] === $line && $backtrace[$index]['file'] === $file) {
434 $cleanedBacktrace = \array_slice($cleanedBacktrace, 1 + $index);
435 break;
436 }
437 ++$index;
438 }
439 return $cleanedBacktrace;
440 }
441 /**
442 * Invokes all the listeners and pass the exception to all of them.
443 *
444 * @param callable[] $listeners The array of listeners to be called
445 * @param \Throwable $throwable The exception to be passed onto listeners
446 */
447 private function invokeListeners(array $listeners, \Throwable $throwable) : void
448 {
449 foreach ($listeners as $listener) {
450 try {
451 $listener($throwable);
452 } catch (\Throwable $exception) {
453 // Do nothing as this should be as transparent as possible
454 }
455 }
456 }
457 }
458