Passed
Push — master ( ea4f63...197c27 )
by Paul
20:35 queued 09:52
created

Console::getRaw()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
cc 1
nc 1
nop 0
crap 2
1
<?php
2
3
namespace GeminiLabs\SiteReviews\Modules;
4
5
use BadMethodCallException;
6
use DateTime;
7
use GeminiLabs\SiteReviews\Helper;
8
use GeminiLabs\SiteReviews\Helpers\Arr;
9
use GeminiLabs\SiteReviews\Helpers\Cast;
10
use GeminiLabs\SiteReviews\Helpers\Str;
11
use GeminiLabs\SiteReviews\Modules\Backtrace;
12
use ReflectionClass;
13
use Throwable;
14
15
/**
16
 * @method static debug($message, $context = [])
17
 * @method static info($message, $context = [])
18
 * @method static notice($message, $context = [])
19
 * @method static warning($message, $context = [])
20
 * @method static error($message, $context = [])
21
 * @method static critical($message, $context = [])
22
 * @method static alert($message, $context = [])
23
 * @method static emergency($message, $context = [])
24
 */
25
class Console
26
{
27
    const DEBUG = 0;      // Detailed debug information
28
    const INFO = 1;       // Interesting events
29
    const NOTICE = 2;     // Normal but significant events
30
    const WARNING = 4;    // Exceptional occurrences that are not errors
31
    const ERROR = 8;      // Runtime errors that do not require immediate action
32
    const CRITICAL = 16;  // Critical conditions
33
    const ALERT = 32;     // Action must be taken immediately
34
    const EMERGENCY = 64; // System is unusable
35
36
    const LOG_LEVEL_KEY = 'glsr_console_level';
37
    const LOG_ONCE_KEY = 'glsr_log_once';
38
39
    protected $file;
40
    protected $log;
41
42 14
    public function __construct()
43
    {
44 14
        $this->setLogFile();
45 14
        $this->reset();
46 14
    }
47
48 14
    public function __call($method, $args)
49
    {
50 14
        $constant = strtoupper($method);
51 14
        $instance = new \ReflectionClass($this);
52 14
        if ($instance->hasConstant($constant)) {
53 14
            $args = Arr::prepend($args, $instance->getConstant($constant));
54 14
            return call_user_func_array([$this, 'log'], array_slice($args, 0, 3));
55
        }
56
        throw new BadMethodCallException("Method [$method] does not exist.");
57
    }
58
59
    /**
60
     * @return string
61
     */
62
    public function __toString()
63
    {
64
        return $this->get();
65
    }
66
67
    /**
68
     * @return void
69
     */
70
    public function clear()
71
    {
72
        $this->log = '';
73
        file_put_contents($this->file, $this->log);
74
    }
75
76
    /**
77
     * @return string
78
     */
79
    public function get()
80
    {
81
        return esc_html(
82
            Helper::ifEmpty($this->log, _x('Console is empty', 'admin-text', 'site-reviews'))
83
        );
84
    }
85
86
    /**
87
     * @return string
88
     */
89
    public function getRaw()
90
    {
91
        return htmlspecialchars_decode($this->get(), ENT_QUOTES);
92
    }
93
94
    /**
95
     * @return int
96
     */
97
    public function getLevel()
98
    {
99
        $level = Cast::toInt(get_option(static::LOG_LEVEL_KEY, static::INFO));
100
        $levels = [
101
            static::ALERT, static::CRITICAL, static::DEBUG, static::EMERGENCY,
102
            static::ERROR, static::INFO, static::NOTICE, static::WARNING,
103
        ];
104
        if (in_array($level, $levels)) {
105
            return $level;
106
        }
107
        return static::INFO;
108
    }
109
110
    /**
111
     * @return array
112
     */
113 14
    public function getLevels()
114
    {
115 14
        $constants = (new ReflectionClass(__CLASS__))->getConstants();
116 14
        return array_map('strtolower', array_flip($constants));
117
    }
118
119
    /**
120
     * @return string
121
     */
122
    public function humanLevel()
123
    {
124
        $level = $this->getLevel();
125
        return sprintf('%s (%d)', strtoupper(Arr::get($this->getLevels(), $level, 'unknown')), $level);
126
    }
127
128
    /**
129
     * @return string
130
     */
131
    public function humanSize()
132
    {
133
        return Str::replaceLast(' B', ' bytes', Cast::toString(size_format($this->size())));
134
    }
135
136
    /**
137
     * @param int $level
138
     * @param mixed $message
139
     * @param array $context
140
     * @param string $backtraceLine
141
     * @return static
142
     */
143 14
    public function log($level, $message, $context = [], $backtraceLine = '')
144
    {
145 14
        if (empty($backtraceLine)) {
146 14
            $backtraceLine = glsr(Backtrace::class)->line();
147
        }
148 14
        if ($this->canLogEntry($level, $backtraceLine)) {
149 14
            $levelName = Arr::get($this->getLevels(), $level);
150 14
            $backtraceLine = glsr(Backtrace::class)->normalizeLine($backtraceLine);
151 14
            $message = $this->interpolate($message, $context);
152 14
            $entry = $this->buildLogEntry($levelName, $message, $backtraceLine);
153 14
            file_put_contents($this->file, $entry.PHP_EOL, FILE_APPEND | LOCK_EX);
154 14
            apply_filters('console', $message, $levelName, $backtraceLine); // Show in Blackbar plugin if installed
155 14
            $this->reset();
156
        }
157 14
        return $this;
158
    }
159
160
    /**
161
     * @return void
162
     */
163
    public function logOnce()
164
    {
165
        $once = glsr()->retrieveAs('array', static::LOG_ONCE_KEY);
166
        $levels = $this->getLevels();
167
        foreach ($once as $entry) {
168
            $levelName = Arr::get($entry, 'level');
169
            if (in_array($levelName, $levels)) {
170
                $level = Arr::get(array_flip($levels), $levelName);
171
                $message = Arr::get($entry, 'message');
172
                $backtraceLine = Arr::get($entry, 'backtrace');
173
                $this->log($level, $message, [], $backtraceLine);
174
            }
175
        }
176
        glsr()->store(static::LOG_ONCE_KEY, []);
177
    }
178
179
    /**
180
     * @param string $levelName
181
     * @param string $handle
182
     * @param mixed $data
183
     * @return void
184
     */
185
    public function once($levelName, $handle, $data)
186
    {
187
        $once = glsr()->retrieveAs('array', static::LOG_ONCE_KEY);
188
        $filtered = array_filter($once, function ($entry) use ($levelName, $handle) {
189
            return Arr::get($entry, 'level') == $levelName
190
                && Arr::get($entry, 'handle') == $handle;
191
        });
192
        if (empty($filtered)) {
193
            $once[] = [
194
                'backtrace' => glsr(Backtrace::class)->lineFromData($data),
195
                'handle' => $handle,
196
                'level' => $levelName,
197
                'message' => '[RECURRING] '.$this->getMessageFromData($data),
198
            ];
199
            glsr()->store(static::LOG_ONCE_KEY, $once);
200
        }
201
    }
202
203
    /**
204
     * @return int
205
     */
206 14
    public function size()
207
    {
208 14
        return file_exists($this->file)
209 14
            ? filesize($this->file)
210 14
            : 0;
211
    }
212
213
    /**
214
     * @param string $levelName
215
     * @param mixed $message
216
     * @param string $backtraceLine
217
     * @return string
218
     */
219 14
    protected function buildLogEntry($levelName, $message, $backtraceLine = '')
220
    {
221 14
        return sprintf('[%s] %s [%s] %s',
222 14
            current_time('mysql'),
223 14
            strtoupper($levelName),
224
            $backtraceLine,
225 14
            esc_html($message)
226
        );
227
    }
228
229
    /**
230
     * @param int $level
231
     * @return bool
232
     */
233 14
    protected function canLogEntry($level, $backtraceLine)
234
    {
235 14
        $levelExists = array_key_exists($level, $this->getLevels());
236 14
        if (!Str::contains(glsr()->path(), $backtraceLine)) {
237 14
            return $levelExists; // ignore level restriction if triggered outside of the plugin
238
        }
239
        return $levelExists && $level >= $this->getLevel();
240
    }
241
242
    /**
243
     * @param mixed $data
244
     * @return string
245
     */
246
    protected function getMessageFromData($data)
247
    {
248
        return $data instanceof Throwable
249
            ? $this->normalizeThrowableMessage($data->getMessage())
250
            : print_r($data, true);
251
    }
252
253
    /**
254
     * Interpolates context values into the message placeholders.
255
     * @param mixed $message
256
     * @param array $context
257
     * @return string
258
     */
259 14
    protected function interpolate($message, $context = [])
260
    {
261 14
        $context = Arr::consolidate($context);
262 14
        if (!is_scalar($message) || empty($context)) {
263 14
            return print_r($message, true);
264
        }
265
        $replace = [];
266
        foreach ($context as $key => $value) {
267
            $replace['{'.$key.'}'] = $this->normalizeValue($value);
268
        }
269
        return strtr($message, $replace);
270
    }
271
272
    /**
273
     * @param string $message
274
     * @return string
275
     */
276
    protected function normalizeThrowableMessage($message)
277
    {
278
        $calledIn = strpos($message, ', called in');
279
        return false !== $calledIn
280
            ? substr($message, 0, $calledIn)
281
            : $message;
282
    }
283
284
    /**
285
     * @param mixed $value
286
     * @return string
287
     */
288
    protected function normalizeValue($value)
289
    {
290
        if ($value instanceof DateTime) {
291
            $value = $value->format('Y-m-d H:i:s');
292
        } elseif (!is_scalar($value)) {
293
            $value = json_encode($value);
294
        }
295
        return (string) $value;
296
    }
297
298
    /**
299
     * @return void
300
     */
301 14
    protected function reset()
302
    {
303 14
        if ($this->size() <= wp_convert_hr_to_bytes('512kb')) {
304 14
            return;
305
        }
306
        $this->clear();
307
        file_put_contents($this->file,
308
            $this->buildLogEntry(static::NOTICE,
309
                _x('Console was automatically cleared (512KB maximum size)', 'admin-text', 'site-reviews')
310
            )
311
        );
312
    }
313
314
    /**
315
     * @return void
316
     */
317 14
    protected function setLogFile()
318
    {
319 14
        require_once ABSPATH.WPINC.'/pluggable.php';
320 14
        $uploads = wp_upload_dir();
321 14
        if (!file_exists($uploads['basedir'])) {
322
            $uploads = wp_upload_dir(null, true, true); // maybe the site has been moved, so refresh the cached uploads path
323
        }
324 14
        $base = trailingslashit($uploads['basedir'].'/'.glsr()->id);
325 14
        $this->file = $base.'logs/'.sanitize_file_name('console-'.wp_hash(glsr()->id).'.log');
326
        $files = [
327 14
            $base.'index.php' => '<?php',
328 14
            $base.'logs/.htaccess' => 'deny from all',
329 14
            $base.'logs/index.php' => '<?php',
330 14
            $this->file => '',
331
        ];
332 14
        foreach ($files as $file => $contents) {
333 14
            if (wp_mkdir_p(dirname($file)) && !file_exists($file)) {
334 1
                file_put_contents($file, $contents);
335
            }
336
        }
337 14
        $this->log = file_get_contents($this->file);
338 14
    }
339
}
340