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
334.21
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
12
 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        /** @var string $pagenow */
57        global $pagenow;
58        /** @var string $plugin_page */
59        global $plugin_page;
60        if ( 'admin.php' !== $pagenow && $this->settings->get_plugin_slug() . '-logs' !== $plugin_page ) {
61            return;
62        }
63
64        /**
65         * @see strip_tags()
66         *
67         * phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
68         */
69        global $title;
70        $title = $this->settings->get_plugin_name() . ' Logs page';
71    }
72
73    /**
74     * Add a WordPress admin UI page, but without any menu linking to it.
75     *
76     * @hooked admin_menu
77     *
78     * @see wp-admin/menu.php
79     */
80    public function add_page(): void {
81
82        $logs_slug  = "{$this->settings->get_plugin_slug()}-logs";
83        $menu_title = 'Logs';
84
85        $parent_slug = '';
86
87        /** @var array<int,string[]> $menu */
88        global $menu;
89        foreach ( $menu as $menu_item ) {
90            if ( stristr( $menu_item[0], 'logs' ) || stristr( $menu_item[2], 'logs' ) || stristr( $menu_item[3], 'logs' ) ) {
91                $parent_slug = $menu_item[2];
92                $menu_title  = $this->settings->get_plugin_name();
93                break;
94            }
95        }
96
97        add_submenu_page(
98            $parent_slug,
99            __( 'Logs', 'bh-wp-logger' ),
100            $menu_title,
101            'manage_options',
102            $logs_slug,
103            array( $this, 'display_page' )
104        );
105    }
106
107    /**
108     * Display the page.
109     * Record the last visited time.
110     *
111     * Registered above.
112     *
113     * @see add_page()
114     */
115    public function display_page(): void {
116
117        echo '<div class="wrap">';
118
119        echo '<h1>';
120        echo esc_html( $this->settings->get_plugin_name() );
121        echo '</h1>';
122
123        $log_files = $this->api->get_log_files();
124
125        if ( empty( $log_files ) ) {
126            // This will occur e.g. immediately after deleting all logs.
127            echo '<p>No logs to display.</p>';
128            echo '</div>';
129            return;
130        }
131
132        $logs_table = new Logs_List_Table( $this->api, $this->settings, $this->logger );
133
134        // Set date here?
135
136        // Show a list of date to switch between dates.
137        echo '<label for="log_date">Log date:</label>';
138        echo '<select name="log_date" id="log_date">';
139
140        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
141        $chosen_date = isset( $_GET['log_date'] ) && is_string( $_GET['log_date'] ) ? sanitize_key( $_GET['log_date'] ) : array_key_last( $log_files );
142
143        // Maybe should use set file?
144        $logs_table->set_date( $chosen_date );
145
146        // TODO: Allow filtering here to add external log files, e.g. from Authorize.net SDK.
147        foreach ( $log_files as $date => $path ) {
148            $date_formatted = $date;
149            echo '<option value="' . esc_attr( $date ) . '"';
150            if ( $date === $chosen_date ) {
151                echo ' selected';
152            }
153            echo '>' . esc_html( $date_formatted ) . '</option>';
154        }
155        echo '</select>';
156
157        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>';
158        echo '<button name="deleteAllButton" id="deleteAllButton" class="button logs-page button-secondary">Delete all logs</button>';
159
160        wp_nonce_field( 'bh-wp-logger-delete', 'delete_logs_wpnonce' );
161
162        echo '<p>Current log level: <b>' . esc_html( ucfirst( $this->settings->get_log_level() ) ) . '</b></p>';
163
164        // 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.
165        $download_url = wp_nonce_url( admin_url( 'admin.php?page=' . $this->settings->get_plugin_slug() . '&date=' . $date . '&download-log=true' ), 'bh-wp-logger-download' );
166        $filepath     = $log_files[ $chosen_date ];
167        $filename     = basename( $filepath );
168        // TODO: Show file size here. Show number of entries.
169        echo '<p>Displaying log file at <a href="' . esc_url( $download_url ) . '" download="' . esc_attr( $filename ) . '"><code>' . esc_html( $filepath ) . '</code></a></p>';
170
171        echo '<p>Display levels: ';
172
173        $log_level_counts = array(
174            LogLevel::ERROR   => 0,
175            LogLevel::WARNING => 0,
176            LogLevel::NOTICE  => 0,
177            LogLevel::INFO    => 0,
178            LogLevel::DEBUG   => 0,
179        );
180        foreach ( $logs_table->get_data() as $datum ) {
181            ++$log_level_counts[ strtolower( $datum['level'] ) ];
182        }
183
184        $checkboxes_first = true;
185        foreach ( $log_level_counts as $log_level => $log_level_count ) {
186            if ( ! $checkboxes_first ) {
187                echo ' • ';
188            }
189            $checkboxes_first = false;
190
191            $disabled       = 0 === $log_level_count ? 'disabled' : '';
192            $friendly_level = ucfirst( $log_level );
193            printf(
194                '<input %s class="log_level_display_checkbox" type="checkbox" id="log_level_display_checkbox_%s" name="log_level_display_checkbox_%s" checked>'
195                . '<label for="log_level_display_checkbox_%s">%s (%s)</label>',
196                esc_attr( $disabled ),
197                esc_attr( $log_level ),
198                esc_attr( $log_level ),
199                esc_attr( $log_level ),
200                esc_html( $friendly_level ),
201                intval( $log_level_count )
202            );
203        }
204
205        echo '</p>';
206
207        // TODO: Add an action here for other plugins to add controls.
208
209        $logs_table->prepare_items();
210        $logs_table->display();
211
212        echo '</div>';
213
214        $this->api->set_last_logs_view_time();
215    }
216
217    /**
218     * Enqueue the logs page JavaScript for changing date and deleting logs.
219     * Checks the plugin slug and only adds the script on the logs page for this plugin.
220     *
221     * @hooked admin_enqueue_scripts
222     */
223    public function enqueue_scripts(): void {
224
225        $slug        = $this->settings->get_plugin_slug();
226        $page_suffix = "_{$slug}-logs";
227
228        $current_page = get_current_screen();
229
230        /**
231         * `$current_page->id` will begin with `admin_page` or `$parent_slug` determined in `add_page()`.
232         *
233         * @see Logs_Page::add_page()
234         */
235        if ( is_null( $current_page ) || ! str_ends_with( $current_page->id, $page_suffix ) ) {
236            return;
237        }
238
239        // This is the bh-wp-logger JavaScript version, not the plugin version.
240        $version = '1.1.0';
241
242        $js_path = realpath( __DIR__ . '/../../' ) . '/assets/bh-wp-logger-admin.js';
243        $js_url  = plugin_dir_url( $js_path ) . 'bh-wp-logger-admin.js';
244
245        wp_enqueue_script( 'bh-wp-logger-admin-logs-page-' . $slug, $js_url, array( 'jquery', 'renderjson', 'colresizable' ), $version, true );
246
247        $renderjson_js_path = realpath( __DIR__ . '/../../' ) . '/assets/vendor/renderjson/renderjson.js';
248        $renderjson_js_url  = plugin_dir_url( $renderjson_js_path ) . 'renderjson.js';
249
250        wp_enqueue_script( 'renderjson', $renderjson_js_url, array(), '1.4', true );
251
252        $colresizable_js_path = realpath( __DIR__ . '/../../' ) . '/assets/vendor/colresizable/colResizable-1.6.min.js';
253        $colresizable_js_url  = plugin_dir_url( $colresizable_js_path ) . 'colResizable-1.6.min.js';
254
255        wp_enqueue_script( 'colresizable', $colresizable_js_url, array( 'jquery' ), '1.6', true );
256    }
257
258    /**
259     * Register the stylesheets for the logs page.
260     * (colours the rows with the severity of the log message!).
261     *
262     * @hooked admin_enqueue_scripts
263     */
264    public function enqueue_styles(): void {
265
266        $slug        = $this->settings->get_plugin_slug();
267        $page_suffix = "_{$slug}-logs";
268
269        $current_page = get_current_screen();
270
271        /**
272         * `$current_page->id` will begin with `admin_page` or `$parent_slug` determined in `add_page()`.
273         *
274         * @see Logs_Page::add_page()
275         */
276        if ( is_null( $current_page ) || ! str_ends_with( $current_page->id, $page_suffix ) ) {
277            return;
278        }
279
280        $handle = "{$this->settings->get_plugin_slug()}-logs";
281
282        $version = '1.0.0';
283
284        $css_path = realpath( __DIR__ . '/../../' ) . '/assets/bh-wp-logger.css';
285        $css_url  = plugin_dir_url( $css_path ) . 'bh-wp-logger.css';
286
287        wp_enqueue_style( $handle, $css_url, array(), $version );
288    }
289}