class names, the second level array keys are method names, and * the array values are whether to ignore the method (usually true). */ self::$ignore_method = array_filter( apply_filters( 'qm/trace/ignore_method', self::$ignore_method ) ); /** * Filters which action and filter names to ignore when constructing user-facing call stacks. * * @since 3.8.0 * * @param array $ignore_hook Array of hook names to ignore. The array keys are hook names to ignore, * the array values are whether to ignore the hook (usually true). */ self::$ignore_hook = array_filter( apply_filters( 'qm/trace/ignore_hook', self::$ignore_hook ) ); /** * Filters the number of argument values to show for the given function name when constructing user-facing * call stacks. * * @since 2.7.0 * * @param array $show_args The number of argument values to show for the given function name. The * array keys are function names, the array values are either integers or * "dir" to specifically treat the function argument as a directory path. */ self::$show_args = apply_filters( 'qm/trace/show_args', self::$show_args ); self::$filtered = true; } } /** * Formats a single trace frame and determines whether it should be hidden. * * @param mixed[] $frame A single frame from debug_backtrace(). * @param array $ignore_namespace * @param array> $ignore_method * @param array $ignore_hook * @param array $show_args */ public function filter_trace( array $frame, array $ignore_namespace, array $ignore_method, array $ignore_hook, array $show_args ) :? QM_Backtrace_Frame { $hook_functions = array( 'apply_filters' => true, 'do_action' => true, 'apply_filters_ref_array' => true, 'do_action_ref_array' => true, 'apply_filters_deprecated' => true, 'do_action_deprecated' => true, ); if ( ! isset( $frame['function'] ) ) { $frame['function'] = '(unknown)'; } $id = ''; $frame_args = null; if ( isset( $frame['class'] ) ) { // WP_Hook methods are always filtered out. The do_action/apply_filters // frame below them is retained with its original file/line. if ( 'WP_Hook' === $frame['class'] ) { return null; } if ( isset( $ignore_method[ $frame['class'] ][ $frame['function'] ] ) ) { return null; } foreach ( array_keys( $ignore_namespace ) as $namespace ) { if ( 0 === strpos( $frame['class'], $namespace . '\\' ) ) { return null; } } $id = $frame['class'] . $frame['type'] . $frame['function']; } else { foreach ( array_keys( $ignore_namespace ) as $namespace ) { if ( 0 === strpos( $frame['function'], $namespace . '\\' ) ) { return null; } } if ( isset( $show_args[ $frame['function'] ] ) ) { $show = $show_args[ $frame['function'] ]; if ( 'dir' === $show ) { if ( isset( $frame['args'][0] ) ) { $id = $frame['function']; $frame_args = QM_Util::standard_dir( $frame['args'][0], '' ); } } else { if ( isset( $hook_functions[ $frame['function'] ], $frame['args'][0] ) && is_string( $frame['args'][0] ) && isset( $ignore_hook[ $frame['args'][0] ] ) ) { return null; } $args = array(); for ( $i = 0; $i < $show; $i++ ) { if ( isset( $frame['args'] ) && array_key_exists( $i, $frame['args'] ) ) { if ( is_string( $frame['args'][ $i ] ) ) { $args[] = '\'' . $frame['args'][ $i ] . '\''; } else { $args[] = QM_Util::display_variable( $frame['args'][ $i ] ); } } } $id = $frame['function']; $frame_args = implode( ',', $args ); } } else { $id = $frame['function']; } } $result = new QM_Backtrace_Frame(); $result->id = $id; $result->args = $frame_args; $result->file = $frame['file'] ?? null; $result->line = $frame['line'] ?? null; $result->function = $frame['function']; if ( isset( $frame['class'] ) ) { $result->class = $frame['class']; } // Hook dispatch functions keep their original file/line so clicking // them takes you to the do_action/apply_filters call site, not to // the internal location where WP_Hook invokes the callback. if ( isset( $hook_functions[ $frame['function'] ] ) ) { $result->keep_file_line = true; } return $result; } /** * Shifts file/line numbers and registers each frame with the frame * registry. Called eagerly from the constructor so the registry is * populated without needing a separate serialization pass. * * @param QM_Backtrace_Frame[] $trace * @return void */ protected function process_output_frames( array $trace ): void { // Seed the shift from the innermost dropped/trimmed frame's location, // or from the call site for PHP error traces. $prev_file = $this->top_frame_location['file'] ?? ( $this->callsite ? $this->callsite->file : null ); $prev_line = $this->top_frame_location['line'] ?? ( $this->callsite ? $this->callsite->line : null ); foreach ( $trace as $frame ) { $data = $frame->to_data(); if ( $frame->keep_file_line ) { // Hook dispatch frames (do_action, apply_filters) keep their // original file/line. Update prev so the frame below gets // shifted to this location (where it calls the hook). $prev_file = $frame->file; $prev_line = $frame->line; } else { // Shift file/line up by one frame so each frame shows where it // makes its call to the frame above, not where it was called from. $data->file = $prev_file; $data->line = $prev_line; $prev_file = $frame->file; $prev_line = $frame->line; } $this->output_frames[] = array( QM_Frame_Registry::register( $data ), $data->line ); } } /** * @phpstan-return array{ * component: QM_Component, * callsite: ?QM_Data_Callsite, * frames: list, * time: float, * } */ public function jsonSerialize(): array { return array( 'component' => $this->get_component(), 'callsite' => $this->callsite, 'frames' => $this->output_frames, 'time' => ( $this->time - $_SERVER['REQUEST_TIME_FLOAT'] ) * 1000, ); } } } else { add_action( 'init', 'QueryMonitor::symlink_warning' ); } class names, the second level array keys are method names, and * the array values are whether to ignore the method (usually true). */ self::$ignore_method = array_filter( apply_filters( 'qm/trace/ignore_method', self::$ignore_method ) ); /** * Filters which action and filter names to ignore when constructing user-facing call stacks. * * @since 3.8.0 * * @param array $ignore_hook Array of hook names to ignore. The array keys are hook names to ignore, * the array values are whether to ignore the hook (usually true). */ self::$ignore_hook = array_filter( apply_filters( 'qm/trace/ignore_hook', self::$ignore_hook ) ); /** * Filters the number of argument values to show for the given function name when constructing user-facing * call stacks. * * @since 2.7.0 * * @param array $show_args The number of argument values to show for the given function name. The * array keys are function names, the array values are either integers or * "dir" to specifically treat the function argument as a directory path. */ self::$show_args = apply_filters( 'qm/trace/show_args', self::$show_args ); self::$filtered = true; } } /** * Formats a single trace frame and determines whether it should be hidden. * * @param mixed[] $frame A single frame from debug_backtrace(). * @param array $ignore_namespace * @param array> $ignore_method * @param array $ignore_hook * @param array $show_args */ public function filter_trace( array $frame, array $ignore_namespace, array $ignore_method, array $ignore_hook, array $show_args ) :? QM_Backtrace_Frame { $hook_functions = array( 'apply_filters' => true, 'do_action' => true, 'apply_filters_ref_array' => true, 'do_action_ref_array' => true, 'apply_filters_deprecated' => true, 'do_action_deprecated' => true, ); if ( ! isset( $frame['function'] ) ) { $frame['function'] = '(unknown)'; } $id = ''; $frame_args = null; if ( isset( $frame['class'] ) ) { // WP_Hook methods are always filtered out. The do_action/apply_filters // frame below them is retained with its original file/line. if ( 'WP_Hook' === $frame['class'] ) { return null; } if ( isset( $ignore_method[ $frame['class'] ][ $frame['function'] ] ) ) { return null; } foreach ( array_keys( $ignore_namespace ) as $namespace ) { if ( 0 === strpos( $frame['class'], $namespace . '\\' ) ) { return null; } } $id = $frame['class'] . $frame['type'] . $frame['function']; } else { foreach ( array_keys( $ignore_namespace ) as $namespace ) { if ( 0 === strpos( $frame['function'], $namespace . '\\' ) ) { return null; } } if ( isset( $show_args[ $frame['function'] ] ) ) { $show = $show_args[ $frame['function'] ]; if ( 'dir' === $show ) { if ( isset( $frame['args'][0] ) ) { $id = $frame['function']; $frame_args = QM_Util::standard_dir( $frame['args'][0], '' ); } } else { if ( isset( $hook_functions[ $frame['function'] ], $frame['args'][0] ) && is_string( $frame['args'][0] ) && isset( $ignore_hook[ $frame['args'][0] ] ) ) { return null; } $args = array(); for ( $i = 0; $i < $show; $i++ ) { if ( isset( $frame['args'] ) && array_key_exists( $i, $frame['args'] ) ) { if ( is_string( $frame['args'][ $i ] ) ) { $args[] = '\'' . $frame['args'][ $i ] . '\''; } else { $args[] = QM_Util::display_variable( $frame['args'][ $i ] ); } } } $id = $frame['function']; $frame_args = implode( ',', $args ); } } else { $id = $frame['function']; } } $result = new QM_Backtrace_Frame(); $result->id = $id; $result->args = $frame_args; $result->file = $frame['file'] ?? null; $result->line = $frame['line'] ?? null; $result->function = $frame['function']; if ( isset( $frame['class'] ) ) { $result->class = $frame['class']; } // Hook dispatch functions keep their original file/line so clicking // them takes you to the do_action/apply_filters call site, not to // the internal location where WP_Hook invokes the callback. if ( isset( $hook_functions[ $frame['function'] ] ) ) { $result->keep_file_line = true; } return $result; } /** * Shifts file/line numbers and registers each frame with the frame * registry. Called eagerly from the constructor so the registry is * populated without needing a separate serialization pass. * * @param QM_Backtrace_Frame[] $trace * @return void */ protected function process_output_frames( array $trace ): void { // Seed the shift from the innermost dropped/trimmed frame's location, // or from the call site for PHP error traces. $prev_file = $this->top_frame_location['file'] ?? ( $this->callsite ? $this->callsite->file : null ); $prev_line = $this->top_frame_location['line'] ?? ( $this->callsite ? $this->callsite->line : null ); foreach ( $trace as $frame ) { $data = $frame->to_data(); if ( $frame->keep_file_line ) { // Hook dispatch frames (do_action, apply_filters) keep their // original file/line. Update prev so the frame below gets // shifted to this location (where it calls the hook). $prev_file = $frame->file; $prev_line = $frame->line; } else { // Shift file/line up by one frame so each frame shows where it // makes its call to the frame above, not where it was called from. $data->file = $prev_file; $data->line = $prev_line; $prev_file = $frame->file; $prev_line = $frame->line; } $this->output_frames[] = array( QM_Frame_Registry::register( $data ), $data->line ); } } /** * @phpstan-return array{ * component: QM_Component, * callsite: ?QM_Data_Callsite, * frames: list, * time: float, * } */ public function jsonSerialize(): array { return array( 'component' => $this->get_component(), 'callsite' => $this->callsite, 'frames' => $this->output_frames, 'time' => ( $this->time - $_SERVER['REQUEST_TIME_FLOAT'] ) * 1000, ); } } } else { add_action( 'init', 'QueryMonitor::symlink_warning' ); }