/var/www/html_de/wp-content/plugins/query-monitor/collectors/php_errors.php


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
<?php declare(strict_types 1);
/**
 * PHP error collector.
 *
 * @package query-monitor
 */

if ( ! defined'ABSPATH' ) ) {
    exit;
}

if ( ! 
defined'QM_ERROR_FATALS' ) ) {
    
define'QM_ERROR_FATALS'E_ERROR E_PARSE E_COMPILE_ERROR E_USER_ERROR E_RECOVERABLE_ERROR );
}

/**
 * @extends QM_DataCollector<QM_Data_PHP_Errors>
 * @phpstan-type errorLabels array{
 *   warning: string,
 *   notice: string,
 *   strict: string,
 *   deprecated: string,
 * }
 * @phpstan-import-type errorObject from QM_Data_PHP_Errors
 */
class QM_Collector_PHP_Errors extends QM_DataCollector {

    
/**
     * @var string
     */
    
public $id 'php_errors';

    
/**
     * @var array<string, array<string, string>>
     * @phpstan-var array{
     *   errors: errorLabels,
     *   suppressed: errorLabels,
     *   silenced: errorLabels,
     * }
     */
    
public $types;

    
/**
     * @var int|null
     */
    
private $error_reporting null;

    
/**
     * @var string|false|null
     */
    
private $display_errors null;

    
/**
     * @var callable|null
     */
    
private $previous_error_handler null;

    
/**
     * @var callable|null
     */
    
private $previous_exception_handler null;

    
/**
     * @var string|null
     */
    
private static $unexpected_error null;

    public function 
get_storage(): QM_Data {
        return new 
QM_Data_PHP_Errors();
    }

    
/**
     * @return void
     */
    
public function set_up() {
        if ( 
defined'QM_DISABLE_ERROR_HANDLER' ) && QM_DISABLE_ERROR_HANDLER ) {
            return;
        }

        
parent::set_up();

        
// Capture the last error that occurred before QM loaded:
        
$prior_error error_get_last();

        
// Non-fatal error handler:
        
$this->previous_error_handler set_error_handler( array( $this'error_handler' ), ( E_ALL QM_ERROR_FATALS ) );

        
// Fatal error and uncaught exception handler:
        
$this->previous_exception_handler set_exception_handler( array( $this'exception_handler' ) );

        
$this->error_reporting error_reporting();
        
$this->display_errors ini_get'display_errors' );
        
ini_set'display_errors''0' );

        if ( 
$prior_error ) {
            
$this->error_handler(
                
$prior_error['type'],
                
$prior_error['message'],
                
$prior_error['file'],
                
$prior_error['line'],
                
null,
                
false
            
);
        }
    }

    
/**
     * @return void
     */
    
public function tear_down() {
        if ( 
defined'QM_DISABLE_ERROR_HANDLER' ) && QM_DISABLE_ERROR_HANDLER ) {
            return;
        }

        if ( 
null !== $this->previous_error_handler ) {
            
restore_error_handler();
        }

        if ( 
null !== $this->previous_exception_handler ) {
            
restore_exception_handler();
        }

        if ( 
null !== $this->error_reporting ) {
            
error_reporting$this->error_reporting );
        }

        if ( 
is_string$this->display_errors ) ) {
            
ini_set'display_errors'$this->display_errors );
        }

        
parent::tear_down();
    }

    
/**
     * Uncaught error handler.
     *
     * @param Throwable $e The error or exception.
     * @return void
     */
    
public function exception_handler$e ) {
        
$error 'Uncaught Error';

        if ( 
$e instanceof Exception ) {
            
$error 'Uncaught Exception';
        }

        
$this->output_fatal'Fatal error', array(
            
'message' => sprintf(
                
'%s: %s',
                
$error,
                
$e->getMessage()
            ),
            
'file' => $e->getFile(),
            
'line' => $e->getLine(),
            
'trace' => $e->getTrace(),
        ) );

        
