Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
41.30% covered (danger)
41.30%
19 / 46
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
Init
41.30% covered (danger)
41.30%
19 / 46
0.00% covered (danger)
0.00%
0 / 3
83.52
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
 maybe_download_log
90.48% covered (success)
90.48%
19 / 21
0.00% covered (danger)
0.00%
0 / 1
9.07
 send_private_file
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2/**
3 * Before any UI loads, check if the request is for a log file download.
4 *
5 * @package brianhenryie/bh-wp-logger
6 */
7
8namespace BrianHenryIE\WP_Logger\WP_Includes;
9
10use BrianHenryIE\WP_Logger\API_Interface;
11use BrianHenryIE\WP_Logger\Logger_Settings_Interface;
12use BrianHenryIE\WP_Private_Uploads\Frontend\Serve_Private_File;
13use Psr\Log\LoggerAwareTrait;
14use Psr\Log\LoggerInterface;
15
16/**
17 * Hook into init, return gracefully if this is not a log download request.
18 */
19class Init {
20    use LoggerAwareTrait;
21
22    /**
23     * Constructor.
24     *
25     * @param API_Interface             $api The logger's main functions, used to get the log file filepath for the requested date.
26     * @param Logger_Settings_Interface $settings The logger settings. Used to check the request is for a log file from this plugin.
27     * @param LoggerInterface           $logger The logger itself for logging.
28     */
29    public function __construct(
30        protected API_Interface $api,
31        protected Logger_Settings_Interface $settings,
32        LoggerInterface $logger
33    ) {
34        $this->setLogger( $logger );
35    }
36
37    /**
38     * Check is this a request to download a log.
39     * Return quietly if not.
40     * Fail hard if nonce is incorrect.
41     * Return if required parameters missing.
42     * Return if plugin slug does not match or if date is malformed.
43     * Invoke `send_private_file()` to download the file.
44     *
45     * This is really only needed when WooCommerce logger is being used because it store the log files in
46     * `/uploads/wc-logs` which has a `.htaccess` preventing downloads.
47     *
48     * @hooked init
49     */
50    public function maybe_download_log(): void {
51
52        if ( ! isset( $_GET['download-log'] ) ) {
53            return;
54        }
55
56        if ( false === check_admin_referer( 'bh-wp-logger-download' ) ) {
57            $this->logger->warning( 'Bad nonce when downloading log.' );
58            wp_die();
59            return; // Needed for tests. @phpstan-ignore-line.
60        }
61
62        if ( 'true' !== sanitize_text_field( wp_unslash( $_GET['download-log'] ) ) ) {
63            return;
64        }
65
66        if ( ! isset( $_GET['page'] ) || ! isset( $_GET['date'] ) ) {
67            return;
68        }
69
70        $page = sanitize_text_field( wp_unslash( $_GET['page'] ) );
71
72        if ( ! str_starts_with( $page, $this->settings->get_plugin_slug() ) ) {
73            return;
74        }
75
76        $date = sanitize_text_field( wp_unslash( $_GET['date'] ) );
77
78        if ( 1 !== preg_match( '/\d{4}-\d{2}-\d{2}/', $date ) ) {
79            return;
80        }
81
82        $files = $this->api->get_log_files( $date );
83
84        if ( ! isset( $files[ $date ] ) ) {
85            return;
86        }
87
88        $file = $files[ $date ];
89
90        $this->send_private_file( $file );
91    }
92
93    /**
94     * Set the correct headers, send the file, die.
95     *
96     * @param string $filepath The requested filename.
97     *
98     * @see Serve_Private_File::send_private_file()
99     *
100     * Nonce was checked above.
101     *
102     * phpcs:disable WordPress.Security.NonceVerification.Recommended
103     */
104    protected function send_private_file( string $filepath ): void {
105
106        // Add the mimetype header.
107        $mime     = wp_check_filetype( $filepath );  // This function just looks at the extension.
108        $mimetype = $mime['type'];
109        if ( ! $mimetype && function_exists( 'mime_content_type' ) ) {
110
111            $mimetype = mime_content_type( $filepath );  // Use ext-fileinfo to look inside the file.
112        }
113        if ( ! $mimetype ) {
114            $mimetype = 'application/octet-stream';
115        }
116
117        header( 'Content-type: ' . $mimetype ); // always send this.
118        header( 'Content-Disposition: attachment; filename="' . basename( $filepath ) . '"' );
119
120        // Add timing headers.
121        $date_format        = 'D, d M Y H:i:s T';  // RFC2616 date format for HTTP.
122        $last_modified_unix = (int) filemtime( $filepath );
123        $last_modified      = gmdate( $date_format, $last_modified_unix );
124        $etag               = md5( $last_modified );
125        header( "Last-Modified: $last_modified" );
126        header( 'ETag: "' . $etag . '"' );
127        header( 'Expires: ' . gmdate( $date_format, time() + HOUR_IN_SECONDS ) ); // an arbitrary hour from now.
128
129        // Support for caching.
130        $client_etag              = isset( $_REQUEST['HTTP_IF_NONE_MATCH'] ) ? trim( sanitize_text_field( wp_unslash( $_REQUEST['HTTP_IF_NONE_MATCH'] ) ) ) : '';
131        $client_if_mod_since      = isset( $_REQUEST['HTTP_IF_MODIFIED_SINCE'] ) ? trim( sanitize_text_field( wp_unslash( $_REQUEST['HTTP_IF_MODIFIED_SINCE'] ) ) ) : '';
132        $client_if_mod_since_unix = strtotime( $client_if_mod_since );
133
134        if ( $etag === $client_etag || $last_modified_unix <= $client_if_mod_since_unix ) {
135            // Return 'not modified' header.
136            status_header( 304 );
137            die();
138        }
139
140        // If we made it this far, just serve the file.
141        status_header( 200 );
142        // (WP_Filesystem is only loaded for admin requests, not applicable here).
143        // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_readfile
144        readfile( $filepath );
145        die();
146    }
147}