From 991d40d135fad97f99282786b87185eee3952a36 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 2 Sep 2025 18:56:29 +0100 Subject: [PATCH 01/32] Initial hello world to get a log page with menu and sub-menu for GraphQL. --- .../src/Admin/View/Grid_Service.php | 11 ++++ .../src/Admin/View_Logs_Page.php | 64 +++++++++++++++++++ plugins/wpgraphql-logging/src/Plugin.php | 2 + 3 files changed, 77 insertions(+) create mode 100644 plugins/wpgraphql-logging/src/Admin/View/Grid_Service.php create mode 100644 plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php diff --git a/plugins/wpgraphql-logging/src/Admin/View/Grid_Service.php b/plugins/wpgraphql-logging/src/Admin/View/Grid_Service.php new file mode 100644 index 00000000..1a76dfa3 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/View/Grid_Service.php @@ -0,0 +1,11 @@ +setup(); + } + + do_action( 'wpgraphql_logging_view_logs_init', self::$instance ); + + return self::$instance; + } + + public function setup(): void { + add_action( 'admin_menu', [ $this, 'register_settings_page' ], 10, 0); + } + + public function register_settings_page(): void { + + // Add submenu under GraphQL menu using the correct parent slug + add_menu_page( + esc_html__( 'GraphQL Logs', 'wpgraphql-logging' ), + esc_html__( 'GraphQL Logs', 'wpgraphql-logging' ), + 'manage_options', + self::ADMIN_PAGE_SLUG, + [ $this, 'render_admin_page' ], + 'dashicons-list-view', + 25 + ); + add_submenu_page( + 'graphiql-ide', + esc_html__( 'GraphQL Logs', 'wpgraphql-logging' ), + esc_html__( 'GraphQL Logs', 'wpgraphql-logging' ), + 'manage_options', + self::ADMIN_PAGE_SLUG, + [ $this, 'render_admin_page' ] + ); + } + + public function render_admin_page(): void { + // For now, just hello world + echo '

Hello World

'; + + // Here is where we will render the grid + $grid_service = new Grid_Service(); + $grid_service->render(); + } +} diff --git a/plugins/wpgraphql-logging/src/Plugin.php b/plugins/wpgraphql-logging/src/Plugin.php index 8c9ad882..61621f8b 100644 --- a/plugins/wpgraphql-logging/src/Plugin.php +++ b/plugins/wpgraphql-logging/src/Plugin.php @@ -5,6 +5,7 @@ namespace WPGraphQL\Logging; use WPGraphQL\Logging\Admin\Settings_Page; +use WPGraphQL\Logging\Admin\View_Logs_Page; use WPGraphQL\Logging\Events\EventManager; use WPGraphQL\Logging\Events\QueryEventLifecycle; use WPGraphQL\Logging\Logger\Database\DatabaseEntity; @@ -54,6 +55,7 @@ public static function init(): self { */ public function setup(): void { Settings_Page::init(); + View_Logs_Page::init(); QueryEventLifecycle::init(); } From 691a15ac7b91b8f194843d40150314f97b15d925 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 3 Sep 2025 18:07:12 +0100 Subject: [PATCH 02/32] Add logs list table and repository for admin UI Replaces the grid view with a paginated logs list table in the admin area using WP_List_Table. Introduces List_Table and LogsRepository classes for log retrieval and display, updates View_Logs_Page to use the new list view, and refactors DatabaseEntity for improved log querying and instantiation. --- .../src/Admin/View/Grid_Service.php | 11 - .../src/Admin/View/List/List_Table.php | 230 ++++++++++++++++++ .../List/Templates/wpgraphql-logger-list.php | 30 +++ .../src/Admin/View_Logs_Page.php | 65 ++++- .../src/Logger/Database/DatabaseEntity.php | 81 ++++-- .../src/Logger/Database/LogsRepository.php | 61 +++++ 6 files changed, 436 insertions(+), 42 deletions(-) delete mode 100644 plugins/wpgraphql-logging/src/Admin/View/Grid_Service.php create mode 100644 plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php create mode 100644 plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php create mode 100644 plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php diff --git a/plugins/wpgraphql-logging/src/Admin/View/Grid_Service.php b/plugins/wpgraphql-logging/src/Admin/View/Grid_Service.php deleted file mode 100644 index 1a76dfa3..00000000 --- a/plugins/wpgraphql-logging/src/Admin/View/Grid_Service.php +++ /dev/null @@ -1,11 +0,0 @@ - $args Optional. An array of arguments. + */ + public function __construct( + public readonly LogsRepository $repository, + $args = [] + ) { + parent::__construct( + [ + 'singular' => __( 'Log', 'wpgraphql-logging' ), + 'plural' => __( 'Logs', 'wpgraphql-logging' ), + 'ajax' => false, + ] + ); + } + + /** + * Prepare items for display. + */ + public function prepare_items(): void { + $this->_column_headers = + apply_filters( + 'wpgraphql_logging_logs_table_column_headers', + [ + $this->get_columns(), + [], // hidden. + $this->get_sortable_columns(), + 'id', + ] + ); + + // @TODO + $per_page = $this->get_items_per_page( 'logs_per_page', 20 ); + $current_page = $this->get_pagenum(); + $total_items = $this->repository->get_log_count(); + + $this->set_pagination_args( + [ + 'total_items' => $total_items, + 'per_page' => $per_page, + ] + ); + + $this->items = $this->repository->get_logs( + [ + 'number' => $per_page, + 'offset' => ( $current_page - 1 ) * $per_page, + ] + ); + } + + /** + * Get the columns for the logs table. + * + * @return array The columns. + */ + public function get_columns(): array { + return apply_filters( + 'wpgraphql_logging_logs_table_column_headers', + [ + 'cb' => '', + 'id' => __( 'ID', 'wpgraphql-logging' ), + 'date' => __( 'Date', 'wpgraphql-logging' ), + 'wpgraphql_query' => __( 'Query', 'wpgraphql-logging' ), + 'level' => __( 'Level', 'wpgraphql-logging' ), + 'level_name' => __( 'Level Name', 'wpgraphql-logging' ), + 'event' => __( 'Event', 'wpgraphql-logging' ), + 'process_id' => __( 'Process ID', 'wpgraphql-logging' ), + 'headers' => __( 'Headers', 'wpgraphql-logging' ), + 'time' => __( 'Time', 'wpgraphql-logging' ), + 'memory_usage' => __( 'Memory Usage', 'wpgraphql-logging' ), + ] + ); + } + + /** + * Get the default column value for a log entry. + * + * @param \WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. + * @param string $column_name The column name. + * + * @phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded + * + * @return mixed The default column value or null. + */ + public function column_default( $item, $column_name ): mixed { + if ( ! $item instanceof DatabaseEntity ) { + return null; + } + + switch ( $column_name ) { + case 'date': + return $item->get_datetime(); + case 'channel': + return $item->get_channel(); + case 'level': + return $item->get_level(); + case 'level_name': + return $item->get_level_name(); + case 'message': + return $item->get_message(); + case 'event': + return $this->get_event( $item ); + case 'process_id': + return $this->get_process_id( $item ); + case 'memory_usage': + return $this->get_memory_usage( $item ); + case 'wpgraphql_query': + return $this->get_query( $item ); + default: + // Users can add their own custom columns and render functionality. + return apply_filters( 'wpgraphql_logging_logs_table_column_value', '', $item, $column_name ); + } + } + + /** + * Renders the checkbox column for a log entry. + * + * @var ?\WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. + * + * @phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingAnyTypeHint + * + * @return string The rendered checkbox column or null. + */ + public function column_cb( $item ): string { + if ( ! $item instanceof DatabaseEntity ) { + return null; + } + return sprintf( + '', + $item->get_id() + ); + } + + /** + * Renders the ID column for a log entry. + * + * @param \WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. + * + * @return string The rendered ID column or null. + */ + public function column_id( DatabaseEntity $item ): string { + $url = isset( $_REQUEST['page'] ) ? esc_attr( sanitize_text_field( $_REQUEST['page'] ) ) : ''; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + $actions = [ + 'view' => sprintf( 'View', $url, 'view', $item->get_id() ), + ]; + return sprintf( '%1$s %2$s', $item->get_id(), $this->row_actions( $actions ) ); + } + + /** + * Renders the query column for a log entry. + * + * @param \WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. + * + * @return string|null The rendered query column or null. + */ + public function column_query( DatabaseEntity $item ): ?string { + $extra = $item->get_extra(); + return ! empty( $extra['wpgraphql_query'] ) ? esc_html( $extra['wpgraphql_query'] ) : ''; + } + + /** + * Gets the query from extra. + * + * @return string The query + */ + public function get_query(DatabaseEntity $item): string { + $extra = $item->get_extra(); + return ! empty( $extra['wpgraphql_query'] ) ? esc_html( $extra['wpgraphql_query'] ) : ''; + } + + /** + * Gets the event from extra. + * + * @return string The event + */ + public function get_event(DatabaseEntity $item): string { + + $extra = $item->get_extra(); + return ! empty( $extra['wpgraphql_event'] ) ? esc_html( $extra['wpgraphql_event'] ) : $item->get_message(); + } + + /** + * Gets the event from extra. + * + * @return string The event + */ + public function get_process_id(DatabaseEntity $item): int { + $extra = $item->get_extra(); + return ! empty( $extra['process_id'] ) ? (int) $extra['process_id'] : 0; + } + + /** + * Gets the event from extra. + * + * @return string The event + */ + public function get_memory_usage(DatabaseEntity $item): string { + $extra = $item->get_extra(); + return ! empty( $extra['memory_peak_usage'] ) ? esc_html( $extra['memory_peak_usage'] ) : ''; + } +} diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php new file mode 100644 index 00000000..e6a58c9f --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php @@ -0,0 +1,30 @@ + + +
+

+
+ +
+ prepare_items(); + $list_table->display(); + ?> +
+
diff --git a/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php index e19f69ee..d26c36fd 100644 --- a/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php +++ b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php @@ -4,14 +4,27 @@ namespace WPGraphQL\Logging\Admin; -use WPGraphQL\Logging\Admin\View\Grid_Service; +use WPGraphQL\Logging\Admin\View\List\List_Table; +use WPGraphQL\Logging\Logger\Database\LogsRepository; +/** + * The view logs page class for WPGraphQL Logging. + * + * @package WPGraphQL\Logging + * + * @since 0.0.1 + */ class View_Logs_Page { + public const ADMIN_PAGE_SLUG = 'wpgraphql-logging-view'; - public const ADMIN_PAGE_SLUG = 'wpgraphql-logging'; - + /** + * The instance of the view logs page. + */ protected static ?View_Logs_Page $instance = null; + /** + * Initializes the view logs page. + */ public static function init(): ?View_Logs_Page { if ( ! current_user_can( 'manage_options' ) ) { return null; @@ -27,13 +40,19 @@ public static function init(): ?View_Logs_Page { return self::$instance; } + /** + * Sets up the view logs page. + */ public function setup(): void { - add_action( 'admin_menu', [ $this, 'register_settings_page' ], 10, 0); + add_action( 'admin_menu', [ $this, 'register_settings_page' ], 10, 0 ); } + /** + * Registers the settings page for the view logs. + */ public function register_settings_page(): void { - // Add submenu under GraphQL menu using the correct parent slug + // Add submenu under GraphQL menu using the correct parent slug. add_menu_page( esc_html__( 'GraphQL Logs', 'wpgraphql-logging' ), esc_html__( 'GraphQL Logs', 'wpgraphql-logging' ), @@ -53,12 +72,38 @@ public function register_settings_page(): void { ); } + /** + * Renders the admin page for the logs. + */ public function render_admin_page(): void { - // For now, just hello world - echo '

Hello World

'; + $action = isset( $_GET['action'] ) ? sanitize_text_field( $_GET['action'] ) : 'list'; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + switch ( $action ) { + case 'view': + $this->render_view_page(); + break; + default: + $this->render_list_page(); + break; + } + } + + /** + * Renders the list page for log entries. + */ + protected function render_list_page(): void { + // Variable required for list template. + $list_table = new List_Table( new LogsRepository() ); // @phpcs:ignore SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable + $list_template = apply_filters( + 'wpgraphql_logging_list_template', + __DIR__ . '/View/List/Templates/wpgraphql-logger-list.php' + ); + require_once $list_template; // @phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable + } - // Here is where we will render the grid - $grid_service = new Grid_Service(); - $grid_service->render(); + /** + * Renders the view page for a single log entry. + */ + protected function render_view_page(): void { + // Render the view page. } } diff --git a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php index 7385bd46..dd549b68 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php @@ -107,7 +107,7 @@ public static function create(string $channel, int $level, string $level_name, s * * @return self|null Returns an instance of DatabaseEntity if found, or null if not found. */ - public static function find(int $id): ?self { + public static function find_by_id(int $id): ?self { global $wpdb; $table_name = self::get_table_name(); @@ -121,6 +121,26 @@ public static function find(int $id): ?self { return self::create_from_db_row( $row ); } + /** + * Helper to populate an instance from a database row. + * + * @param array $row The database row to populate from. + * + * @return self The populated instance. + */ + public static function create_from_db_row(array $row): self { + $log = new self(); + $log->id = (int) $row['id']; + $log->channel = $row['channel']; + $log->level = (int) $row['level']; + $log->level_name = $row['level_name']; + $log->message = $row['message']; + $log->context = ( isset( $row['context'] ) && '' !== $row['context'] ) ? json_decode( $row['context'], true ) : []; + $log->extra = ( isset( $row['extra'] ) && '' !== $row['extra'] ) ? json_decode( $row['extra'], true ) : []; + $log->datetime = $row['datetime']; + return $log; + } + /** * Saves a new logging entity to the database. This is an insert-only operation. * @@ -182,6 +202,8 @@ public function get_level_name(): string { /** * Gets the message of the log entry. + * + * @return string The message of the log entry. */ public function get_message(): string { return $this->message; @@ -214,6 +236,43 @@ public function get_datetime(): string { return $this->datetime; } + /** + * Finds multiple log entries and returns them as an array. + * + * @param int $limit The maximum number of log entries to return. + * @param int $offset The offset for pagination. + * @param string $orderby The column to order by. + * @param string $order The order direction (ASC or DESC). + * + * @return array An array of DatabaseEntity instances, or an empty array if none found. + */ + public static function find_logs(int $limit, int $offset, string $orderby = 'id', string $order = 'DESC'): array { + global $wpdb; + $table_name = self::get_table_name(); + $orderby = esc_sql( $orderby ); + $order = esc_sql( $order ); + + $query = $wpdb->prepare( + "SELECT * FROM {$table_name} ORDER BY {$orderby} {$order} LIMIT %d, %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $offset, + $limit + ); + + // We do not want to cache as this is a paginated query. + $results = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared + + if ( empty( $results ) || ! is_array( $results ) ) { + return []; + } + + return array_map( + static function (array $row) { + return DatabaseEntity::create_from_db_row( $row ); + }, + $results + ); + } + /** * Gets the name of the logging table. */ @@ -297,24 +356,4 @@ protected static function sanitize_array_field(array $data): array { } return $data; } - - /** - * Helper to populate an instance from a database row. - * - * @param array $row The database row to populate from. - * - * @return self The populated instance. - */ - private static function create_from_db_row(array $row): self { - $log = new self(); - $log->id = (int) $row['id']; - $log->channel = $row['channel']; - $log->level = (int) $row['level']; - $log->level_name = $row['level_name']; - $log->message = $row['message']; - $log->context = ( isset( $row['context'] ) && '' !== $row['context'] ) ? json_decode( $row['context'], true ) : []; - $log->extra = ( isset( $row['extra'] ) && '' !== $row['extra'] ) ? json_decode( $row['extra'], true ) : []; - $log->datetime = $row['datetime']; - return $log; - } } diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php new file mode 100644 index 00000000..655392ce --- /dev/null +++ b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php @@ -0,0 +1,61 @@ + $args + * + * @return array + */ + public function get_logs(array $args = []): array { + global $wpdb; + $defaults = [ + 'number' => 100, + 'offset' => 0, + 'orderby' => 'id', + 'order' => 'DESC', + ]; + $args = wp_parse_args( $args, $defaults ); + + $orderby = esc_sql( $args['orderby'] ); + $order = esc_sql( $args['order'] ); + $limit = absint( $args['number'] ); + $offset = absint( $args['offset'] ); + + return DatabaseEntity::find_logs( $limit, $offset, $orderby, $order ); + } + + /** + * Get the total number of log entries. + * + * @return int The total number of log entries. + */ + public function get_log_count(): int { + $cache_key = 'wpgraphql_logs_count'; + $count = wp_cache_get( $cache_key ); + + if ( false === $count ) { + global $wpdb; + $table_name = DatabaseEntity::get_table_name(); + $count = $wpdb->get_var( $wpdb->prepare( // @phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + 'SELECT COUNT(*) FROM %i', + $table_name + ) ); + wp_cache_set( $cache_key, $count, '', 300 ); // Cache for 5 minutes. + } + + return (int) $count; + } +} From bfecdfa6e01cb5f2b2eadfc939c288d3b666d28e Mon Sep 17 00:00:00 2001 From: Theo <328805+theodesp@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:13:32 +0100 Subject: [PATCH 03/32] feat: implement view log by id page --- .../wpgraphql-logging/src/Admin/View/.gitkeep | 0 .../src/Admin/View/List/List_Table.php | 17 +++++-- .../List/Templates/wpgraphql-logger-view.php | 49 +++++++++++++++++++ .../src/Admin/View_Logs_Page.php | 22 ++++++++- .../src/Logger/Database/LogsRepository.php | 11 +++++ 5 files changed, 95 insertions(+), 4 deletions(-) delete mode 100644 plugins/wpgraphql-logging/src/Admin/View/.gitkeep create mode 100644 plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php diff --git a/plugins/wpgraphql-logging/src/Admin/View/.gitkeep b/plugins/wpgraphql-logging/src/Admin/View/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index 472e29fa..537dbe3b 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -168,11 +168,22 @@ public function column_cb( $item ): string { * @return string The rendered ID column or null. */ public function column_id( DatabaseEntity $item ): string { - $url = isset( $_REQUEST['page'] ) ? esc_attr( sanitize_text_field( $_REQUEST['page'] ) ) : ''; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + $url = \WPGraphQL\Logging\Admin\View_Logs_Page::ADMIN_PAGE_SLUG; $actions = [ - 'view' => sprintf( 'View', $url, 'view', $item->get_id() ), + 'view' => sprintf( + '%s', + esc_attr( $url ), + 'view', + $item->get_id(), + esc_html__( 'View', 'wpgraphql-logging' ) + ), ]; - return sprintf( '%1$s %2$s', $item->get_id(), $this->row_actions( $actions ) ); + + return sprintf( + '%1$s %2$s', + $item->get_id(), + $this->row_actions( $actions ) + ); } /** diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php new file mode 100644 index 00000000..e4a330aa --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php @@ -0,0 +1,49 @@ + +
+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
get_id() ); ?>
get_datetime() ); ?>
get_channel() ); ?>
get_level_name() ); ?>
get_message() ); ?>
get_context(), JSON_PRETTY_PRINT ) ); ?>
get_extra(), JSON_PRETTY_PRINT ) ); ?>
+ +