// The error must be re-thrown or passed to the previously registered exception handler so that the error
        // is logged appropriately instead of discarded silently.
        
if ( $this->previous_exception_handler ) {
            
call_user_func$this->previous_exception_handler$e );
        } else {
            throw 
$e;
        }

        exit( 
);
    }

    
/**
     * @param int     $errno    The error number.
     * @param string  $message  The error message.
     * @param string  $file     The file location.
     * @param int     $line     The line number.
     * @param mixed[] $context  The context being passed.
     * @param bool    $do_trace Whether a stack trace should be included in the logged error data.
     * @return bool
     */
    
public function error_handler$errno$message$file null$line null$context null$do_trace true ) {
        
$type null;

        
/**
         * Fires before logging the PHP error in Query Monitor.
         *
         * @since 2.7.0
         *
         * @param int          $errno   The error number.
         * @param string       $message The error message.
         * @param string|null  $file    The file location.
         * @param int|null     $line    The line number.
         * @param mixed[]|null $context The context being passed.
         */
        
do_action'qm/collect/new_php_error'$errno$message$file$line$context );

        switch ( 
$errno ) {

            case 
E_WARNING:
            case 
E_USER_WARNING:
                
$type 'warning';
                break;

            case 
E_NOTICE:
            case 
E_USER_NOTICE:
                
$type 'notice';
                break;

            case 
E_DEPRECATED:
            case 
E_USER_DEPRECATED:
                
$type 'deprecated';
                break;

        }

        
// E_STRICT is deprecated in PHP 8.4 so it needs to be behind a version check.
        
if ( null === $type && version_comparePHP_VERSION'8.4''<' ) && E_STRICT === $errno ) {
            
$type 'strict';
        }

        if ( 
null === $type ) {
            return 
false;
        }

        if ( ! 
class_exists'QM_Backtrace' ) ) {
            return 
false;
        }

        
$error_group 'errors';

        if ( 
=== error_reporting() && !== $this->error_reporting ) {
            
// This is most likely an @-suppressed error
            
$error_group 'suppressed';
        }

        if ( ! isset( 
self::$unexpected_error ) ) {
            
// These strings are from core. They're passed through `__()` as variables so they get translated at runtime
            // but do not get seen by GlotPress when it populates its database of translatable strings for QM.
            
$unexpected_error 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.';
            
$wordpress_forums 'https://wordpress.org/support/forums/';

            
self::$unexpected_error sprintf(
                
call_user_func'__'$unexpected_error ),
                
call_user_func'__'$wordpress_forums )
            );
        }

        
// Intentionally skip reporting these core warnings. They're a distraction when developing offline.
        // The failed HTTP request will still appear in QM's output so it's not a big problem hiding these warnings.
        
if ( false !== strpos$messageself::$unexpected_error ) ) {
            return 
false;
        }

        
$trace = new QM_Backtrace();
        
$trace->push_frame( array(
            
'file' => $file,
            
'line' => $line,
        ) );
        
$caller $trace->get_caller();

        if ( 
$caller ) {
            
$key md5$message $file $line $caller['id'] );
        } else {
            
$key md5$message $file $line );
        }

        if ( isset( 
$this->data->{$error_group}[ $type ][ $key ] ) ) {
            
$this->data->{$error_group}[ $type ][ $key ]['calls']++;
        } else {
            
$this->data->{$error_group}[ $type ][ $key ] = array(
                
'errno' => $errno,
                
'type' => $type,
                
'message' => wp_strip_all_tags$message ),
                
'file' => $file,
                
'filename' => ( $file QM_Util::standard_dir$file'' ) : '' ),
                
'line' => $line,
                
'filtered_trace' => ( $do_trace $trace->get_filtered_trace() : null ),
                
'component' => $trace->get_component(),
                
'calls' => 1,
            );
        }

        
/**
         * Filters the PHP error handler return value. This can be used to control whether or not the default error
         * handler is called after Query Monitor's.
         *
         * @since 2.7.0
         *
         * @param bool $return_value Error handler return value. Default false.
         */
        
