collector->id; } /** * @param array $menu * @return array */ public function admin_menu( array $menu ) { $menu[ $this->collector->id() ] = $this->menu( array( 'title' => esc_html( $this->name() ), ) ); return $menu; } /** * @return string */ public function get_output() { ob_start(); // compat until I convert all the existing outputters to use `get_output()` $this->output(); $out = (string) ob_get_clean(); return $out; } /** * @param string $id * @param string $name * @return void */ protected function before_tabular_output( $id = null, $name = null ) { if ( null === $id ) { $id = $this->collector->id(); } if ( null === $name ) { $name = $this->name(); } $this->current_id = $id; $this->current_name = $name; printf( '
', esc_attr( $id ) ); echo ''; printf( '', esc_attr( $id ), esc_html( $name ) ); } /** * @return void */ protected function after_tabular_output() { echo '

%2$s

'; echo '
'; $this->output_concerns(); } /** * @param string $id * @param string $name * @return void */ protected function before_non_tabular_output( $id = null, $name = null ) { if ( null === $id ) { $id = $this->collector->id(); } if ( null === $name ) { $name = $this->name(); } $this->current_id = $id; $this->current_name = $name; printf( '
', esc_attr( $id ) ); echo '
'; printf( '

%2$s

', esc_attr( $id ), esc_html( $name ) ); } /** * @return void */ protected function after_non_tabular_output() { echo '
'; echo '
'; $this->output_concerns(); } /** * @return void */ protected function output_concerns() { $concerns = array( 'concerned_actions' => array( __( 'Related Hooks with Actions Attached', 'query-monitor' ), __( 'Action', 'query-monitor' ), ), 'concerned_filters' => array( __( 'Related Hooks with Filters Attached', 'query-monitor' ), __( 'Filter', 'query-monitor' ), ), ); if ( empty( $this->collector->concerned_actions ) && empty( $this->collector->concerned_filters ) ) { return; } printf( '
', esc_attr( $this->current_id . '-concerned_hooks' ) ); echo ''; printf( '', esc_attr( $this->current_id . '-concerned_hooks' ), sprintf( /* translators: %s: Panel name */ esc_html__( '%s: Related Hooks with Filters or Actions Attached', 'query-monitor' ), esc_html( $this->name() ) ) ); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ( $concerns as $key => $labels ) { if ( empty( $this->collector->$key ) ) { continue; } QM_Output_Html_Hooks::output_hook_table( $this->collector->$key, true ); } echo ''; echo '

%2$s

' . esc_html__( 'Hook', 'query-monitor' ) . '' . esc_html__( 'Type', 'query-monitor' ) . '' . esc_html__( 'Priority', 'query-monitor' ) . '' . esc_html__( 'Callback', 'query-monitor' ) . '' . esc_html__( 'Component', 'query-monitor' ) . '
'; echo '
'; } /** * @param string $id * @param string $name * @return void */ protected function before_debug_bar_output( $id = null, $name = null ) { if ( null === $id ) { $id = $this->collector->id(); } if ( null === $name ) { $name = $this->name(); } printf( '
', esc_attr( $id ) ); printf( '

%2$s

', esc_attr( $id ), esc_html( $name ) ); } /** * @return void */ protected function after_debug_bar_output() { echo '
'; } /** * @param string $notice * @return string */ protected function build_notice( $notice ) { $return = '
'; $return .= '
'; $return .= '

'; $return .= $notice; $return .= '

'; $return .= '
'; $return .= '
'; return $return; } /** * @param array $vars * @return void */ public static function output_inner( array $vars ) { echo ''; foreach ( $vars as $key => $value ) { echo ''; echo ''; if ( is_array( $value ) ) { echo ''; } elseif ( is_object( $value ) ) { echo ''; } elseif ( is_bool( $value ) ) { if ( $value ) { echo ''; } else { echo ''; } } else { echo ''; } echo ''; echo ''; } echo '
' . esc_html( $key ) . ''; self::output_inner( $value ); echo ''; self::output_inner( get_object_vars( $value ) ); echo 'truefalse'; echo nl2br( esc_html( $value ) ); echo '
'; } /** * Returns the table filter controls. Safe for output. * * @param string $name The name for the `data-` attributes that get filtered by this control. * @param (string|int)[] $values Option values for this control. * @param string $label Label text for the filter control. * @param array $args { * @type string $highlight The name for the `data-` attributes that get highlighted by this control. * @type string[] $prepend Associative array of options to prepend to the list of values. * @type string[] $append Associative array of options to append to the list of values. * } * @phpstan-param array{ * highlight?: string, * prepend?: array, * append?: array, * } $args * @return string Markup for the table filter controls. */ protected function build_filter( $name, $values, $label, $args = array() ) { if ( empty( $values ) || ! is_array( $values ) ) { return esc_html( $label ); // Return label text, without being marked up as a label element. } if ( ! is_array( $args ) ) { $args = array( 'highlight' => $args, ); } $args = array_merge( array( 'highlight' => '', 'prepend' => array(), 'append' => array(), 'all' => _x( 'All', '"All" option for filters', 'query-monitor' ), ), $args ); $core_val = __( 'WordPress Core', 'query-monitor' ); $core_key = array_search( $core_val, $values, true ); if ( 'component' === $name && count( $values ) > 1 && false !== $core_key ) { $args['append'][ $core_val ] = $core_val; $args['append']['non-core'] = __( 'Non-WordPress Core', 'query-monitor' ); unset( $values[ $core_key ] ); } $filter_id = 'qm-filter-' . $this->collector->id . '-' . $name; $out = '
'; $out .= ''; $out .= ''; $out .= '
'; return $out; } /** * Returns the column sorter controls. Safe for output. * * @param string $heading Heading text for the column. Optional. * @return string Markup for the column sorter controls. */ protected function build_sorter( $heading = '' ) { $out = ''; $out .= ''; $out .= ''; if ( '#' === $heading ) { $out .= '' . esc_html__( 'Sequence', 'query-monitor' ) . ''; } elseif ( $heading ) { $out .= esc_html( $heading ); } $out .= ''; $out .= ''; $out .= ''; return $out; } /** * Returns a toggle control. Safe for output. * * @return string Markup for the column sorter controls. */ protected static function build_toggler() { $out = ''; return $out; } /** * Returns a filter trigger. * * @param string $target * @param string $filter * @param string $value * @param string $label * @return string */ protected static function build_filter_trigger( $target, $filter, $value, $label ) { return sprintf( '', esc_attr( $target ), esc_attr( $filter ), esc_attr( $value ), $label, QueryMonitor::icon( 'filter' ) ); } /** * Returns a link. * * @param string $href * @param string $label * @return string */ protected static function build_link( $href, $label ) { return sprintf( '%2$s%3$s', esc_attr( $href ), $label, QueryMonitor::icon( 'external' ) ); } /** * @param array $args * @return array */ protected function menu( array $args ) { return array_merge( array( 'id' => esc_attr( "query-monitor-{$this->collector->id}" ), 'href' => esc_attr( '#' . $this->collector->id() ), ), $args ); } /** * Returns the given SQL string in a nicely presented format. Safe for output. * * @param string $sql An SQL query string. * @return string The SQL formatted with markup. */ public static function format_sql( $sql ) { $sql = str_replace( array( "\r\n", "\r", "\n", "\t" ), ' ', $sql ); $sql = esc_html( $sql ); $sql = trim( $sql ); $regex = 'ADD|AFTER|ALTER|AND|BEGIN|COMMIT|CREATE|DELETE|DESCRIBE|DO|DROP|ELSE|END|EXCEPT|EXPLAIN|FROM|GROUP|HAVING|INNER|INSERT|INTERSECT|LEFT|LIMIT|ON|OR|ORDER|OUTER|RENAME|REPLACE|RIGHT|ROLLBACK|SELECT|SET|SHOW|START|THEN|TRUNCATE|UNION|UPDATE|USE|USING|VALUES|WHEN|WHERE|XOR'; $sql = preg_replace( '# (' . $regex . ') #', '
$1 ', $sql ); $keywords = '\b(?:ACTION|ADD|AFTER|AGAINST|ALTER|AND|ASC|AS|AUTO_INCREMENT|BEGIN|BETWEEN|BIGINT|BINARY|BIT|BLOB|BOOLEAN|BOOL|BREAK|BY|CASE|COLLATE|COLUMNS?|COMMIT|CONTINUE|CREATE|DATA(?:BASES?)?|DATE(?:TIME)?|DECIMAL|DECLARE|DEC|DEFAULT|DELAYED|DELETE|DESCRIBE|DESC|DISTINCT|DOUBLE|DO|DROP|DUPLICATE|ELSE|END|ENUM|EXCEPT|EXISTS|EXPLAIN|FIELDS|FLOAT|FORCE|FOREIGN|FOR|FROM|FULL|FUNCTION|GROUP|HAVING|IF|IGNORE|INDEX|INNER|INSERT|INTEGER|INTERSECT|INTERVAL|INTO|INT|IN|IS|JOIN|KEYS?|LEFT|LIKE|LIMIT|LONG(?:BLOB|TEXT)|MEDIUM(?:BLOB|INT|TEXT)|MATCH|MERGE|MIDDLEINT|NOT|NO|NULLIF|ON|ORDER|OR|OUTER|PRIMARY|PROC(?:EDURE)?|REGEXP|RENAME|REPLACE|RIGHT|RLIKE|ROLLBACK|SCHEMA|SELECT|SET|SHOW|SMALLINT|START|TABLES?|TEXT(?:SIZE)?|THEN|TIME(?:STAMP)?|TINY(?:BLOB|INT|TEXT)|TRUNCATE|UNION|UNIQUE|UNSIGNED|UPDATE|USE|USING|VALUES?|VAR(?:BINARY|CHAR)|WHEN|WHERE|WHILE|XOR)\b'; $sql = preg_replace( '#' . $keywords . '#', '$0', $sql ); return '' . $sql . ''; } /** * Returns the given URL in a nicely presented format. Safe for output. * * @param string $url A URL. * @return string The URL formatted with markup. */ public static function format_url( $url ) { return str_replace( array( '?', '&' ), array( '
?', '
&' ), esc_html( $url ) ); } /** * Returns a file path, name, and line number, or a clickable link to the file. Safe for output. * * @link https://querymonitor.com/blog/2019/02/clickable-stack-traces-and-function-names-in-query-monitor/ * * @param string $text The display text, such as a function name or file name. * @param string $file The full file path and name. * @param int $line Optional. A line number, if appropriate. * @param bool $is_filename Optional. Is the text a plain file name? Default false. * @return string The fully formatted file link or file name, safe for output. */ public static function output_filename( $text, $file, $line = 0, $is_filename = false ) { if ( empty( $file ) ) { if ( $is_filename ) { return esc_html( $text ); } else { return '' . esc_html( $text ) . ''; } } $link_line = $line ?: 1; if ( ! self::has_clickable_links() ) { $fallback = QM_Util::standard_dir( $file, '' ); if ( $line ) { $fallback .= ':' . $line; } if ( $is_filename ) { $return = esc_html( $text ); } else { $return = '' . esc_html( $text ) . ''; } if ( $fallback !== $text ) { $return .= '
' . esc_html( $fallback ) . ''; } return $return; } $map = self::get_file_path_map(); if ( ! empty( $map ) ) { foreach ( $map as $from => $to ) { $file = str_replace( $from, $to, $file ); } } /** @var string */ $link_format = self::get_file_link_format(); $link = sprintf( $link_format, rawurlencode( $file ), intval( $link_line ) ); if ( $is_filename ) { $format = '%2$s%3$s'; } else { $format = '%2$s%3$s'; } return sprintf( $format, esc_attr( $link ), esc_html( $text ), QueryMonitor::icon( 'edit' ) ); } /** * Provides a protocol URL for edit links in QM stack traces for various editors. * * @param string $editor The chosen code editor. * @param string|false $default_format A format to use if no editor is found. * @return string|false A protocol URL format or boolean false. */ public static function get_editor_file_link_format( $editor, $default_format ) { switch ( $editor ) { case 'phpstorm': return 'phpstorm://open?file=%f&line=%l'; case 'vscode': return 'vscode://file/%f:%l'; case 'atom': return 'atom://open/?url=file://%f&line=%l'; case 'sublime': return 'subl://open/?url=file://%f&line=%l'; case 'textmate': return 'txmt://open/?url=file://%f&line=%l'; case 'netbeans': return 'nbopen://%f:%l'; case 'nova': return 'nova://open?path=%f&line=%l'; default: return $default_format; } } /** * @return string|false */ public static function get_file_link_format() { if ( ! isset( self::$file_link_format ) ) { $format = ini_get( 'xdebug.file_link_format' ); if ( defined( 'QM_EDITOR_COOKIE' ) && isset( $_COOKIE[ QM_EDITOR_COOKIE ] ) ) { $format = self::get_editor_file_link_format( $_COOKIE[ QM_EDITOR_COOKIE ], $format ); } /** * Filters the clickable file link format. * * @link https://querymonitor.com/blog/2019/02/clickable-stack-traces-and-function-names-in-query-monitor/ * @since 3.0.0 * * @param string|false $format The format of the clickable file link, or false if there is none. */ $format = apply_filters( 'qm/output/file_link_format', $format ); if ( empty( $format ) ) { self::$file_link_format = false; } else { self::$file_link_format = str_replace( array( '%f', '%l' ), array( '%1$s', '%2$d' ), $format ); } } return self::$file_link_format; } /** * @return array */ public static function get_file_path_map() { /** * Filters the file path mapping for clickable file links. * * @link https://querymonitor.com/blog/2019/02/clickable-stack-traces-and-function-names-in-query-monitor/ * @since 3.0.0 * * @param array $file_map Array of file path mappings. Keys are the source paths and values are the replacement paths. */ return apply_filters( 'qm/output/file_path_map', array() ); } /** * @return bool */ public static function has_clickable_links() { return ( false !== self::get_file_link_format() ); } }