Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
4.62% covered (danger)
4.62%
3 / 65
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Logger
4.62% covered (danger)
4.62%
3 / 65
0.00% covered (danger)
0.00%
0 / 8
567.39
0.00% covered (danger)
0.00%
0 / 1
 get_instance
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 prepare_file
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 debug
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
4.68
 log
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 read
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
110
 get_log_file
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 delete_old_logs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/*
3 * This file may be called before WordPress is fully initialized. See the README file for info.
4 */
5
6namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress;
7
8use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions\Filter_Older;
9use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions\Simple_Delete;
10
11/**
12 * A utility that manages logging for the boost cache.
13 */
14class Logger {
15    /**
16     * The singleton instance of the logger.
17     *
18     * @var self
19     */
20    private static $instance = null;
21
22    /**
23     * The header to place on top of every log file.
24     */
25    const LOG_HEADER = "<?php die(); // This file is not intended to be accessed directly. ?>\n\n";
26
27    /**
28     * The directory where log files are stored.
29     */
30    const LOG_DIRECTORY = WP_CONTENT_DIR . '/boost-cache/logs';
31
32    /**
33     * The Process Identifier used by this Logger instance.
34     *
35     * @var int|float
36     */
37    private $pid = null;
38
39    /**
40     * Get the singleton instance of the logger.
41     */
42    public static function get_instance() {
43        if ( self::$instance !== null ) {
44            return self::$instance;
45        }
46
47        $instance          = new Logger();
48        $prepared_log_file = $instance->prepare_file();
49        if ( $prepared_log_file instanceof Boost_Cache_Error ) {
50            return $prepared_log_file;
51        }
52
53        self::$instance = $instance;
54        return $instance;
55    }
56
57    private function __construct() {
58        if ( function_exists( 'getmypid' ) ) {
59            $this->pid = getmypid();
60        } else {
61            // Where PID is not available, use the microtime of the first log of the session.
62            $this->pid = microtime( true );
63        }
64    }
65
66    /**
67     * Ensure that the log file exists, and if not, create it.
68     */
69    private function prepare_file() {
70        $log_file = $this->get_log_file();
71        if ( file_exists( $log_file ) ) {
72            return true;
73        }
74
75        $directory = dirname( $log_file );
76        if ( ! Filesystem_Utils::create_directory( $directory ) ) {
77            return new Boost_Cache_Error( 'could-not-create-log-dir', 'Could not create boost cache log directory' );
78        }
79
80        return Filesystem_Utils::write_to_file( $log_file, self::LOG_HEADER );
81    }
82
83    /**
84     * Add a debug message to the log file after doing necessary checks.
85     */
86    public static function debug( $message ) {
87        $settings = Boost_Cache_Settings::get_instance();
88        if ( ! $settings->get_logging() ) {
89            return;
90        }
91
92        $logger = self::get_instance();
93
94        // TODO: Check to make sure that current request IP is allowed to create logs.
95
96        if ( $logger instanceof Boost_Cache_Error ) {
97            return;
98        }
99
100        $logger->log( $message );
101    }
102
103    /**
104     * Writes a message to the log file.
105     *
106     * @param string $message - The message to write to the log file.
107     */
108    public function log( $message ) {
109        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
110        $request_uri = htmlspecialchars( isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '<unknown request uri>', ENT_QUOTES, 'UTF-8' );
111
112        // don't log the ABSPATH constant. Logs may be copied to a public forum.
113        $message = str_replace( ABSPATH, '[...]/', $message );
114
115        // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
116        $line = json_encode(
117            array(
118                'time' => gmdate( 'Y-m-d H:i:s' ),
119                'pid'  => $this->pid,
120                'uri'  => $request_uri,
121                'msg'  => $message,
122                'uid'  => uniqid(), // Uniquely identify this log line.
123            ),
124            JSON_UNESCAPED_SLASHES
125        );
126
127        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
128        error_log( $line . PHP_EOL, 3, $this->get_log_file() );
129    }
130
131    /**
132     * Reads the log file and returns the contents.
133     *
134     * @return string
135     */
136    public static function read() {
137        $instance = self::get_instance();
138
139        // If we failed to set up a Logger instance (e.g.: unwriteable directory), return the error as log content.
140        if ( $instance instanceof Boost_Cache_Error ) {
141            return $instance->get_error_message();
142        }
143
144        $log_file = $instance->get_log_file();
145        if ( ! file_exists( $log_file ) ) {
146            return '';
147        }
148
149        // Get the content after skipping the LOG_HEADER.
150        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
151        $logs  = file_get_contents( $log_file, false, null, strlen( self::LOG_HEADER ) ) ?? '';
152        $logs  = explode( PHP_EOL, $logs );
153        $lines = array();
154
155        foreach ( $logs as $log ) {
156            $line = json_decode( $log, true );
157            if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $line ) ) {
158                continue;
159            }
160
161            // The current log format requires time, pid, uri, and msg.
162            if ( ! isset( $line['time'] ) || ! isset( $line['pid'] ) || ! isset( $line['uri'] ) || ! isset( $line['msg'] ) ) {
163                continue;
164            }
165
166            $info = sprintf(
167                '[%s] [%s] ',
168                $line['time'],
169                $line['pid']
170            );
171
172            $formatted = $info . $line['uri'];
173            // Add msg to the next line offset by the length of the info string.
174            $formatted .= PHP_EOL . str_repeat( ' ', strlen( $info ) ) . $line['msg'];
175
176            $lines[] = $formatted;
177        }
178
179        return implode( PHP_EOL, $lines );
180    }
181
182    /**
183     * Returns the path to the log file.
184     *
185     * @return string
186     */
187    private static function get_log_file() {
188        $today = gmdate( 'Y-m-d' );
189        return self::LOG_DIRECTORY . "/log-{$today}.log.php";
190    }
191
192    public static function delete_old_logs() {
193        Filesystem_Utils::iterate_directory( self::LOG_DIRECTORY, new Filter_Older( time() - 24 * 60 * 60, new Simple_Delete() ) );
194    }
195}