+ + + +

+
diff --git a/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php index d26c36fd..05b35721 100644 --- a/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php +++ b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php @@ -104,6 +104,26 @@ protected function render_list_page(): void { * Renders the view page for a single log entry. */ protected function render_view_page(): void { - // Render the view page. + $log_id = isset( $_GET['log'] ) ? absint( $_GET['log'] ) : 0; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( ! $log_id ) { + echo '

' . esc_html__( 'Invalid log ID.', 'wpgraphql-logging' ) . '

'; + return; + } + + $repository = new LogsRepository(); + $log = $repository->get_log( $log_id ); + + if ( ! $log ) { + echo '

' . esc_html__( 'Log not found.', 'wpgraphql-logging' ) . '

'; + return; + } + + $log_template = apply_filters( + 'wpgraphql_logging_view_template', + __DIR__ . '/View/List/Templates/wpgraphql-logger-view.php' + ); + + require_once $log_template; // @phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable } } diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php index 655392ce..2e769557 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php @@ -58,4 +58,15 @@ public function get_log_count(): int { return (int) $count; } + + /** + * Get a single log entry by ID. + * + * @param int $id The log entry ID. + * + * @return ?DatabaseEntity The log entry or null if not found. + */ + public function get_log( int $id ): ?DatabaseEntity { + return DatabaseEntity::find_by_id( $id ); + } } From 4d8b7f4aa45ad6fe92b766613ef18d7bc17a7185 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Thu, 4 Sep 2025 14:14:44 +0100 Subject: [PATCH 04/32] Fixed QA issues and also posts per page. --- plugins/wpgraphql-logging/composer.json | 4 +-- .../src/Admin/View/List/List_Table.php | 34 ++++++++++++------- .../src/Admin/View_Logs_Page.php | 13 +++++-- .../src/Logger/Database/DatabaseEntity.php | 10 +++--- .../src/Logger/Database/LogsRepository.php | 32 ++++++++++------- 5 files changed, 61 insertions(+), 32 deletions(-) diff --git a/plugins/wpgraphql-logging/composer.json b/plugins/wpgraphql-logging/composer.json index 11e22b28..d729773a 100644 --- a/plugins/wpgraphql-logging/composer.json +++ b/plugins/wpgraphql-logging/composer.json @@ -143,8 +143,8 @@ "phpstan": [ "vendor/bin/phpstan analyze --ansi --memory-limit=1G" ], - "php:psalm": "psalm --output-format=text --no-progress", - "php:psalm:fix": "psalm --alter --output-format=text --no-progress", + "php:psalm": "psalm --no-progress", + "php:psalm:fix": "psalm --alter --no-progress", "qa": "sh bin/local/run-qa.sh", "test": [ "sh bin/local/run-unit-tests.sh coverage", diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index 472e29fa..a5ddab6b 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -10,7 +10,7 @@ // Include the WP_List_Table class if not already loaded. if ( ! class_exists( 'WP_List_Table' ) ) { - require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; + require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; // @phpstan-ignore-line } /** @@ -23,6 +23,13 @@ * @since 0.0.1 */ class List_Table extends WP_List_Table { + /** + * Default number of items per page. + * + * @var int + */ + public const DEFAULT_PER_PAGE = 25; + /** * Constructor. * @@ -33,13 +40,15 @@ public function __construct( public readonly LogsRepository $repository, $args = [] ) { - parent::__construct( + $args = wp_parse_args( + $args, [ 'singular' => __( 'Log', 'wpgraphql-logging' ), 'plural' => __( 'Logs', 'wpgraphql-logging' ), 'ajax' => false, ] ); + parent::__construct( $args ); } /** @@ -58,7 +67,7 @@ public function prepare_items(): void { ); // @TODO - $per_page = $this->get_items_per_page( 'logs_per_page', 20 ); + $per_page = $this->get_items_per_page( 'logs_per_page', self::DEFAULT_PER_PAGE ); $current_page = $this->get_pagenum(); $total_items = $this->repository->get_log_count(); @@ -104,8 +113,8 @@ public function get_columns(): array { /** * Get the default column value for a log entry. * - * @param \WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. - * @param string $column_name The column name. + * @param mixed|\WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. + * @param string $column_name The column name. * * @phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded * @@ -144,15 +153,13 @@ public function column_default( $item, $column_name ): mixed { /** * Renders the checkbox column for a log entry. * - * @var ?\WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. - * - * @phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingAnyTypeHint + * @param mixed|\WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. * * @return string The rendered checkbox column or null. */ public function column_cb( $item ): string { if ( ! $item instanceof DatabaseEntity ) { - return null; + return ''; } return sprintf( '', @@ -168,11 +175,12 @@ public function column_cb( $item ): string { * @return string The rendered ID column or null. */ public function column_id( DatabaseEntity $item ): string { - $url = isset( $_REQUEST['page'] ) ? esc_attr( sanitize_text_field( $_REQUEST['page'] ) ) : ''; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + /** @psalm-suppress PossiblyInvalidArgument */ + $url = sanitize_text_field( $_REQUEST['page'] ?? '' ); // @phpcs:ignore WordPress.Security.NonceVerification.Recommended $actions = [ 'view' => sprintf( 'View', $url, 'view', $item->get_id() ), ]; - return sprintf( '%1$s %2$s', $item->get_id(), $this->row_actions( $actions ) ); + return sprintf( '%1$d %2$s', $item->get_id(), $this->row_actions( $actions ) ); } /** @@ -211,7 +219,9 @@ public function get_event(DatabaseEntity $item): string { /** * Gets the event from extra. * - * @return string The event + * @param \WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. + * + * @return int The event */ public function get_process_id(DatabaseEntity $item): int { $extra = $item->get_extra(); diff --git a/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php index d26c36fd..61eda49c 100644 --- a/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php +++ b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php @@ -15,12 +15,19 @@ * @since 0.0.1 */ class View_Logs_Page { + /** + * The admin page slug. + * + * @var string + */ public const ADMIN_PAGE_SLUG = 'wpgraphql-logging-view'; /** * The instance of the view logs page. + * + * @var self|null */ - protected static ?View_Logs_Page $instance = null; + protected static ?self $instance = null; /** * Initializes the view logs page. @@ -76,7 +83,9 @@ public function register_settings_page(): void { * Renders the admin page for the logs. */ public function render_admin_page(): void { - $action = isset( $_GET['action'] ) ? sanitize_text_field( $_GET['action'] ) : 'list'; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + /** @psalm-suppress PossiblyInvalidArgument */ + $action = sanitize_text_field( $_REQUEST['action'] ?? 'link' ); // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + switch ( $action ) { case 'view': $this->render_view_page(); diff --git a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php index dd549b68..c1fcdcb5 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php @@ -175,8 +175,8 @@ public function save(): int { /** * Gets the ID of the log entry. */ - public function get_id(): ?int { - return $this->id; + public function get_id(): int { + return (int) $this->id; } /** @@ -244,7 +244,7 @@ public function get_datetime(): string { * @param string $orderby The column to order by. * @param string $order The order direction (ASC or DESC). * - * @return array An array of DatabaseEntity instances, or an empty array if none found. + * @return array<\WPGraphQL\Logging\Logger\Database\DatabaseEntity> An array of DatabaseEntity instances, or an empty array if none found. */ public static function find_logs(int $limit, int $offset, string $orderby = 'id', string $order = 'DESC'): array { global $wpdb; @@ -253,7 +253,9 @@ public static function find_logs(int $limit, int $offset, string $orderby = 'id' $order = esc_sql( $order ); $query = $wpdb->prepare( - "SELECT * FROM {$table_name} ORDER BY {$orderby} {$order} LIMIT %d, %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "SELECT * FROM {$table_name} ORDER BY %s %s LIMIT %d, %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $orderby, + $order, $offset, $limit ); diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php index 655392ce..83ce3b10 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php @@ -17,7 +17,7 @@ class LogsRepository { /** * @param array $args * - * @return array + * @return array<\WPGraphQL\Logging\Logger\Database\DatabaseEntity> */ public function get_logs(array $args = []): array { global $wpdb; @@ -30,9 +30,15 @@ public function get_logs(array $args = []): array { $args = wp_parse_args( $args, $defaults ); $orderby = esc_sql( $args['orderby'] ); - $order = esc_sql( $args['order'] ); - $limit = absint( $args['number'] ); - $offset = absint( $args['offset'] ); + if ( ! is_string( $orderby ) || '' === $orderby ) { + $orderby = $defaults['orderby']; + } + $order = esc_sql( $args['order'] ); + if ( ! is_string( $order ) || '' === $order ) { + $order = $defaults['order']; + } + $limit = absint( $args['number'] ); + $offset = absint( $args['offset'] ); return DatabaseEntity::find_logs( $limit, $offset, $orderby, $order ); } @@ -46,16 +52,18 @@ public function get_log_count(): int { $cache_key = 'wpgraphql_logs_count'; $count = wp_cache_get( $cache_key ); - if ( false === $count ) { - global $wpdb; - $table_name = DatabaseEntity::get_table_name(); - $count = $wpdb->get_var( $wpdb->prepare( // @phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - 'SELECT COUNT(*) FROM %i', - $table_name - ) ); - wp_cache_set( $cache_key, $count, '', 300 ); // Cache for 5 minutes. + if ( is_int( $count ) ) { + return $count; } + global $wpdb; + $table_name = DatabaseEntity::get_table_name(); + $count = $wpdb->get_var( $wpdb->prepare( // @phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + 'SELECT COUNT(*) FROM %i', + $table_name + ) ); + wp_cache_set( $cache_key, $count, '', 300 ); + return (int) $count; } } From 87c68933221ed4895e536c3ea62a027dd797a475 Mon Sep 17 00:00:00 2001 From: Theo <328805+theodesp@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:44:56 +0100 Subject: [PATCH 05/32] feat: delete item and delete all log items --- .../src/Admin/View/List/List_Table.php | 29 ++++++++++++++++++ .../List/Templates/wpgraphql-logger-list.php | 4 ++- .../src/Logger/Database/LogsRepository.php | 30 +++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index 537dbe3b..b3b6c452 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -46,6 +46,7 @@ public function __construct( * Prepare items for display. */ public function prepare_items(): void { + $this->process_bulk_action(); $this->_column_headers = apply_filters( 'wpgraphql_logging_logs_table_column_headers', @@ -77,6 +78,34 @@ public function prepare_items(): void { ); } + /** + * Define bulk actions. + */ + public function get_bulk_actions(): array { + return [ + 'delete' => __( 'Delete Selected', 'wpgraphql-logging' ), + 'delete_all' => __( 'Delete All', 'wpgraphql-logging' ), + ]; + } + + /** + * Handle bulk actions. + */ + public function process_bulk_action(): void { + $repository = $this->repository; + + if ('delete' === $this->current_action() && !empty($_POST['log'])) { + $ids = array_map('absint', (array) $_POST['log']); + foreach ($ids as $id) { + $repository->delete($id); + } + } + + if ('delete_all' === $this->current_action()) { + $repository->delete_all(); + } + } + /** * Get the columns for the logs table. * diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php index e6a58c9f..e3aa5e75 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php @@ -16,7 +16,7 @@ exit; } ?> - +


@@ -24,7 +24,9 @@
prepare_items(); + $list_table->display_bulk_actions_top(); $list_table->display(); + $list_table->display_bulk_actions_bottom(); ?>
diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php index 2e769557..4788047d 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php @@ -69,4 +69,34 @@ public function get_log_count(): int { public function get_log( int $id ): ?DatabaseEntity { return DatabaseEntity::find_by_id( $id ); } + + /** + * Delete a single log entry by ID. + * + * @param int $id + * @return bool + */ + public function delete(int $id): bool { + global $wpdb; + $table_name = DatabaseEntity::get_table_name(); + + if ($id <= 0) { + return false; + } + + $result = $wpdb->delete($table_name, ['id' => $id], ['%d']); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + return false !== $result; + } + + /** + * Delete all log entries. + * + * @return bool True if all logs were deleted successfully, false otherwise. + */ + public function delete_all(): bool { + global $wpdb; + $table_name = DatabaseEntity::get_table_name(); + $result = $wpdb->query("TRUNCATE TABLE {$table_name}"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + return false !== $result; + } } From 94c1390eba3410df865c67b508d54d9953cc8ffa Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Thu, 4 Sep 2025 14:46:32 +0100 Subject: [PATCH 06/32] Fixing small QA issues. --- .../View/List/Templates/wpgraphql-logger-view.php | 11 +++++++---- .../src/Logger/Database/LogsRepository.php | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php index e4a330aa..dab1c65c 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php @@ -1,4 +1,7 @@ - get_id() ); ?> + get_id() ); ?> - get_datetime() ); ?> + get_datetime() ?? '' ); ?> - get_channel() ); ?> + get_channel() ?? '' ); ?> - get_level_name() ); ?> + get_level_name() ?? '' ); ?> diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php index e4d50f3c..a49bcd30 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php @@ -72,7 +72,7 @@ public function get_log_count(): int { * * @param int $id The log entry ID. * - * @return ?DatabaseEntity The log entry or null if not found. + * @return ?\WPGraphQL\Logging\Logger\Database\DatabaseEntity The log entry or null if not found. */ public function get_log( int $id ): ?DatabaseEntity { return DatabaseEntity::find_by_id( $id ); From f242763214571f7cac86f8852d9214d695ebf9b7 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Thu, 4 Sep 2025 14:58:25 +0100 Subject: [PATCH 07/32] Fixed QA errors. --- plugins/wpgraphql-logging/psalm.xml | 6 +++ .../src/Admin/View/List/List_Table.php | 12 +++-- .../List/Templates/wpgraphql-logger-list.php | 6 +-- .../List/Templates/wpgraphql-logger-view.php | 12 ++--- .../src/Admin/View_Logs_Page.php | 4 +- .../src/Logger/Database/LogsRepository.php | 49 +++++++++---------- 6 files changed, 48 insertions(+), 41 deletions(-) diff --git a/plugins/wpgraphql-logging/psalm.xml b/plugins/wpgraphql-logging/psalm.xml index 1bfaf14c..b10c2a22 100644 --- a/plugins/wpgraphql-logging/psalm.xml +++ b/plugins/wpgraphql-logging/psalm.xml @@ -36,5 +36,11 @@ + + + + + + diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index f6fbdd16..c68cc18b 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -89,6 +89,8 @@ public function prepare_items(): void { /** * Define bulk actions. + * + * @return array The bulk actions. */ public function get_bulk_actions(): array { return [ @@ -103,14 +105,14 @@ public function get_bulk_actions(): array { public function process_bulk_action(): void { $repository = $this->repository; - if ('delete' === $this->current_action() && !empty($_POST['log'])) { - $ids = array_map('absint', (array) $_POST['log']); - foreach ($ids as $id) { - $repository->delete($id); + if ( 'delete' === $this->current_action() && ! empty( $_POST['log'] ) ) { // @phpcs:ignore WordPress.Security.NonceVerification.Missing + $ids = array_map( 'absint', (array) $_POST['log'] ); // @phpcs:ignore WordPress.Security.NonceVerification.Missing + foreach ( $ids as $id ) { + $repository->delete( $id ); } } - if ('delete_all' === $this->current_action()) { + if ( 'delete_all' === $this->current_action() ) { $repository->delete_all(); } } diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php index e3aa5e75..c8af8237 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php @@ -16,7 +16,7 @@ exit; } ?> - +


@@ -24,9 +24,9 @@
prepare_items(); - $list_table->display_bulk_actions_top(); + // $list_table->display_bulk_actions_top(); $list_table->display(); - $list_table->display_bulk_actions_bottom(); + // $list_table->display_bulk_actions_bottom(); ?>
diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php index dab1c65c..775cf68a 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php @@ -19,27 +19,27 @@ - get_datetime() ?? '' ); ?> + get_datetime() ); ?> - get_channel() ?? '' ); ?> + get_channel() ); ?> - get_level_name() ?? '' ); ?> + get_level_name() ); ?> - get_message() ); ?> + get_message() ); ?> -
get_context(), JSON_PRETTY_PRINT ) ); ?>
+
get_context(), JSON_PRETTY_PRINT ) ); ?>
-
get_extra(), JSON_PRETTY_PRINT ) ); ?>
+
get_extra(), JSON_PRETTY_PRINT ) ); ?>
diff --git a/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php index 523fba97..2fc0a936 100644 --- a/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php +++ b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php @@ -115,7 +115,7 @@ protected function render_list_page(): void { protected function render_view_page(): void { $log_id = isset( $_GET['log'] ) ? absint( $_GET['log'] ) : 0; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended - if ( ! $log_id ) { + if ( 0 === (int) $log_id ) { echo '

' . esc_html__( 'Invalid log ID.', 'wpgraphql-logging' ) . '

'; return; } @@ -123,7 +123,7 @@ protected function render_view_page(): void { $repository = new LogsRepository(); $log = $repository->get_log( $log_id ); - if ( ! $log ) { + if ( is_null( $log ) ) { echo '

' . esc_html__( 'Log not found.', 'wpgraphql-logging' ) . '

'; return; } diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php index 49dc4ccd..2a9defdf 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php @@ -79,32 +79,31 @@ public function get_log( int $id ): ?DatabaseEntity { } /** - * Delete a single log entry by ID. - * - * @param int $id - * @return bool - */ - public function delete(int $id): bool { - global $wpdb; - $table_name = DatabaseEntity::get_table_name(); + * Delete a single log entry by ID. + * + * @param int $id + */ + public function delete(int $id): bool { + global $wpdb; + $table_name = DatabaseEntity::get_table_name(); - if ($id <= 0) { - return false; - } + if ( $id <= 0 ) { + return false; + } - $result = $wpdb->delete($table_name, ['id' => $id], ['%d']); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - return false !== $result; - } + $result = $wpdb->delete( $table_name, [ 'id' => $id ], [ '%d' ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + return false !== $result; + } - /** - * Delete all log entries. - * - * @return bool True if all logs were deleted successfully, false otherwise. - */ - public function delete_all(): bool { - global $wpdb; - $table_name = DatabaseEntity::get_table_name(); - $result = $wpdb->query("TRUNCATE TABLE {$table_name}"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - return false !== $result; - } + /** + * Delete all log entries. + * + * @return bool True if all logs were deleted successfully, false otherwise. + */ + public function delete_all(): bool { + global $wpdb; + $table_name = DatabaseEntity::get_table_name(); + $result = $wpdb->query( $wpdb->prepare( 'TRUNCATE TABLE %s', $table_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + return false !== $result; + } } From 883cae751549bd07460332adaf38701e8d8487b0 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Thu, 4 Sep 2025 17:32:11 +0100 Subject: [PATCH 08/32] Added request headers to the log. Displayed this in the admin view. --- .../src/Admin/View/List/List_Table.php | 37 +++++++++++-- .../List/Templates/wpgraphql-logger-list.php | 2 - .../src/Logger/Database/DatabaseEntity.php | 7 ++- .../src/Logger/LoggerService.php | 2 + .../Processors/RequestHeadersProcessor.php | 52 +++++++++++++++++++ 5 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 plugins/wpgraphql-logging/src/Logger/Processors/RequestHeadersProcessor.php diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index c68cc18b..7de83bdb 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -134,8 +134,7 @@ public function get_columns(): array { 'level_name' => __( 'Level Name', 'wpgraphql-logging' ), 'event' => __( 'Event', 'wpgraphql-logging' ), 'process_id' => __( 'Process ID', 'wpgraphql-logging' ), - 'headers' => __( 'Headers', 'wpgraphql-logging' ), - 'time' => __( 'Time', 'wpgraphql-logging' ), + 'request_headers' => __( 'Headers', 'wpgraphql-logging' ), 'memory_usage' => __( 'Memory Usage', 'wpgraphql-logging' ), ] ); @@ -175,6 +174,8 @@ public function column_default( $item, $column_name ): mixed { return $this->get_memory_usage( $item ); case 'wpgraphql_query': return $this->get_query( $item ); + case 'request_headers': + return $this->get_request_headers( $item ); default: // Users can add their own custom columns and render functionality. return apply_filters( 'wpgraphql_logging_logs_table_column_value', '', $item, $column_name ); @@ -243,7 +244,18 @@ public function column_query( DatabaseEntity $item ): ?string { */ public function get_query(DatabaseEntity $item): string { $extra = $item->get_extra(); - return ! empty( $extra['wpgraphql_query'] ) ? esc_html( $extra['wpgraphql_query'] ) : ''; + $query = ! empty( $extra['wpgraphql_query'] ) ? esc_html( $extra['wpgraphql_query'] ) : ''; + return '
' . esc_html( $query ) . '
'; } /** @@ -278,4 +290,23 @@ public function get_memory_usage(DatabaseEntity $item): string { $extra = $item->get_extra(); return ! empty( $extra['memory_peak_usage'] ) ? esc_html( $extra['memory_peak_usage'] ) : ''; } + + /** + * Gets the request headers from extra. + * + * @return string The event + */ + public function get_request_headers(DatabaseEntity $item): string { + $extra = $item->get_extra(); + $request_headers = $extra['request_headers'] ?? []; + if ( empty( $request_headers ) || ! is_array( $request_headers ) ) { + return ''; + } + + $formatted_request_headers = wp_json_encode( $request_headers, JSON_PRETTY_PRINT ); + if ( false === $formatted_request_headers ) { + return ''; + } + return '
' . esc_html( $formatted_request_headers ) . '
'; + } } diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php index c8af8237..e6a58c9f 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php @@ -24,9 +24,7 @@
prepare_items(); - // $list_table->display_bulk_actions_top(); $list_table->display(); - // $list_table->display_bulk_actions_bottom(); ?>
diff --git a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php index c1fcdcb5..e0b338e9 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php @@ -249,13 +249,12 @@ public function get_datetime(): string { public static function find_logs(int $limit, int $offset, string $orderby = 'id', string $order = 'DESC'): array { global $wpdb; $table_name = self::get_table_name(); + $order = esc_sql( strtoupper( $order ) ); $orderby = esc_sql( $orderby ); - $order = esc_sql( $order ); + /** @psalm-suppress PossiblyInvalidCast */ $query = $wpdb->prepare( - "SELECT * FROM {$table_name} ORDER BY %s %s LIMIT %d, %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $orderby, - $order, + "SELECT * FROM {$table_name} ORDER BY {$orderby} {$order} LIMIT %d, %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $offset, $limit ); diff --git a/plugins/wpgraphql-logging/src/Logger/LoggerService.php b/plugins/wpgraphql-logging/src/Logger/LoggerService.php index 459c1e02..bdc7d35d 100644 --- a/plugins/wpgraphql-logging/src/Logger/LoggerService.php +++ b/plugins/wpgraphql-logging/src/Logger/LoggerService.php @@ -12,6 +12,7 @@ use Monolog\Processor\ProcessorInterface; use Monolog\Processor\WebProcessor; use WPGraphQL\Logging\Logger\Handlers\WordPressDatabaseHandler; +use WPGraphQL\Logging\Logger\Processors\RequestHeadersProcessor; use WPGraphQL\Logging\Logger\Processors\WPGraphQLQueryProcessor; /** @@ -225,6 +226,7 @@ public static function get_default_processors(): array { new WebProcessor(), // Logs web request data. e.g. IP address, request method, URI, etc. new ProcessIdProcessor(), // Logs the process ID. new WPGraphQLQueryProcessor(), // Custom processor to capture GraphQL request data. + new RequestHeadersProcessor(), // Custom processor to capture request headers. ]; // Filter for users to add their own processors. diff --git a/plugins/wpgraphql-logging/src/Logger/Processors/RequestHeadersProcessor.php b/plugins/wpgraphql-logging/src/Logger/Processors/RequestHeadersProcessor.php new file mode 100644 index 00000000..006107e2 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Logger/Processors/RequestHeadersProcessor.php @@ -0,0 +1,52 @@ + The request headers. + */ + private function get_headers(): array { + $headers = []; + foreach ( $_SERVER as $key => $value ) { + if ( ! is_string( $value ) || empty( $value ) ) { + continue; + } + $header_key = substr( $key, 5 ); + $header_key = str_replace( '_', '-', $header_key ); + $header_key = ucwords( strtolower( sanitize_text_field( (string) $header_key ) ), '-' ); + $headers[ $header_key ] = sanitize_text_field( $value ); + } + + return $headers; + } + + /** + * This method is called for each log record. It adds the captured + * request headers to the record's 'extra' array. + * + * @param \Monolog\LogRecord $record The log record to process. + * + * @return \Monolog\LogRecord The processed log record. + */ + public function __invoke( LogRecord $record ): LogRecord { + $record->extra['request_headers'] = $this->get_headers(); + + return $record; + } +} From 603efab956b513d230943472c5324409ab1cd1b3 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Thu, 4 Sep 2025 17:47:03 +0100 Subject: [PATCH 09/32] Small fixes to list table. Allow developers to fully modify any column in the grid. --- .../src/Admin/View/List/List_Table.php | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index 7de83bdb..a44a85b4 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -155,31 +155,42 @@ public function column_default( $item, $column_name ): mixed { return null; } + $value = ''; + switch ( $column_name ) { case 'date': - return $item->get_datetime(); + $value = $item->get_datetime(); + break; case 'channel': - return $item->get_channel(); + $value = $item->get_channel(); + break; case 'level': - return $item->get_level(); + $value = $item->get_level(); + break; case 'level_name': - return $item->get_level_name(); + $value = $item->get_level_name(); + break; case 'message': - return $item->get_message(); + $value = $item->get_message(); + break; case 'event': - return $this->get_event( $item ); + $value = $this->get_event( $item ); + break; case 'process_id': - return $this->get_process_id( $item ); + $value = $this->get_process_id( $item ); + break; case 'memory_usage': - return $this->get_memory_usage( $item ); + $value = $this->get_memory_usage( $item ); + break; case 'wpgraphql_query': - return $this->get_query( $item ); + $value = $this->get_query( $item ); + break; case 'request_headers': - return $this->get_request_headers( $item ); - default: - // Users can add their own custom columns and render functionality. - return apply_filters( 'wpgraphql_logging_logs_table_column_value', '', $item, $column_name ); + $value = $this->get_request_headers( $item ); + break; } + + return apply_filters( 'wpgraphql_logging_logs_table_column_value', $value, $item, $column_name ); } /** @@ -245,17 +256,7 @@ public function column_query( DatabaseEntity $item ): ?string { public function get_query(DatabaseEntity $item): string { $extra = $item->get_extra(); $query = ! empty( $extra['wpgraphql_query'] ) ? esc_html( $extra['wpgraphql_query'] ) : ''; - return '
' . esc_html( $query ) . '
'; + return '
' . esc_html( $query ) . '
'; } /** From 90d64a9e14dc756c82942bb857e14b9c9cbeef9d Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Thu, 4 Sep 2025 19:02:15 +0100 Subject: [PATCH 10/32] Added sorting to the admin grid. --- .../src/Admin/View/List/List_Table.php | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index a44a85b4..de6922b4 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -53,6 +53,10 @@ public function __construct( /** * Prepare items for display. + * + * @phpcs:disable WordPress.Security.NonceVerification.Recommended + * + * @psalm-suppress PossiblyInvalidCast */ public function prepare_items(): void { $this->process_bulk_action(); @@ -67,7 +71,6 @@ public function prepare_items(): void { ] ); - // @TODO $per_page = $this->get_items_per_page( 'logs_per_page', self::DEFAULT_PER_PAGE ); $current_page = $this->get_pagenum(); $total_items = $this->repository->get_log_count(); @@ -79,12 +82,21 @@ public function prepare_items(): void { ] ); - $this->items = $this->repository->get_logs( - [ - 'number' => $per_page, - 'offset' => ( $current_page - 1 ) * $per_page, - ] - ); + $args = [ + 'number' => $per_page, + 'offset' => ( $current_page - 1 ) * $per_page, + ]; + + if ( array_key_exists( 'orderby', $_REQUEST ) ) { + $args['orderby'] = sanitize_text_field( wp_unslash( (string) $_REQUEST['orderby'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + + if ( array_key_exists( 'order', $_REQUEST ) ) { + $args['order'] = sanitize_text_field( wp_unslash( (string) $_REQUEST['order'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + + + $this->items = $this->repository->get_logs( apply_filters( 'wpgraphql_logging_logs_table_query_args', $args ) ); } /** @@ -308,6 +320,20 @@ public function get_request_headers(DatabaseEntity $item): string { if ( false === $formatted_request_headers ) { return ''; } - return '
' . esc_html( $formatted_request_headers ) . '
'; + return '
' . esc_html( $formatted_request_headers ) . '
'; + } + + /** + * Get a list of sortable columns. + * + * @return array The sortable columns. + */ + protected function get_sortable_columns(): array { + return [ + 'id' => [ 'id', false ], + 'date' => [ 'datetime', true ], + 'level' => [ 'level', false ], + 'level_name' => [ 'level_name', false ], + ]; } } From 37da3b80cb115a3281d848b4d6f5b1f3141a88ec Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Thu, 4 Sep 2025 20:39:17 +0100 Subject: [PATCH 11/32] Added filters for date and level Allows users to filter by date and level. --- plugins/wpgraphql-logging/psalm.xml | 6 ++ .../src/Admin/View/List/List_Table.php | 56 ++++++++++++++++++- .../Templates/wpgraphql-logger-filters.php | 56 +++++++++++++++++++ .../List/Templates/wpgraphql-logger-list.php | 2 +- .../src/Logger/Database/DatabaseEntity.php | 28 +++++++--- .../src/Logger/Database/LogsRepository.php | 13 ++++- 6 files changed, 146 insertions(+), 15 deletions(-) create mode 100644 plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-filters.php diff --git a/plugins/wpgraphql-logging/psalm.xml b/plugins/wpgraphql-logging/psalm.xml index b10c2a22..543f06ee 100644 --- a/plugins/wpgraphql-logging/psalm.xml +++ b/plugins/wpgraphql-logging/psalm.xml @@ -42,5 +42,11 @@ + + + + + + diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index de6922b4..f08d67c7 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -94,9 +94,9 @@ public function prepare_items(): void { if ( array_key_exists( 'order', $_REQUEST ) ) { $args['order'] = sanitize_text_field( wp_unslash( (string) $_REQUEST['order'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } - - - $this->items = $this->repository->get_logs( apply_filters( 'wpgraphql_logging_logs_table_query_args', $args ) ); + /** @psalm-suppress InvalidArgument */ + $args['where'] = $this->process_where( $_REQUEST ); + $this->items = $this->repository->get_logs( apply_filters( 'wpgraphql_logging_logs_table_query_args', $args ) ); } /** @@ -323,6 +323,41 @@ public function get_request_headers(DatabaseEntity $item): string { return '
' . esc_html( $formatted_request_headers ) . '
'; } + /** + * Process the where clauses for filtering. + * + * @param array $request The request data. + * + * @return array The where clauses. + */ + protected function process_where(array $request): array { + $where_clauses = []; + + if ( ! empty( $request['wpgraphql_logging_nonce'] ) && false === wp_verify_nonce( $request['wpgraphql_logging_nonce'], 'wpgraphql_logging_filter' ) ) { + return []; + } + + if ( ! empty( $request['level_filter'] ) ) { + $level = sanitize_text_field( wp_unslash( (string) $request['level_filter'] ) ); + $where_clauses[] = "level_name = '" . $level . "'"; + } + + if ( ! empty( $request['start_date'] ) ) { + $start_date = sanitize_text_field( $request['start_date'] ); + $date = new \DateTime( $start_date ); + $where_clauses[] = "datetime >= '" . $date->format( 'Y-m-d H:i:s' ) . "'"; + } + + if ( ! empty( $request['end_date'] ) ) { + $end_date = sanitize_text_field( $request['end_date'] ); + $date = new \DateTime( $end_date ); + $where_clauses[] = "datetime <= '" . $date->format( 'Y-m-d H:i:s' ) . "'"; + } + + // Allow developers to modify the where clauses. + return apply_filters( 'wpgraphql_logging_logs_table_where_clauses', $where_clauses, $request ); + } + /** * Get a list of sortable columns. * @@ -336,4 +371,19 @@ protected function get_sortable_columns(): array { 'level_name' => [ 'level_name', false ], ]; } + + /** + * Render extra table navigation controls. + * + * @param string $which The location of the nav ('top' or 'bottom'). + */ + protected function extra_tablenav( $which ): void { + + // Only display above the table. + if ( 'top' !== $which ) { + return; + } + $template = apply_filters( 'wpgraphql_logging_filters_template', __DIR__ . '/Templates/wpgraphql-logger-filters.php' ); + require_once $template; // @phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable + } } diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-filters.php b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-filters.php new file mode 100644 index 00000000..c52b3037 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-filters.php @@ -0,0 +1,56 @@ + +
+ + + + + + +
diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php index e6a58c9f..398ed9dc 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php @@ -3,7 +3,7 @@ declare(strict_types=1); /** - * Webhooks list view template using WP_List_Table. + * Logs list view template using WP_List_Table. * * @package WPGraphQL\Logger\Admin\View\List\Templates * diff --git a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php index e0b338e9..34926990 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php @@ -239,22 +239,34 @@ public function get_datetime(): string { /** * Finds multiple log entries and returns them as an array. * - * @param int $limit The maximum number of log entries to return. - * @param int $offset The offset for pagination. - * @param string $orderby The column to order by. - * @param string $order The order direction (ASC or DESC). + * @param int $limit The maximum number of log entries to return. + * @param int $offset The offset for pagination. + * @param array $where_clauses Optional. Additional WHERE conditions. + * @param string $orderby The column to order by. + * @param string $order The order direction (ASC or DESC). * * @return array<\WPGraphQL\Logging\Logger\Database\DatabaseEntity> An array of DatabaseEntity instances, or an empty array if none found. */ - public static function find_logs(int $limit, int $offset, string $orderby = 'id', string $order = 'DESC'): array { + public static function find_logs(int $limit, int $offset, array $where_clauses = [], string $orderby = 'id', string $order = 'DESC'): array { global $wpdb; $table_name = self::get_table_name(); - $order = esc_sql( strtoupper( $order ) ); - $orderby = esc_sql( $orderby ); + $order = sanitize_text_field( strtoupper( $order ) ); + $orderby = sanitize_text_field( $orderby ); + + $where = ''; + foreach ( $where_clauses as $clause ) { + if ( '' !== $where ) { + $where .= ' AND '; + } + $where .= (string) $clause; + } + if ( '' !== $where ) { + $where = 'WHERE ' . $where; + } /** @psalm-suppress PossiblyInvalidCast */ $query = $wpdb->prepare( - "SELECT * FROM {$table_name} ORDER BY {$orderby} {$order} LIMIT %d, %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "SELECT * FROM {$table_name} {$where} ORDER BY {$orderby} {$order} LIMIT %d, %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $offset, $limit ); diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php index 2a9defdf..54b2017b 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php @@ -26,21 +26,28 @@ public function get_logs(array $args = []): array { 'offset' => 0, 'orderby' => 'id', 'order' => 'DESC', + 'where' => [], ]; $args = wp_parse_args( $args, $defaults ); - $orderby = esc_sql( $args['orderby'] ); + $orderby = $args['orderby']; if ( ! is_string( $orderby ) || '' === $orderby ) { $orderby = $defaults['orderby']; } - $order = esc_sql( $args['order'] ); + $order = $args['order']; if ( ! is_string( $order ) || '' === $order ) { $order = $defaults['order']; } + $where = $args['where']; + if ( ! is_array( $where ) ) { + $where = $defaults['where']; + } + $limit = absint( $args['number'] ); $offset = absint( $args['offset'] ); - return DatabaseEntity::find_logs( $limit, $offset, $orderby, $order ); + + return DatabaseEntity::find_logs( $limit, $offset, $where, $orderby, $order ); } /** From 8c80c1ea20bbff8e024285e930aafe049d0a1b90 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Thu, 4 Sep 2025 21:25:16 +0100 Subject: [PATCH 12/32] Fixed count when filters are applied. --- .../src/Admin/View/List/List_Table.php | 10 +++--- .../Templates/wpgraphql-logger-filters.php | 6 ++-- .../src/Logger/Database/LogsRepository.php | 33 +++++++++++-------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index f08d67c7..36df148a 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -73,7 +73,9 @@ public function prepare_items(): void { $per_page = $this->get_items_per_page( 'logs_per_page', self::DEFAULT_PER_PAGE ); $current_page = $this->get_pagenum(); - $total_items = $this->repository->get_log_count(); + /** @psalm-suppress InvalidArgument */ + $where = $this->process_where( $_REQUEST ); + $total_items = $this->repository->get_log_count( $where ); $this->set_pagination_args( [ @@ -94,9 +96,9 @@ public function prepare_items(): void { if ( array_key_exists( 'order', $_REQUEST ) ) { $args['order'] = sanitize_text_field( wp_unslash( (string) $_REQUEST['order'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } - /** @psalm-suppress InvalidArgument */ - $args['where'] = $this->process_where( $_REQUEST ); - $this->items = $this->repository->get_logs( apply_filters( 'wpgraphql_logging_logs_table_query_args', $args ) ); + $args['where'] = $where; + + $this->items = $this->repository->get_logs( apply_filters( 'wpgraphql_logging_logs_table_query_args', $args ) ); } /** diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-filters.php b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-filters.php index c52b3037..7cf7d50c 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-filters.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-filters.php @@ -27,7 +27,7 @@ $wpgraphql_logging_current_end_date = ''; // Verify nonce before processing form data. - if ( ! empty( $request['wpgraphql_logging_nonce'] ) && false === wp_verify_nonce( $request['wpgraphql_logging_nonce'], 'wpgraphql_logging_filter' ) ) { + if ( isset( $_REQUEST['wpgraphql_logging_nonce'] ) && (bool) wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['wpgraphql_logging_nonce'] ) ), 'wpgraphql_logging_filter' ) ) { $wpgraphql_logging_current_level = isset( $_REQUEST['level_filter'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['level_filter'] ) ) : ''; $wpgraphql_logging_current_start_date = isset( $_REQUEST['start_date'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['start_date'] ) ) : ''; $wpgraphql_logging_current_end_date = isset( $_REQUEST['end_date'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['end_date'] ) ) : ''; @@ -52,5 +52,7 @@ - + + + diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php index 54b2017b..8f860b8b 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php @@ -53,25 +53,32 @@ public function get_logs(array $args = []): array { /** * Get the total number of log entries. * + * @param array $where_clauses Array of where clauses to filter the count. + * + * @phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching + * * @return int The total number of log entries. */ - public function get_log_count(): int { - $cache_key = 'wpgraphql_logs_count'; - $count = wp_cache_get( $cache_key ); + public function get_log_count(array $where_clauses): int { + global $wpdb; + $table_name = DatabaseEntity::get_table_name(); - if ( is_int( $count ) ) { - return $count; + if (empty($where_clauses)) { + return (int) $wpdb->get_var( $wpdb->prepare( // @phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + 'SELECT COUNT(*) FROM %i', + $table_name + ) ); } - global $wpdb; - $table_name = DatabaseEntity::get_table_name(); - $count = $wpdb->get_var( $wpdb->prepare( // @phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - 'SELECT COUNT(*) FROM %i', - $table_name - ) ); - wp_cache_set( $cache_key, $count, '', 300 ); + $where = ''; + foreach ( $where_clauses as $clause ) { + if ( '' !== $where ) { + $where .= ' AND '; + } + $where .= (string) $clause; + } - return (int) $count; + return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table_name} WHERE {$where}" ); // @phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** From 93a45ae0a8427325882e5a59dd42751c154143de Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 5 Sep 2025 10:34:24 +0100 Subject: [PATCH 13/32] Updated filters to be part of the $_GET request so that the parameters are passed when sorting and using pagination. --- .../src/Admin/View/List/List_Table.php | 5 +- .../Templates/wpgraphql-logger-filters.php | 9 +-- .../src/Admin/View_Logs_Page.php | 66 ++++++++++++++++++- .../src/Logger/Database/LogsRepository.php | 2 +- 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index 36df148a..44229aca 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -74,8 +74,9 @@ public function prepare_items(): void { $per_page = $this->get_items_per_page( 'logs_per_page', self::DEFAULT_PER_PAGE ); $current_page = $this->get_pagenum(); /** @psalm-suppress InvalidArgument */ - $where = $this->process_where( $_REQUEST ); - $total_items = $this->repository->get_log_count( $where ); + /** @psalm-suppress InvalidArgument */ + $where = $this->process_where( $_REQUEST ); + $total_items = $this->repository->get_log_count( $where ); $this->set_pagination_args( [ diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-filters.php b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-filters.php index 7cf7d50c..57103c07 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-filters.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-filters.php @@ -26,12 +26,9 @@ $wpgraphql_logging_current_start_date = ''; $wpgraphql_logging_current_end_date = ''; - // Verify nonce before processing form data. - if ( isset( $_REQUEST['wpgraphql_logging_nonce'] ) && (bool) wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['wpgraphql_logging_nonce'] ) ), 'wpgraphql_logging_filter' ) ) { - $wpgraphql_logging_current_level = isset( $_REQUEST['level_filter'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['level_filter'] ) ) : ''; - $wpgraphql_logging_current_start_date = isset( $_REQUEST['start_date'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['start_date'] ) ) : ''; - $wpgraphql_logging_current_end_date = isset( $_REQUEST['end_date'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['end_date'] ) ) : ''; - } + $wpgraphql_logging_current_level = isset( $_GET['level_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['level_filter'] ) ) : ''; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + $wpgraphql_logging_current_start_date = isset( $_GET['start_date'] ) ? sanitize_text_field( wp_unslash( $_GET['start_date'] ) ) : ''; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + $wpgraphql_logging_current_end_date = isset( $_GET['end_date'] ) ? sanitize_text_field( wp_unslash( $_GET['end_date'] ) ) : ''; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended /** * Log levels for filtering. diff --git a/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php index 2fc0a936..5830d5ae 100644 --- a/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php +++ b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php @@ -60,7 +60,7 @@ public function setup(): void { public function register_settings_page(): void { // Add submenu under GraphQL menu using the correct parent slug. - add_menu_page( + $menu_page = add_menu_page( esc_html__( 'GraphQL Logs', 'wpgraphql-logging' ), esc_html__( 'GraphQL Logs', 'wpgraphql-logging' ), 'manage_options', @@ -77,6 +77,9 @@ public function register_settings_page(): void { self::ADMIN_PAGE_SLUG, [ $this, 'render_admin_page' ] ); + + // Updates the list table when filters are applied. + add_action( 'load-' . $menu_page, [ $this, 'process_filters_redirect' ], 10, 0 ); } /** @@ -96,6 +99,67 @@ public function render_admin_page(): void { } } + /** + * Process filter form submission and redirect to a GET request. + * This runs before any HTML is output. + */ + public function process_filters_redirect(): void { + // Handle POST from filter form and redirect to GET. + $nonce = $this->get_post_value( 'wpgraphql_logging_nonce' ); + if ( ! is_string( $nonce ) ) { + return; + } + + // Verify nonce for security. + if ( false === wp_verify_nonce( $nonce, 'wpgraphql_logging_filter' ) ) { + return; + } + + $redirect_url = menu_page_url( self::ADMIN_PAGE_SLUG, false ); + + $possible_filters = [ + 'start_date', + 'end_date', + 'level_filter', + 'orderby', + 'order', + ]; + $filters = []; + foreach ( $possible_filters as $key ) { + $value = $this->get_post_value( $key ); + if ( null !== $value ) { + $filters[ $key ] = $value; + } + } + + $redirect_url = add_query_arg( array_filter( $filters, static function ( $value ) { + return '' !== $value; + } ), $redirect_url ); + $redirect_url = apply_filters( 'wpgraphql_logging_filter_redirect_url', $redirect_url, $filters ); + + wp_safe_redirect( $redirect_url ); + exit; + } + + /** + * Retrieves and sanitizes a value from the $_POST superglobal. + * + * @param string $key The key to retrieve from $_POST. + * + * @return string|null The sanitized value or null if not set or invalid. + */ + protected function get_post_value(string $key): ?string { + $value = $_POST[ $key ] ?? null; // @phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + if ( ! is_string( $value ) || '' === $value ) { + return null; + } + $value = wp_unslash( $value ); + if ( ! is_string( $value ) ) { + return null; + } + return sanitize_text_field( $value ); + } + /** * Renders the list page for log entries. */ diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php index 8f860b8b..4ea83ae0 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php @@ -63,7 +63,7 @@ public function get_log_count(array $where_clauses): int { global $wpdb; $table_name = DatabaseEntity::get_table_name(); - if (empty($where_clauses)) { + if ( empty( $where_clauses ) ) { return (int) $wpdb->get_var( $wpdb->prepare( // @phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 'SELECT COUNT(*) FROM %i', $table_name From 99e25296348bf1d43c3d52e4e4999448bcfa4147 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 5 Sep 2025 15:33:58 +0100 Subject: [PATCH 14/32] Add CSV download for individual log entries Introduces a Download_Log_Service using league/csv to export single log entries as CSV files. Adds a 'Download' action to the log list and view pages, updates admin page logic to handle download requests, and updates dependencies to include league/csv. --- plugins/wpgraphql-logging/composer.json | 1 + plugins/wpgraphql-logging/composer.lock | 93 ++++++++++++++++++- .../View/Download/Download_Log_Service.php | 82 ++++++++++++++++ .../src/Admin/View/List/List_Table.php | 9 +- .../List/Templates/wpgraphql-logger-view.php | 20 +++- .../src/Admin/View_Logs_Page.php | 32 ++++++- 6 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 plugins/wpgraphql-logging/src/Admin/View/Download/Download_Log_Service.php diff --git a/plugins/wpgraphql-logging/composer.json b/plugins/wpgraphql-logging/composer.json index d729773a..d7654a98 100644 --- a/plugins/wpgraphql-logging/composer.json +++ b/plugins/wpgraphql-logging/composer.json @@ -27,6 +27,7 @@ ], "require": { "php": "^8.1", + "league/csv": "^9.9", "monolog/monolog": "^3.9" }, "minimum-stability": "dev", diff --git a/plugins/wpgraphql-logging/composer.lock b/plugins/wpgraphql-logging/composer.lock index d943791d..2aa3d329 100644 --- a/plugins/wpgraphql-logging/composer.lock +++ b/plugins/wpgraphql-logging/composer.lock @@ -4,8 +4,99 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "964e2ef59c5c3fb7f03c0439c6c94ae3", + "content-hash": "1f76c12e499ba7193832e2bee3a8f6d7", "packages": [ + { + "name": "league/csv", + "version": "9.24.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/csv.git", + "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/e0221a3f16aa2a823047d59fab5809d552e29bc8", + "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1.2" + }, + "require-dev": { + "ext-dom": "*", + "ext-xdebug": "*", + "friendsofphp/php-cs-fixer": "^3.75.0", + "phpbench/phpbench": "^1.4.1", + "phpstan/phpstan": "^1.12.27", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.2", + "phpstan/phpstan-strict-rules": "^1.6.2", + "phpunit/phpunit": "^10.5.16 || ^11.5.22", + "symfony/var-dumper": "^6.4.8 || ^7.3.0" + }, + "suggest": { + "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes", + "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters", + "ext-mbstring": "Needed to ease transcoding CSV using mb stream filters", + "ext-mysqli": "Requiered to use the package with the MySQLi extension", + "ext-pdo": "Required to use the package with the PDO extension", + "ext-pgsql": "Requiered to use the package with the PgSQL extension", + "ext-sqlite3": "Required to use the package with the SQLite3 extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "League\\Csv\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://github.com/nyamsprod/", + "role": "Developer" + } + ], + "description": "CSV data manipulation made easy in PHP", + "homepage": "https://csv.thephpleague.com", + "keywords": [ + "convert", + "csv", + "export", + "filter", + "import", + "read", + "transform", + "write" + ], + "support": { + "docs": "https://csv.thephpleague.com", + "issues": "https://github.com/thephpleague/csv/issues", + "rss": "https://github.com/thephpleague/csv/releases.atom", + "source": "https://github.com/thephpleague/csv" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2025-06-25T14:53:51+00:00" + }, { "name": "monolog/monolog", "version": "3.9.0", diff --git a/plugins/wpgraphql-logging/src/Admin/View/Download/Download_Log_Service.php b/plugins/wpgraphql-logging/src/Admin/View/Download/Download_Log_Service.php new file mode 100644 index 00000000..0338234d --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/View/Download/Download_Log_Service.php @@ -0,0 +1,82 @@ +get_log( $log_id ); + if ( is_null( $log ) ) { + wp_die( esc_html__( 'Log not found.', 'wpgraphql-logging' ) ); + } + + // Set headers for CSV download. + $filename = apply_filters( 'wpgraphql_logging_csv_filename', 'graphql_log_' . $log_id . '.csv' ); + header( 'Content-Type: text/csv; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); + header( 'Cache-Control: must-revalidate, post-check=0, pre-check=0' ); + header( 'Expires: 0' ); + + // Create CSV. + $output = fopen( 'php://output', 'w' ); + if ( ! is_resource( $output ) ) { + wp_die( esc_html__( 'Failed to create CSV output.', 'wpgraphql-logging' ) ); + } + $writer = Writer::createFromStream( $output ); + + $headers = [ + 'ID', + 'Date', + 'Level', + 'Level Name', + 'Message', + 'Channel', + 'Context', + 'Extra', + ]; + + $content = [ + $log->get_id(), + $log->get_datetime(), + $log->get_level(), + $log->get_level_name(), + $log->get_message(), + wp_json_encode( $log->get_context() ), + $log->get_channel(), + wp_json_encode( $log->get_extra() ), + ]; + + + $headers = apply_filters( 'wpgraphql_logging_csv_headers', $headers, $log_id, $log ); + $content = apply_filters( 'wpgraphql_logging_csv_content', $content, $log_id, $log ); + $writer->insertOne( $headers ); + $writer->insertOne( $content ); + fclose( $output ); + exit; + } +} diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index 44229aca..d7ed5f1d 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -235,13 +235,20 @@ public function column_cb( $item ): string { public function column_id( DatabaseEntity $item ): string { $url = \WPGraphQL\Logging\Admin\View_Logs_Page::ADMIN_PAGE_SLUG; $actions = [ - 'view' => sprintf( + 'view' => sprintf( '%s', esc_attr( $url ), 'view', $item->get_id(), esc_html__( 'View', 'wpgraphql-logging' ) ), + 'download' => sprintf( + '%s', + esc_attr( $url ), + 'download', + $item->get_id(), + esc_html__( 'Download', 'wpgraphql-logging' ) + ), ]; return sprintf( diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php index 775cf68a..13ee6c89 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php @@ -9,7 +9,25 @@ */ ?>
-

+
+

+ + + +
diff --git a/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php index 5830d5ae..0b1b88f5 100644 --- a/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php +++ b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php @@ -4,6 +4,7 @@ namespace WPGraphQL\Logging\Admin; +use WPGraphQL\Logging\Admin\View\Download\Download_Log_Service; use WPGraphQL\Logging\Admin\View\List\List_Table; use WPGraphQL\Logging\Logger\Database\LogsRepository; @@ -79,7 +80,7 @@ public function register_settings_page(): void { ); // Updates the list table when filters are applied. - add_action( 'load-' . $menu_page, [ $this, 'process_filters_redirect' ], 10, 0 ); + add_action( 'load-' . $menu_page, [ $this, 'process_page_actions_before_rendering' ], 10, 0 ); } /** @@ -93,12 +94,28 @@ public function render_admin_page(): void { case 'view': $this->render_view_page(); break; + case 'download': + // Handled in process_page_actions_before_rendering. + break; default: $this->render_list_page(); break; } } + /** + * Processes actions for the page, such as filtering and downloading logs. + * This runs before any HTML is output. + */ + public function process_page_actions_before_rendering(): void { + // Check for a download request. + if ( isset( $_GET['action'] ) && 'download' === $_GET['action'] ) { // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + $this->process_log_download(); + } + + $this->process_filters_redirect(); + } + /** * Process filter form submission and redirect to a GET request. * This runs before any HTML is output. @@ -173,6 +190,19 @@ protected function render_list_page(): void { require_once $list_template; // @phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable } + /** + * Renders the list page for log entries. + */ + protected function process_log_download(): void { + if ( ! current_user_can( 'manage_options' ) || ! is_admin() ) { + wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wpgraphql-logging' ) ); + } + + $log_id = isset( $_GET['log'] ) ? absint( $_GET['log'] ) : 0; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + $downloader = new Download_Log_Service(); + $downloader->generate_csv( $log_id ); + } + /** * Renders the view page for a single log entry. */ From f7dd72e72e3f639f4bfaaa630a72a544fd7300a9 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 5 Sep 2025 15:36:22 +0100 Subject: [PATCH 15/32] Fixed failing tests. --- .../tests/wpunit/Logger/Database/DatabaseEntityTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php index 0be1c628..cc0b1e05 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php @@ -82,7 +82,7 @@ public function test_save_method_inserts_log_into_database(): void $this->assertIsInt( $insert_id ); $this->assertGreaterThan(0, $insert_id, 'The save method should return a positive insert ID.'); - $entity = DatabaseEntity::find($insert_id); + $entity = DatabaseEntity::find_by_id($insert_id); $this->assertInstanceOf(DatabaseEntity::class, $entity, 'The find method should return an instance of DatabaseEntity.'); $this->assertEquals($log_data['channel'], $entity->get_channel(), 'The channel should match the saved data.'); $this->assertEquals($log_data['level'], $entity->get_level(), 'The level should match the saved data.'); @@ -98,8 +98,8 @@ public function test_save_method_inserts_log_into_database(): void public function test_find_returns_null_for_nonexistent_id(): void { - $entity = DatabaseEntity::find(999999); - $this->assertNull($entity, 'find() should return null for a non-existent ID.'); + $entity = DatabaseEntity::find_by_id(999999); + $this->assertNull($entity, 'find_by_id() should return null for a non-existent ID.'); } /** From 5c637adf903cc04aecb4bb2a687eb2b58fc64f20 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 5 Sep 2025 16:00:27 +0100 Subject: [PATCH 16/32] Small template tweaks. Moved templates up a directory as they are not all list templates. --- plugins/wpgraphql-logging/composer.json | 2 +- .../src/Admin/Settings/Templates/admin.php | 3 +++ .../wpgraphql-logging/src/Admin/View/List/List_Table.php | 2 +- .../View/{List => }/Templates/wpgraphql-logger-filters.php | 0 .../View/{List => }/Templates/wpgraphql-logger-list.php | 0 .../View/{List => }/Templates/wpgraphql-logger-view.php | 6 +++++- plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php | 4 ++-- 7 files changed, 12 insertions(+), 5 deletions(-) rename plugins/wpgraphql-logging/src/Admin/View/{List => }/Templates/wpgraphql-logger-filters.php (100%) rename plugins/wpgraphql-logging/src/Admin/View/{List => }/Templates/wpgraphql-logger-list.php (100%) rename plugins/wpgraphql-logging/src/Admin/View/{List => }/Templates/wpgraphql-logger-view.php (90%) diff --git a/plugins/wpgraphql-logging/composer.json b/plugins/wpgraphql-logging/composer.json index d7654a98..7d7322bd 100644 --- a/plugins/wpgraphql-logging/composer.json +++ b/plugins/wpgraphql-logging/composer.json @@ -27,7 +27,7 @@ ], "require": { "php": "^8.1", - "league/csv": "^9.9", + "league/csv": "^9", "monolog/monolog": "^3.9" }, "minimum-stability": "dev", diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php index 02388e6b..6ebd58ba 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php @@ -96,6 +96,9 @@ break; } ?> + + + diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index d7ed5f1d..6b6d9e0d 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -393,7 +393,7 @@ protected function extra_tablenav( $which ): void { if ( 'top' !== $which ) { return; } - $template = apply_filters( 'wpgraphql_logging_filters_template', __DIR__ . '/Templates/wpgraphql-logger-filters.php' ); + $template = apply_filters( 'wpgraphql_logging_filters_template', __DIR__ . '/../Templates/wpgraphql-logger-filters.php' ); require_once $template; // @phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable } } diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-filters.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php similarity index 100% rename from plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-filters.php rename to plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-list.php similarity index 100% rename from plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-list.php rename to plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-list.php diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-view.php similarity index 90% rename from plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php rename to plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-view.php index 13ee6c89..3f44f7d7 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-view.php @@ -45,11 +45,15 @@ + + + + - + diff --git a/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php index 0b1b88f5..58d1ca95 100644 --- a/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php +++ b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php @@ -185,7 +185,7 @@ protected function render_list_page(): void { $list_table = new List_Table( new LogsRepository() ); // @phpcs:ignore SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable $list_template = apply_filters( 'wpgraphql_logging_list_template', - __DIR__ . '/View/List/Templates/wpgraphql-logger-list.php' + __DIR__ . '/View/Templates/wpgraphql-logger-list.php' ); require_once $list_template; // @phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable } @@ -224,7 +224,7 @@ protected function render_view_page(): void { $log_template = apply_filters( 'wpgraphql_logging_view_template', - __DIR__ . '/View/List/Templates/wpgraphql-logger-view.php' + __DIR__ . '/View/Templates/wpgraphql-logger-view.php' ); require_once $log_template; // @phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable From 2cf038b67b51591f2d023aef7369d30b6537d862 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 5 Sep 2025 16:40:22 +0100 Subject: [PATCH 17/32] Fixed composer issues. Set PHP min 8.1.2 for league csv. --- plugins/wpgraphql-logging/composer.json | 6 +++--- plugins/wpgraphql-logging/composer.lock | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/wpgraphql-logging/composer.json b/plugins/wpgraphql-logging/composer.json index 7d7322bd..c3e1970a 100644 --- a/plugins/wpgraphql-logging/composer.json +++ b/plugins/wpgraphql-logging/composer.json @@ -26,8 +26,8 @@ "monolog" ], "require": { - "php": "^8.1", - "league/csv": "^9", + "php": ">=8.1.2", + "league/csv": "^9.9", "monolog/monolog": "^3.9" }, "minimum-stability": "dev", @@ -64,7 +64,7 @@ }, "optimize-autoloader": true, "platform": { - "php": "8.1" + "php": "8.1.2" }, "preferred-install": "dist", "sort-packages": true diff --git a/plugins/wpgraphql-logging/composer.lock b/plugins/wpgraphql-logging/composer.lock index 2aa3d329..d4ad5448 100644 --- a/plugins/wpgraphql-logging/composer.lock +++ b/plugins/wpgraphql-logging/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1f76c12e499ba7193832e2bee3a8f6d7", + "content-hash": "39c90e7e58d79f6b447c2e916f498bdc", "packages": [ { "name": "league/csv", @@ -11961,11 +11961,11 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.1" + "php": ">=8.1.2" }, "platform-dev": {}, "platform-overrides": { - "php": "8.1" + "php": "8.1.2" }, "plugin-api-version": "2.6.0" } From d807b09155149b4a48038cea1d12a6a0b665aa25 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 5 Sep 2025 16:41:04 +0100 Subject: [PATCH 18/32] Updated min version. --- plugins/wpgraphql-logging/wpgraphql-logging.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/wpgraphql-logging/wpgraphql-logging.php b/plugins/wpgraphql-logging/wpgraphql-logging.php index 91b33a12..35ecbe84 100644 --- a/plugins/wpgraphql-logging/wpgraphql-logging.php +++ b/plugins/wpgraphql-logging/wpgraphql-logging.php @@ -144,7 +144,7 @@ static function (): void { * Display an admin notice if the PHP version is not met. */ function wpgraphql_logging_plugin_admin_notice_min_php_version(): void { - if ( version_compare( PHP_VERSION, '8.1', '>=' ) ) { + if ( version_compare( PHP_VERSION, '8.1.2', '>=' ) ) { return; } @@ -155,7 +155,7 @@ static function (): void {

From 4b48420a722dc38bf680d0ce2d1859659ec8bc5c Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 5 Sep 2025 16:52:51 +0100 Subject: [PATCH 19/32] Fixed template path for psalm. --- plugins/wpgraphql-logging/psalm.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/wpgraphql-logging/psalm.xml b/plugins/wpgraphql-logging/psalm.xml index 543f06ee..9bf8ed5a 100644 --- a/plugins/wpgraphql-logging/psalm.xml +++ b/plugins/wpgraphql-logging/psalm.xml @@ -39,13 +39,13 @@ - + - + From 5341b6dacf10c7e53199af7b13f7d1cd1ff03083 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 5 Sep 2025 17:00:48 +0100 Subject: [PATCH 20/32] Updated admin settings for #399 --- .../Fields/Tab/Basic_Configuration_Tab.php | 26 ++++--------------- .../src/Admin/Settings/Templates/admin.php | 1 - 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php index 54e433f8..e752b365 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php @@ -52,13 +52,6 @@ class Basic_Configuration_Tab implements Settings_Tab_Interface { */ public const ADMIN_USER_LOGGING = 'admin_user_logging'; - /** - * The field ID for the performance metrics text input. - * - * @var string - */ - public const PERFORMANCE_METRICS = 'performance_metrics'; - /** * The field ID for the log point selection select. * @@ -129,26 +122,17 @@ public function get_fields(): array { $this->get_name(), __( 'Data Sampling Rate', 'wpgraphql-logging' ), [ - '100' => __( '100% (All requests)', 'wpgraphql-logging' ), - '50' => __( '50% (Every other request)', 'wpgraphql-logging' ), - '25' => __( '25% (Every 4th request)', 'wpgraphql-logging' ), '10' => __( '10% (Every 10th request)', 'wpgraphql-logging' ), + '25' => __( '25% (Every 4th request)', 'wpgraphql-logging' ), + '50' => __( '50% (Every other request)', 'wpgraphql-logging' ), + '75' => __( '75% (Every 3 out of 4 requests)', 'wpgraphql-logging' ), + '100' => __( '100% (All requests)', 'wpgraphql-logging' ), ], '', __( 'Percentage of requests to log for performance optimization.', 'wpgraphql-logging' ), false ); - $fields[ self::PERFORMANCE_METRICS ] = new Text_Input_Field( - self::PERFORMANCE_METRICS, - $this->get_name(), - __( 'Performance Threshold (seconds)', 'wpgraphql-logging' ), - '', - __( 'Only log requests that take longer than this threshold. 0 logs all requests. Calculated in seconds.', 'wpgraphql-logging' ), - __( 'e.g., 1.5', 'wpgraphql-logging' ) - ); - - $fields[ self::EVENT_LOG_SELECTION ] = new Select_Field( self::EVENT_LOG_SELECTION, $this->get_name(), @@ -162,7 +146,7 @@ public function get_fields(): array { Events::RESPONSE_HEADERS_TO_SEND => __( 'Response Headers', 'wpgraphql-logging' ), ], '', - __( 'Select which points in the request lifecycle to log. By default, all points are logged.', 'wpgraphql-logging' ), + __( 'Select which points in the request lifecycle to log. By default, no events are logged.', 'wpgraphql-logging' ), true ); diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php index 6ebd58ba..ef74a8dd 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php @@ -69,7 +69,6 @@
  • -
  • Date: Fri, 5 Sep 2025 17:35:21 +0100 Subject: [PATCH 21/32] Refactored get_query and added this to the view and CSV download. --- .../View/Download/Download_Log_Service.php | 4 ++- .../src/Admin/View/List/List_Table.php | 17 +++++++----- .../View/Templates/wpgraphql-logger-view.php | 12 ++++++--- .../src/Logger/Database/DatabaseEntity.php | 26 +++++++++++++++++++ 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/Download/Download_Log_Service.php b/plugins/wpgraphql-logging/src/Admin/View/Download/Download_Log_Service.php index 0338234d..b16f8ba5 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Download/Download_Log_Service.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Download/Download_Log_Service.php @@ -56,6 +56,7 @@ public function generate_csv( int $log_id ): void { 'Level Name', 'Message', 'Channel', + 'Query', 'Context', 'Extra', ]; @@ -66,8 +67,9 @@ public function generate_csv( int $log_id ): void { $log->get_level(), $log->get_level_name(), $log->get_message(), - wp_json_encode( $log->get_context() ), $log->get_channel(), + $log->get_query(), + wp_json_encode( $log->get_context() ), wp_json_encode( $log->get_extra() ), ]; diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index 6b6d9e0d..1a0d099b 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -28,7 +28,7 @@ class List_Table extends WP_List_Table { * * @var int */ - public const DEFAULT_PER_PAGE = 25; + public const DEFAULT_PER_PAGE = 20; /** * Constructor. @@ -73,7 +73,6 @@ public function prepare_items(): void { $per_page = $this->get_items_per_page( 'logs_per_page', self::DEFAULT_PER_PAGE ); $current_page = $this->get_pagenum(); - /** @psalm-suppress InvalidArgument */ /** @psalm-suppress InvalidArgument */ $where = $this->process_where( $_REQUEST ); $total_items = $this->repository->get_log_count( $where ); @@ -276,9 +275,8 @@ public function column_query( DatabaseEntity $item ): ?string { * @return string The query */ public function get_query(DatabaseEntity $item): string { - $extra = $item->get_extra(); - $query = ! empty( $extra['wpgraphql_query'] ) ? esc_html( $extra['wpgraphql_query'] ) : ''; - return '
    ' . esc_html( $query ) . '
    '; + $query = $item->get_query(); + return $this->format_code( $query ); } /** @@ -330,7 +328,14 @@ public function get_request_headers(DatabaseEntity $item): string { if ( false === $formatted_request_headers ) { return ''; } - return '
    ' . esc_html( $formatted_request_headers ) . '
    '; + return $this->format_code( $formatted_request_headers ); + } + + protected function format_code(string $code): string { + if ( empty( $code ) ) { + return ''; + } + return '
    ' . esc_html( $code	 ) . '
    '; } /** diff --git a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-view.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-view.php index 3f44f7d7..7a782aac 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-view.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-view.php @@ -39,10 +39,6 @@
    - - - - @@ -55,6 +51,14 @@ + + + + + + + + diff --git a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php index 34926990..ec6e7554 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php @@ -236,6 +236,32 @@ public function get_datetime(): string { return $this->datetime; } + public function get_query(): ?string { + + $extra = $this->get_extra(); + $context = $this->get_context(); + if ( empty( $context ) || ! is_array( $context ) ) { + return null; + } + + $query = $context['query']; + + $request = $context['request'] ?? null; + if ( empty( $request ) || ! is_array( $request ) ) { + return $query; + } + + $params = $request['params'] ?? null; + if ( empty( $params ) || ! is_array( $params ) ) { + return $query; + } + if ( isset( $params['query'] ) && is_string( $params['query'] ) ) { + return $params['query']; + } + + return $query; + } + /** * Finds multiple log entries and returns them as an array. * From 2ac658426a7a0c070dcd6483dd250660e5c9bb1f Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 5 Sep 2025 17:39:12 +0100 Subject: [PATCH 22/32] Fixed QA issues. --- .../src/Admin/View/List/List_Table.php | 12 +++++++++++- .../src/Logger/Database/DatabaseEntity.php | 11 +++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index 1a0d099b..22bccd8f 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -276,6 +276,9 @@ public function column_query( DatabaseEntity $item ): ?string { */ public function get_query(DatabaseEntity $item): string { $query = $item->get_query(); + if (! is_string($query) || '' === $query) { + return ''; + } return $this->format_code( $query ); } @@ -331,11 +334,18 @@ public function get_request_headers(DatabaseEntity $item): string { return $this->format_code( $formatted_request_headers ); } + /** + * Format code for display in a table cell. + * + * @param string $code The code to format. + * + * @return string The formatted code. + */ protected function format_code(string $code): string { if ( empty( $code ) ) { return ''; } - return '
    ' . esc_html( $code	 ) . '
    '; + return '
    ' . esc_html( $code ) . '
    '; } /** diff --git a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php index ec6e7554..c1ea5a18 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php @@ -236,11 +236,17 @@ public function get_datetime(): string { return $this->datetime; } + /** + * Extracts and returns the GraphQL query from the context, if available. + * + * @phpcs:disable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh + * + * @return string|null The GraphQL query string, or null if not available. + */ public function get_query(): ?string { - $extra = $this->get_extra(); $context = $this->get_context(); - if ( empty( $context ) || ! is_array( $context ) ) { + if ( empty( $context ) ) { return null; } @@ -255,6 +261,7 @@ public function get_query(): ?string { if ( empty( $params ) || ! is_array( $params ) ) { return $query; } + if ( isset( $params['query'] ) && is_string( $params['query'] ) ) { return $params['query']; } From 1751f1ce67bdfa2bf9824b7954cd201ce4536816 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 5 Sep 2025 17:45:25 +0100 Subject: [PATCH 23/32] Added date and time picker for the list. --- .../src/Admin/View/List/List_Table.php | 2 +- .../Templates/wpgraphql-logger-filters.php | 4 +- .../src/Admin/View_Logs_Page.php | 53 ++++++++++++++++++- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index 22bccd8f..0f500c5b 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -276,7 +276,7 @@ public function column_query( DatabaseEntity $item ): ?string { */ public function get_query(DatabaseEntity $item): string { $query = $item->get_query(); - if (! is_string($query) || '' === $query) { + if ( ! is_string( $query ) || '' === $query ) { return ''; } return $this->format_code( $query ); diff --git a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php index 57103c07..fad69a41 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php @@ -38,8 +38,8 @@ */ $wpgraphql_logging_log_levels = [ 'debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency' ]; ?> - - + + - - - - - +$log_levels = [ 'debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency' ]; +?> - +
    + + + + + + + 'margin: 0;' ] ); ?> +
    \ No newline at end of file diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php index 4ea83ae0..e9db0727 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php @@ -114,10 +114,18 @@ public function delete(int $id): bool { * * @return bool True if all logs were deleted successfully, false otherwise. */ - public function delete_all(): bool { + public function delete_all(): void { global $wpdb; - $table_name = DatabaseEntity::get_table_name(); - $result = $wpdb->query( $wpdb->prepare( 'TRUNCATE TABLE %s', $table_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - return false !== $result; + + if ( isset( $this->table ) && ! empty( $this->table ) ) { + $table = $this->table; + } else { + $table = $wpdb->prefix . 'wpgraphql_logging'; + } + $table_quoted = "`" . str_replace( "`", "``", $table ) . "`"; + $sql = "TRUNCATE TABLE {$table_quoted}"; + + // Execute + $wpdb->query( $sql ); } } From 5ba71b5cf518f9ec9dd159f912a478998573fe17 Mon Sep 17 00:00:00 2001 From: Theo <328805+theodesp@users.noreply.github.com> Date: Tue, 9 Sep 2025 11:51:17 +0100 Subject: [PATCH 28/32] lint: fix phpstan --- .../src/Admin/View/List/List_Table.php | 7 ++++--- .../src/Logger/Database/LogsRepository.php | 16 ++++------------ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index 5b563dc1..41d6b397 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -128,7 +128,7 @@ public function process_bulk_action(): void { $nonce_action = 'bulk-' . $this->_args['plural']; $nonce = $_REQUEST['_wpnonce'] ?? ''; - if ( ! wp_verify_nonce( $nonce, $nonce_action ) ) { + if ( false === wp_verify_nonce( $nonce, $nonce_action ) ) { wp_die( esc_html__( 'Nonce verification failed!', 'wpgraphql-logging' ) ); } @@ -449,6 +449,7 @@ protected function get_sortable_columns(): array { * @param string $which The location of the nav ('top' or 'bottom'). */ protected function display_tablenav( $which ): void { + $which_position = ( 'top' === $which ) ? 'top' : 'bottom'; ?>
    @@ -457,12 +458,12 @@ protected function display_tablenav( $which ): void {
    - bulk_actions( $which ); ?> + bulk_actions( $which_position ); ?>
    extra_tablenav( $which ); - $this->pagination( $which ); + $this->pagination( $which_position ); ?>
    diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php index e9db0727..9d91f1b2 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php @@ -112,20 +112,12 @@ public function delete(int $id): bool { /** * Delete all log entries. * - * @return bool True if all logs were deleted successfully, false otherwise. + * @return void */ public function delete_all(): void { global $wpdb; - - if ( isset( $this->table ) && ! empty( $this->table ) ) { - $table = $this->table; - } else { - $table = $wpdb->prefix . 'wpgraphql_logging'; - } - $table_quoted = "`" . str_replace( "`", "``", $table ) . "`"; - $sql = "TRUNCATE TABLE {$table_quoted}"; - - // Execute - $wpdb->query( $sql ); + // Use DatabaseEntity::get_table_name() to get the table name + $table_name = DatabaseEntity::get_table_name(); + $wpdb->query( "DELETE FROM {$table_name}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery } } From ccfb39dce52853d1b654f2569770b44c79a6f521 Mon Sep 17 00:00:00 2001 From: Theo <328805+theodesp@users.noreply.github.com> Date: Tue, 9 Sep 2025 12:02:10 +0100 Subject: [PATCH 29/32] lint: fix psalm errors --- .../src/Admin/View/List/List_Table.php | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index 41d6b397..8ad194d0 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -121,28 +121,36 @@ public function get_bulk_actions(): array { */ public function process_bulk_action(): void { $action = $this->current_action(); + if ( ! in_array( $action, [ 'delete', 'bulk_delete', 'delete_all' ], true ) ) { return; } + // Nonce action WordPress uses for bulk actions is 'bulk-' . $this->_args['plural'] $nonce_action = 'bulk-' . $this->_args['plural']; - $nonce = $_REQUEST['_wpnonce'] ?? ''; - - if ( false === wp_verify_nonce( $nonce, $nonce_action ) ) { + $nonce_value = $_REQUEST['_wpnonce'] ?? ''; + + // Ensure nonce is a string for wp_verify_nonce + $nonce = is_string( $nonce_value ) ? $nonce_value : ''; + + // Fix for PHPStan: wp_verify_nonce returns int|false, need explicit boolean check + $nonce_result = wp_verify_nonce( $nonce, $nonce_action ); + if ( false === $nonce_result ) { wp_die( esc_html__( 'Nonce verification failed!', 'wpgraphql-logging' ) ); } $deleted_count = 0; - // WordPress sometimes sends 'delete' for selected items (action or action2), treat it as 'bulk_delete'. + // WordPress sometimes sends 'delete' for selected items if ( in_array( $action, [ 'delete', 'bulk_delete' ], true ) && ! empty( $_REQUEST['log'] ) ) { $ids = array_map( 'absint', (array) $_REQUEST['log'] ); - if ( ! empty( $ids ) ) { - foreach ( $ids as $id ) { + // Remove redundant empty check since array_map always returns array + foreach ( $ids as $id ) { + if ( $id > 0 ) { // Only process valid IDs $this->repository->delete( $id ); } - $deleted_count = count( $ids ); } + $deleted_count = count( array_filter( $ids, static fn( $id ) => $id > 0 ) ); } if ( 'delete_all' === $action ) { @@ -152,13 +160,14 @@ public function process_bulk_action(): void { } if ( $deleted_count > 0 ) { - // Preserve filters during redirect. + // Preserve filters during redirect $preserved_filters = []; $filter_keys = [ 'level_filter', 'start_date', 'end_date' ]; foreach ( $filter_keys as $key ) { - if ( ! empty( $_REQUEST[ $key ] ) ) { - $preserved_filters[ $key ] = sanitize_text_field( wp_unslash( (string) $_REQUEST[ $key ] ) ); + $value = $_REQUEST[ $key ] ?? null; + if ( ! empty( $value ) && is_string( $value ) ) { + $preserved_filters[ $key ] = sanitize_text_field( wp_unslash( $value ) ); } } @@ -175,8 +184,7 @@ public function process_bulk_action(): void { exit; } } - - + /** * Get the columns for the logs table. * From d6f7eedb03ec1cb49f95b567e8d75e9c13296236 Mon Sep 17 00:00:00 2001 From: Theo <328805+theodesp@users.noreply.github.com> Date: Tue, 9 Sep 2025 12:14:52 +0100 Subject: [PATCH 30/32] lint: run phpcbf --- .../src/Admin/View/List/List_Table.php | 14 +++--- .../Templates/wpgraphql-logger-filters.php | 49 ++++++++++--------- .../src/Logger/Database/LogsRepository.php | 2 - .../src/Logger/LoggingHelper.php | 27 +++++----- 4 files changed, 46 insertions(+), 46 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index 8ad194d0..a29abb26 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -128,7 +128,7 @@ public function process_bulk_action(): void { // Nonce action WordPress uses for bulk actions is 'bulk-' . $this->_args['plural'] $nonce_action = 'bulk-' . $this->_args['plural']; - $nonce_value = $_REQUEST['_wpnonce'] ?? ''; + $nonce_value = $_REQUEST['_wpnonce'] ?? ''; // Ensure nonce is a string for wp_verify_nonce $nonce = is_string( $nonce_value ) ? $nonce_value : ''; @@ -162,7 +162,7 @@ public function process_bulk_action(): void { if ( $deleted_count > 0 ) { // Preserve filters during redirect $preserved_filters = []; - $filter_keys = [ 'level_filter', 'start_date', 'end_date' ]; + $filter_keys = [ 'level_filter', 'start_date', 'end_date' ]; foreach ( $filter_keys as $key ) { $value = $_REQUEST[ $key ] ?? null; @@ -488,10 +488,12 @@ protected function render_custom_filters(): void { __DIR__ . '/../Templates/wpgraphql-logger-filters.php' ); - if ( file_exists( $template ) ) { - echo '
    '; - require $template; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable - echo '
    '; + if ( ! file_exists( $template ) ) { + return; } + + echo '
    '; + require $template; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable + echo '
    '; } } diff --git a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php index a94c16c3..91dc67f1 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php @@ -1,8 +1,9 @@
    - - - + + + - + - 'margin: 0;' ] ); ?> -
    \ No newline at end of file + 'margin: 0;' ] ); ?> +
    diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php index 9d91f1b2..05505052 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php @@ -111,8 +111,6 @@ public function delete(int $id): bool { /** * Delete all log entries. - * - * @return void */ public function delete_all(): void { global $wpdb; diff --git a/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php index 9ba45b7c..eaaa2eee 100644 --- a/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php +++ b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php @@ -50,9 +50,9 @@ protected function is_logging_enabled( array $config, ?string $query_string = nu } // Check if the query is an introspection query and skip logging if it is. - if ( $is_enabled && $this->is_introspection_query( $query_string ) ) { - $is_enabled = false; - } + if ( $is_enabled && $this->is_introspection_query( $query_string ) ) { + $is_enabled = false; + } /** * Filter the final decision on whether to log a request. @@ -64,16 +64,15 @@ protected function is_logging_enabled( array $config, ?string $query_string = nu } /** - * Checks if a query is an introspection query. - * - * @param string|null $query_string The GraphQL query string. - * @return bool - */ - protected function is_introspection_query( ?string $query_string ): bool { - if ( null === $query_string ) { - return false; - } + * Checks if a query is an introspection query. + * + * @param string|null $query_string The GraphQL query string. + */ + protected function is_introspection_query( ?string $query_string ): bool { + if ( null === $query_string ) { + return false; + } - return strpos( $query_string, '__schema' ) !== false; - } + return strpos( $query_string, '__schema' ) !== false; + } } From fddbb3a501cabf0a2b878e49c0089d5728f7720a Mon Sep 17 00:00:00 2001 From: Theo <328805+theodesp@users.noreply.github.com> Date: Tue, 9 Sep 2025 12:33:00 +0100 Subject: [PATCH 31/32] lint: phpcs fixes --- .../wpgraphql-logging/src/Admin/View/List/List_Table.php | 6 +----- .../src/Logger/Database/LogsRepository.php | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index a29abb26..eebf27bd 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -126,14 +126,11 @@ public function process_bulk_action(): void { return; } - // Nonce action WordPress uses for bulk actions is 'bulk-' . $this->_args['plural'] $nonce_action = 'bulk-' . $this->_args['plural']; $nonce_value = $_REQUEST['_wpnonce'] ?? ''; - // Ensure nonce is a string for wp_verify_nonce $nonce = is_string( $nonce_value ) ? $nonce_value : ''; - // Fix for PHPStan: wp_verify_nonce returns int|false, need explicit boolean check $nonce_result = wp_verify_nonce( $nonce, $nonce_action ); if ( false === $nonce_result ) { wp_die( esc_html__( 'Nonce verification failed!', 'wpgraphql-logging' ) ); @@ -141,7 +138,7 @@ public function process_bulk_action(): void { $deleted_count = 0; - // WordPress sometimes sends 'delete' for selected items + // WordPress sometimes sends 'delete' for selected items. if ( in_array( $action, [ 'delete', 'bulk_delete' ], true ) && ! empty( $_REQUEST['log'] ) ) { $ids = array_map( 'absint', (array) $_REQUEST['log'] ); // Remove redundant empty check since array_map always returns array @@ -160,7 +157,6 @@ public function process_bulk_action(): void { } if ( $deleted_count > 0 ) { - // Preserve filters during redirect $preserved_filters = []; $filter_keys = [ 'level_filter', 'start_date', 'end_date' ]; diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php index 05505052..83132411 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php @@ -114,8 +114,7 @@ public function delete(int $id): bool { */ public function delete_all(): void { global $wpdb; - // Use DatabaseEntity::get_table_name() to get the table name $table_name = DatabaseEntity::get_table_name(); - $wpdb->query( "DELETE FROM {$table_name}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->query( $wpdb->prepare( "DELETE FROM %i", $table_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery } } From 036e5f98c39a6d435cd5aa15d8821f4dd9ee1c58 Mon Sep 17 00:00:00 2001 From: Theo <328805+theodesp@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:09:27 +0100 Subject: [PATCH 32/32] fix: fixed headers already sent --- .../src/Admin/View/List/List_Table.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php index eebf27bd..664206c4 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -122,7 +122,7 @@ public function get_bulk_actions(): array { public function process_bulk_action(): void { $action = $this->current_action(); - if ( ! in_array( $action, [ 'delete', 'bulk_delete', 'delete_all' ], true ) ) { + if ( ! in_array( $action, [ 'delete', 'delete_all' ], true ) ) { return; } @@ -176,8 +176,19 @@ public function process_bulk_action(): void { $redirect_url ); - wp_safe_redirect( esc_url_raw( $redirect_url ) ); - exit; + if ( ! headers_sent() ) { + wp_safe_redirect( esc_url_raw( $redirect_url ) ); + exit; + } else { + echo ''; + printf( + '

    %s %s

    ', + esc_html__( 'Logs deleted successfully.', 'wpgraphql-logging' ), + esc_url( $redirect_url ), + esc_html__( 'Return to Logs', 'wpgraphql-logging' ) + ); + exit; + } } }
    get_level(); ?>
    get_level_name() ); ?>
    get_message() ); ?>get_message() ); ?>
    get_datetime() ); ?>
    get_channel() ); ?>
    get_level(); ?> get_message() ); ?>
    get_channel() ); ?>
    get_query() ); ?>
    get_context(), JSON_PRETTY_PRINT ) ); ?>