Skip to content

Commit 99e2529

Browse files
committed
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.
1 parent 93a45ae commit 99e2529

File tree

6 files changed

+233
-4
lines changed

6 files changed

+233
-4
lines changed

plugins/wpgraphql-logging/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
],
2828
"require": {
2929
"php": "^8.1",
30+
"league/csv": "^9.9",
3031
"monolog/monolog": "^3.9"
3132
},
3233
"minimum-stability": "dev",

plugins/wpgraphql-logging/composer.lock

Lines changed: 92 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WPGraphQL\Logging\Admin\View\Download;
6+
7+
use League\Csv\Writer;
8+
use WPGraphQL\Logging\Logger\Database\LogsRepository;
9+
10+
/**
11+
* Service for handling log downloads.
12+
*
13+
* @package WPGraphQL\Logging
14+
*
15+
* @since 0.0.1
16+
*/
17+
class Download_Log_Service {
18+
/**
19+
* Generates and serves a CSV file for a single log entry.
20+
*
21+
* @param int $log_id The ID of the log to download.
22+
*/
23+
public function generate_csv( int $log_id ): void {
24+
if ( ! current_user_can( 'manage_options' ) || ! is_admin() ) {
25+
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wpgraphql-logging' ) );
26+
}
27+
28+
if ( 0 === $log_id ) {
29+
wp_die( esc_html__( 'Invalid log ID.', 'wpgraphql-logging' ) );
30+
}
31+
32+
$repository = new LogsRepository();
33+
$log = $repository->get_log( $log_id );
34+
if ( is_null( $log ) ) {
35+
wp_die( esc_html__( 'Log not found.', 'wpgraphql-logging' ) );
36+
}
37+
38+
// Set headers for CSV download.
39+
$filename = apply_filters( 'wpgraphql_logging_csv_filename', 'graphql_log_' . $log_id . '.csv' );
40+
header( 'Content-Type: text/csv; charset=utf-8' );
41+
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
42+
header( 'Cache-Control: must-revalidate, post-check=0, pre-check=0' );
43+
header( 'Expires: 0' );
44+
45+
// Create CSV.
46+
$output = fopen( 'php://output', 'w' );
47+
if ( ! is_resource( $output ) ) {
48+
wp_die( esc_html__( 'Failed to create CSV output.', 'wpgraphql-logging' ) );
49+
}
50+
$writer = Writer::createFromStream( $output );
51+
52+
$headers = [
53+
'ID',
54+
'Date',
55+
'Level',
56+
'Level Name',
57+
'Message',
58+
'Channel',
59+
'Context',
60+
'Extra',
61+
];
62+
63+
$content = [
64+
$log->get_id(),
65+
$log->get_datetime(),
66+
$log->get_level(),
67+
$log->get_level_name(),
68+
$log->get_message(),
69+
wp_json_encode( $log->get_context() ),
70+
$log->get_channel(),
71+
wp_json_encode( $log->get_extra() ),
72+
];
73+
74+
75+
$headers = apply_filters( 'wpgraphql_logging_csv_headers', $headers, $log_id, $log );
76+
$content = apply_filters( 'wpgraphql_logging_csv_content', $content, $log_id, $log );
77+
$writer->insertOne( $headers );
78+
$writer->insertOne( $content );
79+
fclose( $output );
80+
exit;
81+
}
82+
}

plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,20 @@ public function column_cb( $item ): string {
235235
public function column_id( DatabaseEntity $item ): string {
236236
$url = \WPGraphQL\Logging\Admin\View_Logs_Page::ADMIN_PAGE_SLUG;
237237
$actions = [
238-
'view' => sprintf(
238+
'view' => sprintf(
239239
'<a href="?page=%s&action=%s&log=%d">%s</a>',
240240
esc_attr( $url ),
241241
'view',
242242
$item->get_id(),
243243
esc_html__( 'View', 'wpgraphql-logging' )
244244
),
245+
'download' => sprintf(
246+
'<a href="?page=%s&action=%s&log=%d">%s</a>',
247+
esc_attr( $url ),
248+
'download',
249+
$item->get_id(),
250+
esc_html__( 'Download', 'wpgraphql-logging' )
251+
),
245252
];
246253

247254
return sprintf(

plugins/wpgraphql-logging/src/Admin/View/List/Templates/wpgraphql-logger-view.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,25 @@
99
*/
1010
?>
1111
<div class="wrap">
12-
<h1><?php esc_html_e( 'Log Entry', 'wpgraphql-logging' ); ?></h1>
12+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
13+
<h1><?php esc_html_e( 'Log Entry', 'wpgraphql-logging' ); ?></h1>
14+
<a href="
15+
<?php
16+
echo esc_url(
17+
admin_url(
18+
sprintf(
19+
'admin.php?page=%s&action=%s&log=%d',
20+
\WPGraphQL\Logging\Admin\View_Logs_Page::ADMIN_PAGE_SLUG,
21+
'download',
22+
$log->get_id()
23+
)
24+
)
25+
);
26+
?>
27+
" class="button">
28+
<?php esc_html_e( 'Download Log', 'wpgraphql-logging' ); ?>
29+
</a>
30+
</div>
1331

1432
<table class="widefat striped">
1533
<tbody>

plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace WPGraphQL\Logging\Admin;
66

7+
use WPGraphQL\Logging\Admin\View\Download\Download_Log_Service;
78
use WPGraphQL\Logging\Admin\View\List\List_Table;
89
use WPGraphQL\Logging\Logger\Database\LogsRepository;
910

@@ -79,7 +80,7 @@ public function register_settings_page(): void {
7980
);
8081

8182
// Updates the list table when filters are applied.
82-
add_action( 'load-' . $menu_page, [ $this, 'process_filters_redirect' ], 10, 0 );
83+
add_action( 'load-' . $menu_page, [ $this, 'process_page_actions_before_rendering' ], 10, 0 );
8384
}
8485

8586
/**
@@ -93,12 +94,28 @@ public function render_admin_page(): void {
9394
case 'view':
9495
$this->render_view_page();
9596
break;
97+
case 'download':
98+
// Handled in process_page_actions_before_rendering.
99+
break;
96100
default:
97101
$this->render_list_page();
98102
break;
99103
}
100104
}
101105

106+
/**
107+
* Processes actions for the page, such as filtering and downloading logs.
108+
* This runs before any HTML is output.
109+
*/
110+
public function process_page_actions_before_rendering(): void {
111+
// Check for a download request.
112+
if ( isset( $_GET['action'] ) && 'download' === $_GET['action'] ) { // @phpcs:ignore WordPress.Security.NonceVerification.Recommended
113+
$this->process_log_download();
114+
}
115+
116+
$this->process_filters_redirect();
117+
}
118+
102119
/**
103120
* Process filter form submission and redirect to a GET request.
104121
* This runs before any HTML is output.
@@ -173,6 +190,19 @@ protected function render_list_page(): void {
173190
require_once $list_template; // @phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable
174191
}
175192

193+
/**
194+
* Renders the list page for log entries.
195+
*/
196+
protected function process_log_download(): void {
197+
if ( ! current_user_can( 'manage_options' ) || ! is_admin() ) {
198+
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wpgraphql-logging' ) );
199+
}
200+
201+
$log_id = isset( $_GET['log'] ) ? absint( $_GET['log'] ) : 0; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended
202+
$downloader = new Download_Log_Service();
203+
$downloader->generate_csv( $log_id );
204+
}
205+
176206
/**
177207
* Renders the view page for a single log entry.
178208
*/

0 commit comments

Comments
 (0)