return apply_filters'qm/collect/php_errors_return_value'false );

    }

    
/**
     * @param string $error
     * @param mixed[] $e
     * @phpstan-param array{
     *   message: string,
     *   file: string,
     *   line: int,
     *   type?: int,
     *   trace?: mixed|null,
     * } $e
     * @return void
     */
    
protected function output_fatal$error, array $e ) {
        
$is_rest_request = ( defined'REST_REQUEST' ) && REST_REQUEST );
        
$is_ajax_request = ( defined'DOING_AJAX' ) && DOING_AJAX );

        if ( 
$is_rest_request ) {
            
$dispatcher QM_Dispatchers::get'rest' );
        } elseif ( 
$is_ajax_request ) {
            
$dispatcher QM_Dispatchers::get'ajax' );
        } else {
            
$dispatcher QM_Dispatchers::get'html' );
        }

        if ( empty( 
$dispatcher ) ) {
            return;
        }

        if ( empty( 
$this->display_errors ) && ! $dispatcher::user_can_view() ) {
            return;
        }

        if ( ! 
headers_sent() ) {
            
status_header500 );
        }

        
$message sprintf(
            
'%s in %s on line %d',
            
$e['message'],
            
$e['file'],
            
$e['line']
        );

        
$dispatcher->output_fatal$message$e );
        exit;
    }

    
/**
     * Runs post-processing on the collected errors and updates the
     * errors collected in the data->errors property.
     *
     * Any unreportable errors are placed in the data->filtered_errors
     * property.
     *
     * @return void
     */
    
public function process() {
        
$this->types = array(
            
'errors' => array(
                
'warning' => _x'Warning''PHP error level''query-monitor' ),
                
'notice' => _x'Notice''PHP error level''query-monitor' ),
                
'strict' => _x'Strict''PHP error level''query-monitor' ),
                
'deprecated' => _x'Deprecated''PHP error level''query-monitor' ),
            ),
            
'suppressed' => array(
                
'warning' => _x'Warning (Suppressed)''Suppressed PHP error level''query-monitor' ),
                
'notice' => _x'Notice (Suppressed)''Suppressed PHP error level''query-monitor' ),
                
'strict' => _x'Strict (Suppressed)''Suppressed PHP error level''query-monitor' ),
                
'deprecated' => _x'Deprecated (Suppressed)''Suppressed PHP error level''query-monitor' ),
            ),
            
'silenced' => array(
                
'warning' => _x'Warning (Silenced)''Silenced PHP error level''query-monitor' ),
                
'notice' => _x'Notice (Silenced)''Silenced PHP error level''query-monitor' ),
                
'strict' => _x'Strict (Silenced)''Silenced PHP error level''query-monitor' ),
                
'deprecated' => _x'Deprecated (Silenced)''Silenced PHP error level''query-monitor' ),
            ),
        );
        
