Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
20.91% covered (danger)
20.91%
23 / 110
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Logs_Page
20.91% covered (danger)
20.91%
23 / 110
0.00% covered (danger)
0.00%
0 / 6
360.45
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_page_title
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 add_page
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 display_page
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 1
110
 enqueue_scripts
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
3.00
 enqueue_styles
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
3.01
1<?php
2/**
3 * The UI around the logs table.
4 *
5 * E.g. /wp-admin/admin.php?page=bh-wp-logger-development-plugin-logs.
6 *
7 * TODO: Add "send to plugin developer" button.
8 * TODO: Add copy to clipboard button.
9 *
10 * @package brianhenryie/bh-wp-logger
11 */
12
13namespace BrianHenryIE\WP_Logger\Admin;
14
15use BrianHenryIE\WP_Logger\API\BH_WP_PSR_Logger;
16use BrianHenryIE\WP_Logger\API_Interface;
17use BrianHenryIE\WP_Logger\Logger_Settings_Interface;
18use BrianHenryIE\WP_Logger\WP_Includes\Plugin_Logger_Actions;
19use Psr\Log\LoggerAwareTrait;
20use Psr\Log\LogLevel;
21use Psr\Log\NullLogger;
22
23/**
24 * Functions for registering a "hidden menu" item, to add the wp-admin page to display the logs.
25 *
26 * @see Plugin_Logger_Actions::add_admin_ui_logs_page_hooks()
27 *
28 * @uses Logger_Settings_Interface::get_plugin_slug()
29 */
30class Logs_Page {
31    use LoggerAwareTrait;
32
33    /**
34     * Logs_Page constructor.
35     *
36     * @param API_Interface             $api The main functions of the logger. Used to get the list of log files. Needed to instantiate the table.
37     * @param Logger_Settings_Interface $settings The configuration used to set up the logger. The logger settings. i.e. what is the plugin slug this logger is for.
38     * @param ?BH_WP_PSR_Logger         $logger The logger itself, for logging.
39     */
40    public function __construct(
41        protected API_Interface $api,
42        protected Logger_Settings_Interface $settings,
43        ?BH_WP_PSR_Logger $logger = null
44    ) {
45        $this->setLogger( $logger ?? new NullLogger() );
46    }
47
48    /**
49     * Set the `global $title` before it is used to avoid a null error.
50     *
51     * `Deprecated: strip_tags(): Passing null to parameter #1 ($string) of type string is deprecated in /.../wp-admin/admin-header.php on line 41`
52     *
53     * @hooked admin_init
54     */
55    public function set_page_title(): void {
56        /**
57         * Data is never written.
58         * phpcs:disable WordPress.Security.NonceVerification.Recommended
59         */
60        if (
61            ! isset( $_REQUEST['page'] )
62            || ! is_string( $_REQUEST['page'] )
63            || $this->settings->get_plugin_slug() . '-logs' !== sanitize_key( wp_unslash( $_REQUEST['page'] ) )
64        ) {
65            return;
66        }
67
68        /**
69         * @see strip_tags()
70         *
71         * phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
72         */
73        global $title;
74        $title = $this->settings->get_plugin_name() . ' Logs page';
75    }
76
77    /**
78     * Add a WordPress admin UI page, but without any menu linking to it.
79     *
80     * @hooked admin_menu
81     *
82     * @see wp-admin/menu.php
83     */
84    public function add_page(): void {
85
86        $logs_slug  = "{$this->settings->get_plugin_slug()}-logs";
87        $menu_title = 'Logs';
88
89        $parent_slug = '';
90
91        /** @var array<int,string[]> $menu */
92        global $menu;
93        foreach ( $menu as $menu_item ) {
94            if ( stristr( $menu_item[0], 'logs' ) || stristr( $menu_item[2], 'logs' ) || stristr( $menu_item[3], 'logs' ) ) {
95                $parent_slug = $menu_item[2];
96                $menu_title  = $this->settings->get_plugin_name();
97                break;
98            }
99        }
100
101        add_submenu_page(
102            $parent_slug,
103            __( 'Logs', 'bh-wp-logger' ),
104            $menu_title,
105            'manage_options',
106            $logs_slug,
107            array( $this, 'display_page' )
108        );
109    }
110
111    /**
112     * Display the page.
113     * Record the last visited time.
114     *
115     * Registered above.
116     *
117     * @see add_page()
118     */
119    public function display_page(): void {
120
121        echo '<div class="wrap">';
122
123        echo '<h1>';
124        echo esc_html( $this->settings->get_plugin_name() );
125        echo '</h1>';
126
127        $log_files = $this->api->get_log_files();
128
129        if ( empty( $log_files ) ) {
130            // This will occur e.g. immediately after deleting all logs.
131            echo '<p>No logs to display.</p>';
132            echo '</div>';
133            return;
134        }
135
136        $logs_table = new Logs_List_Table( $this->api, $this->settings, $this->logger );
137
138        // Set date here?
139
140        // Show a list of date to switch between dates.
141        echo '<label for="log_date">Log date:</label>';
142        echo '<select name="log_date" id="log_date">';
143
144        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
145        $chosen_date = isset( $_GET['log_date'] ) && is_string( $_GET['log_date'] ) ? sanitize_key( $_GET['log_date'] ) : array_key_last( $log_files );
146
147        // Maybe should use set file?
148        $logs_table->set_date( $chosen_date );
149
150        // TODO: Allow filtering here to add external log files, e.g. from Authorize.net SDK.
151        foreach ( $log_files as $date => $path ) {
152            $date_formatted = $date;
153            echo '<option value="' . esc_attr( $date ) . '"';
154            if ( $date === $chosen_date ) {
155                echo ' selected';
156            }
157            echo '>' . esc_html( $date_formatted ) . '</option>';
158        }
159        echo '</select>';
160
161        echo '<button name="deleteButton" id="deleteButton" data-date="' . esc_attr( $chosen_date ) . '" class="button logs-page button-primary">Delete ' . esc_html( $chosen_date ) . ' logs</button>';
162        echo '<button name="deleteAllButton" id="deleteAllButton" class="button logs-page button-secondary">Delete all logs</button>';
163
164        wp_nonce_field( 'bh-wp-logger-delete', 'delete_logs_wpnonce' );
165
166        echo '<p>Current log level: <b>' . esc_html( ucfirst( $this->settings->get_log_level() ) ) . '</b></p>';
167
168        // If this is in the logger's private-uploads directory, then it already should be accessible, but if it's in the wc-logs folder, it will not be.
169        $download_url = wp_nonce_url( admin_url( 'admin.php?page=' . $this->settings->get_plugin_slug() . '&date=' . $date . '&download-log=true' ), 'bh-wp-logger-download' );
170        $filepath     = $log_files[ $chosen_date ];
171        $filename     = basename( $filepath );
172        // TODO: Show file size here. Show number of entries.
173        echo '<p>Displaying log file at <a href="' . esc_url( $download_url ) . '" download="' . esc_attr( $filename ) . '"><code>' . esc_html( $filepath ) . '</code></a></p>';
174
175        echo '<p>Display levels: ';
176
177        $log_level_counts = array(
178            LogLevel::ERROR   => 0,
179            LogLevel::WARNING => 0,
180            LogLevel::NOTICE  => 0,
181            LogLevel::INFO    => 0,
182            LogLevel::DEBUG   => 0,
183        );
184        foreach ( $logs_table->get_data() as $datum ) {
185            ++$log_level_counts[ strtolower( $datum['level'] ) ];
186        }
187
188        $checkboxes_first = true;
189        foreach ( $log_level_counts as $log_level => $log_level_count ) {
190            if ( ! $checkboxes_first ) {
191                echo ' • ';
192            }
193            $checkboxes_first = false;
194
195            $disabled       = 0 === $log_level_count ? 'disabled' : '';
196            $friendly_level = ucfirst( $log_level );
197            printf(
198                '<input %s class="log_level_display_checkbox" type="checkbox" id="log_level_display_checkbox_%s" name="log_level_display_checkbox_%s" checked>'
199                . '<label for="log_level_display_checkbox_%s">%s (%s)</label>',
200                esc_attr( $disabled ),
201                esc_attr( $log_level ),
202                esc_attr( $log_level ),
203                esc_attr( $log_level ),
204                esc_html( $friendly_level ),
205                intval( $log_level_count )
206            );
207        }
208
209        echo '</p>';
210
211        // TODO: Add an action here for other plugins to add controls.
212
213        $logs_table->prepare_items();
214        $logs_table->display();
215
216        echo '</div>';
217
218        $this->api->set_last_logs_view_time();
219    }
220
221    /**
222     * Enqueue the logs page JavaScript for changing date and deleting logs.
223     * Checks the plugin slug and only adds the script on the logs page for this plugin.
224     *
225     * @hooked admin_enqueue_scripts
226     */
227    public function enqueue_scripts(): void {
228
229        $slug        = $this->settings->get_plugin_slug();
230        $page_suffix = "_{$slug}-logs";
231
232        $current_page = get_current_screen();
233
234        /**
235         * `$current_page->id` will begin with `admin_page` or `$parent_slug` determined in `add_page()`.
236         *
237         * @see Logs_Page::add_page()
238         */
239        if ( is_null( $current_page ) || ! str_ends_with( $current_page->id, $page_suffix ) ) {
240            return;
241        }
242
243        // This is the bh-wp-logger JavaScript version, not the plugin version.
244        $version = '1.1.0';
245
246        $js_path = realpath( __DIR__ . '/../../' ) . '/assets/bh-wp-logger-admin.js';
247        $js_url  = plugin_dir_url( $js_path ) . 'bh-wp-logger-admin.js';
248
249        wp_enqueue_script( 'bh-wp-logger-admin-logs-page-' . $slug, $js_url, array( 'jquery', 'renderjson', 'colresizable' ), $version, true );
250
251        $renderjson_js_path = realpath( __DIR__ . '/../../' ) . '/assets/vendor/renderjson/renderjson.js';
252        $renderjson_js_url  = plugin_dir_url( $renderjson_js_path ) . 'renderjson.js';
253
254        wp_enqueue_script( 'renderjson', $renderjson_js_url, array(), '1.4', true );
255
256        $colresizable_js_path = realpath( __DIR__ . '/../../' ) . '/assets/vendor/colresizable/colResizable-1.6.min.js';
257        $colresizable_js_url  = plugin_dir_url( $colresizable_js_path ) . 'colResizable-1.6.min.js';
258
259        wp_enqueue_script( 'colresizable', $colresizable_js_url, array( 'jquery' ), '1.6', true );
260    }
261
262    /**
263     * Register the stylesheets for the logs page.
264     * (colours the rows with the severity of the log message!).
265     *
266     * @hooked admin_enqueue_scripts
267     */
268    public function enqueue_styles(): void {
269
270        $slug        = $this->settings->get_plugin_slug();
271        $page_suffix = "_{$slug}-logs";
272
273        $current_page = get_current_screen();
274
275        /**
276         * `$current_page->id` will begin with `admin_page` or `$parent_slug` determined in `add_page()`.
277         *
278         * @see Logs_Page::add_page()
279         */
280        if ( is_null( $current_page ) || ! str_ends_with( $current_page->id, $page_suffix ) ) {
281            return;
282        }
283
284        $handle = "{$this->settings->get_plugin_slug()}-logs";
285
286        $version = '1.0.0';
287
288        $css_path = realpath( __DIR__ . '/../../' ) . '/assets/bh-wp-logger.css';
289        $css_url  = plugin_dir_url( $css_path ) . 'bh-wp-logger.css';
290
291        wp_enqueue_style( $handle, $css_url, array(), $version );
292    }
293}