Passed
Push — master ( 35e88b...60b488 )
by Paul
17:33 queued 08:42
created

Console::getLevel()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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