$components = array();

        if ( ! empty( 
$this->data->errors ) ) {
            
/**
             * Filters the levels used for reported PHP errors on a per-component basis.
             *
             * Error levels can be specified in order to silence certain error levels from
             * plugins or the current theme. Most commonly, you may wish to use this filter
             * in order to silence annoying notices from third party plugins that you do not
             * have control over.
             *
             * Silenced errors will still appear in Query Monitor's output, but will not
             * cause highlighting to appear in the top level admin toolbar.
             *
             * For example, to show all errors in the 'foo' plugin except PHP notices use:
             *
             *     add_filter( 'qm/collect/php_error_levels', function( array $levels ) {
             *         $levels['plugin']['foo'] = ( E_ALL & ~E_NOTICE );
             *         return $levels;
             *     } );
             *
             * Errors from themes, WordPress core, and other components can also be filtered:
             *
             *     add_filter( 'qm/collect/php_error_levels', function( array $levels ) {
             *         $levels['theme']['stylesheet'] = ( E_WARNING & E_USER_WARNING );
             *         $levels['theme']['template']   = ( E_WARNING & E_USER_WARNING );
             *         $levels['core']['core']        = ( 0 );
             *         return $levels;
             *     } );
             *
             * Any component which doesn't have an error level specified via this filter is
             * assumed to have the default level of `E_ALL`, which shows all errors.
             *
             * Valid PHP error level bitmasks are supported for each component, including `0`
             * to silence all errors from a component. See the PHP documentation on error
             * reporting for more info: http://php.net/manual/en/function.error-reporting.php
             *
             * @since 2.7.0
             *
             * @param array<string,array<string,int>> $levels The error levels used for each component.
             */
            
$levels apply_filters'qm/collect/php_error_levels', array() );

            
array_map( array( $this'filter_reportable_errors' ), $levelsarray_keys$levels ) );

            foreach ( 
$this->types as $error_group => $error_types ) {
                foreach ( 
$error_types as $type => $title ) {
                    if ( isset( 
$this->data->{$error_group}[ $type ] ) ) {
                        
/**
                         * @var array<string, mixed> $error
                         * @phpstan-var errorObject $error
                         */
                        
foreach ( $this->data->{$error_group}[ $type ] as $error ) {
                            
$components$error['component']->name ] = $error['component']->name;
                        }
                    }
                }
            }
        }

        
$this->data->components $components;
    }

    
/**
     * Filters the reportable PHP errors using the table specified. Users can customize the levels
     * using the `qm/collect/php_error_levels` filter.
     *
     * @param array<string, int> $components     The error levels keyed by component name.
     * @param string             $component_type The component type, for example 'plugin' or 'theme'.
     * @return void
     */
    
public function filter_reportable_errors( array $components$component_type ) {
        
$all_errors $this->data->errors;

        foreach ( 
$components as $component_context => $allowed_level ) {
            foreach ( 
$all_errors as $error_level => $errors ) {
                foreach ( 
$errors as $error_id => $error ) {
                    if ( 
$this->is_reportable_error$error['errno'], $allowed_level ) ) {
                        continue;
                    }

                    if ( ! 
$this->is_affected_component$error['component'], $component_type$component_context ) ) {
                        continue;
                    }

                    unset( 
$this->data->errors$error_level ][ $error_id ] );

                    
$this->data->silenced$error_level ][ $error_id ] = $error;
                }
            }
        }

        
$this->data->errors array_filter$this->data->errors );
    }

    
/**
     * Checks if the component is of the given type and has the given context. This is
     * used to scope an error to a plugin or theme.
     *
     * @param QM_Component $component         The component.
     * @param string       $component_type    The component type for comparison.
     * @param string       $component_context The component context for comparison.
     * @return bool
     */
    
public function is_affected_component$component$component_type$component_context ) {
        return ( 
$component->type === $component_type && $component->context === $component_context );
    }

    
/**
     * Checks if the error number specified is viewable based on the
     * flags specified.
     *
     * Eg:- If a plugin had the config flags,
     *
     *     E_ALL & ~E_NOTICE
     *
     * then,
     *
     *     is_reportable_error( E_NOTICE, E_ALL & ~E_NOTICE ) is false
     *     is_reportable_error( E_WARNING, E_ALL & ~E_NOTICE ) is true
     *
     * If the `$flag` is null, all errors are assumed to be
     * reportable by default.
     *
     * @param int      $error_no The errno from PHP
     * @param int|null $flags The config flags specified by users
     * @return bool Whether the error is reportable.
     */
    
public function is_reportable_error$error_no$flags ) {
        
$result true;

        if ( 
null !== $flags ) {
            
$result = (bool) ( $error_no $flags );
        }

        return 
$result;
    }

    
/**
     * For testing purposes only. Sets the errors property manually.
     * Needed to test the filter since the data property is protected.
     *
     * @param array<string, mixed> $errors The list of errors
     * @return void
     */
    
public function set_php_errors$errors ) {
        
$this->data->errors $errors;
    }
}

# Load early to catch early errors
QM_Collectors::add( new QM_Collector_PHP_Errors() );