Passed
Push — hotfix/fix-counts ( 428f33...c27bc0 )
by Paul
04:52
created

Console::getLevel()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace GeminiLabs\SiteReviews\Modules;
4
5
use DateTime;
6
use GeminiLabs\SiteReviews\Helper;
7
use GeminiLabs\SiteReviews\Modules\Session;
8
use ReflectionClass;
9
use Throwable;
10
11
class Console
12
{
13
	const DEBUG = 0;      // Detailed debug information
14
	const INFO = 1;       // Interesting events
15
	const NOTICE = 2;     // Normal but significant events
16
	const WARNING = 4;    // Exceptional occurrences that are not errors
17
	const ERROR = 8;      // Runtime errors that do not require immediate action
18
	const CRITICAL = 16;  // Critical conditions
19
	const ALERT = 32;     // Action must be taken immediately
20
	const EMERGENCY = 64; // System is unusable
21
22
	protected $file;
23
	protected $log;
24
	protected $logOnceKey = 'glsr_log_once';
25
26 7
	public function __construct()
27
	{
28 7
		$this->file = glsr()->path( 'console.log' );
29 7
		$this->log = file_exists( $this->file )
30 7
			? file_get_contents( $this->file )
31
			: '';
32 7
		$this->reset();
33 7
	}
34
35
	/**
36
	 * @return string
37
	 */
38
	public function __toString()
39
	{
40
		return $this->get();
41
	}
42
43
	/**
44
	 * Action must be taken immediately
45
	 * Example: Entire website down, database unavailable, etc. This should trigger the SMS alerts and wake you up
46
	 * @param mixed $message
47
	 * @param array $context
48
	 * @return static
49
	 */
50
	public function alert( $message, array $context = [] )
51
	{
52
		return $this->log( static::ALERT, $message, $context );
53
	}
54
55
	/**
56
	 * @return void
57
	 */
58
	public function clear()
59
	{
60
		$this->log = '';
61
		file_put_contents( $this->file, $this->log );
62
	}
63
64
	/**
65
	 * Critical conditions
66
	 * Example: Application component unavailable, unexpected exception
67
	 * @param mixed $message
68
	 * @param array $context
69
	 * @return static
70
	 */
71
	public function critical( $message, array $context = [] )
72
	{
73
		return $this->log( static::CRITICAL, $message, $context );
74
	}
75
76
	/**
77
	 * Detailed debug information
78
	 * @param mixed $message
79
	 * @param array $context
80
	 * @return static
81
	 */
82
	public function debug( $message, array $context = [] )
83
	{
84
		return $this->log( static::DEBUG, $message, $context );
85
	}
86
87
	/**
88
	 * System is unusable
89
	 * @param mixed $message
90
	 * @param array $context
91
	 * @return static
92
	 */
93
	public function emergency( $message, array $context = [] )
94
	{
95
		return $this->log( static::EMERGENCY, $message, $context );
96
	}
97
98
	/**
99
	 * Runtime errors that do not require immediate action but should typically be logged and monitored
100
	 * @param mixed $message
101
	 * @param array $context
102
	 * @return static
103
	 */
104
	public function error( $message, array $context = [] )
105
	{
106
		return $this->log( static::ERROR, $message, $context );
107
	}
108
109
	/**
110
	 * @return string
111
	 */
112
	public function get()
113
	{
114
		return empty( $this->log )
115
			? __( 'Console is empty', 'site-reviews' )
116
			: $this->log;
117
	}
118
119
	/**
120
	 * @return int
121
	 */
122 7
	public function getLevel()
123
	{
124 7
		return intval( apply_filters( 'site-reviews/console/level', static::INFO ));
125
	}
126
127
	/**
128
	 * @return array
129
	 */
130 7
	public function getLevels()
131
	{
132 7
		$constants = ( new ReflectionClass( __CLASS__ ))->getConstants();
133 7
		return array_map( 'strtolower', array_flip( $constants ));
134
	}
135
136
	/**
137
	 * @return string
138
	 */
139
	public function humanLevel()
140
	{
141
		$level = $this->getLevel();
142
		return sprintf( '%s (%d)', strtoupper( glsr_get( $this->getLevels(), $level, 'unknown' )), $level );
143
	}
144
145
	/**
146
	 * @param null|string $valueIfEmpty
147
	 * @return string
148
	 */
149
	public function humanSize( $valueIfEmpty = null )
150
	{
151
		$bytes = $this->size();
152
		if( empty( $bytes ) && is_string( $valueIfEmpty )) {
153
			return $valueIfEmpty;
154
		}
155
		$exponent = floor( log( max( $bytes, 1 ), 1024 ));
156
		return round( $bytes / pow( 1024, $exponent ), 2 ).' '.['bytes','KB','MB','GB'][$exponent];
157
	}
158
159
	/**
160
	 * Interesting events
161
	 * Example: User logs in, SQL logs
162
	 * @param mixed $message
163
	 * @param array $context
164
	 * @return static
165
	 */
166
	public function info( $message, array $context = [] )
167
	{
168
		return $this->log( static::INFO, $message, $context );
169
	}
170
171
	/**
172
	 * @param int $level
173
	 * @param mixed $message
174
	 * @param array $context
175
	 * @param string $backtraceLine
176
	 * @return static
177
	 */
178 7
	public function log( $level, $message, $context = [], $backtraceLine = '' )
179
	{
180 7
		if( empty( $backtraceLine )) {
181 7
			$backtraceLine = $this->getBacktraceLine();
182
		}
183 7
		if( $this->canLogEntry( $level, $backtraceLine )) {
184 7
			$levelName = glsr_get( $this->getLevels(), $level );
185 7
			$context = glsr( Helper::class )->consolidateArray( $context );
186 7
			$backtraceLine = $this->normalizeBacktraceLine( $backtraceLine );
187 7
			$message = $this->interpolate( $message, $context );
188 7
			$entry = $this->buildLogEntry( $levelName, $message, $backtraceLine );
189 7
			file_put_contents( $this->file, $entry.PHP_EOL, FILE_APPEND|LOCK_EX );
190 7
			apply_filters( 'console', $message, $levelName, $backtraceLine ); // Show in Blackbar plugin if installed
191 7
			$this->reset();
192
		}
193 7
		return $this;
194
	}
195
196
	/**
197
	 * @return void
198
	 */
199
	public function logOnce()
200
	{
201
		$once = glsr( Helper::class )->consolidateArray( glsr()->{$this->logOnceKey} );
202
		$levels = $this->getLevels();
203
		foreach( $once as $entry ) {
204
			$levelName = glsr_get( $entry, 'level' );
205
			if( !in_array( $levelName, $levels ))continue;
206
			$level = glsr_get( array_flip( $levels ), $levelName );
207
			$message = glsr_get( $entry, 'message' );
208
			$backtraceLine = glsr_get( $entry, 'backtrace' );
209
			$this->log( $level, $message, [], $backtraceLine );
210
		}
211
		glsr()->{$this->logOnceKey} = [];
212
	}
213
214
	/**
215
	 * Normal but significant events
216
	 * @param mixed $message
217
	 * @param array $context
218
	 * @return static
219
	 */
220 7
	public function notice( $message, array $context = [] )
221
	{
222 7
		return $this->log( static::NOTICE, $message, $context );
223
	}
224
225
	/**
226
	 * @param string $levelName
227
	 * @param string $handle
228
	 * @param mixed $data
229
	 * @return void
230
	 */
231
	public function once( $levelName, $handle, $data )
232
	{
233
		$once = glsr( Helper::class )->consolidateArray( glsr()->{$this->logOnceKey} );
234
		$filtered = array_filter( $once, function( $entry ) use( $level, $handle ) {
0 ignored issues
show
Unused Code introduced by
The import $level is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
Comprehensibility Best Practice introduced by
The variable $level does not exist. Did you maybe mean $levelName?
Loading history...
235
			return glsr_get( $entry, 'level' ) == $levelName
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $levelName does not exist. Did you maybe mean $level?
Loading history...
236
				&& glsr_get( $entry, 'handle' ) == $handle;
237
		});
238
		if( !empty( $filtered ))return;
239
		$once[] = [
240
			'backtrace' => $this->getBacktraceLineFromData( $data ),
241
			'handle' => $handle,
242
			'level' => $levelName,
243
			'message' => '[RECURRING] '.$this->getMessageFromData( $data ),
244
		];
245
		glsr()->{$this->logOnceKey} = $once;
246
	}
247
248
	/**
249
	 * @return int
250
	 */
251 7
	public function size()
252
	{
253 7
		return file_exists( $this->file )
254 7
			? filesize( $this->file )
255 7
			: 0;
256
	}
257
258
	/**
259
	 * Exceptional occurrences that are not errors
260
	 * Example: Use of deprecated APIs, poor use of an API, undesirable things that are not necessarily wrong
261
	 * @param mixed $message
262
	 * @param array $context
263
	 * @return static
264
	 */
265
	public function warning( $message, array $context = [] )
266
	{
267
		return $this->log( static::WARNING, $message, $context );
268
	}
269
270
	/**
271
	 * @param array $backtrace
272
	 * @param int $index
273
	 * @return string
274
	 */
275 7
	protected function buildBacktraceLine( $backtrace, $index )
276
	{
277 7
		return sprintf( '%s:%s',
278 7
			glsr_get( $backtrace, $index.'.file' ), // realpath
279 7
			glsr_get( $backtrace, $index.'.line' )
280
		);
281
	}
282
283
	/**
284
	 * @param string $levelName
285
	 * @param mixed $message
286
	 * @param string $backtraceLine
287
	 * @return string
288
	 */
289 7
	protected function buildLogEntry( $levelName, $message, $backtraceLine = '' )
290
	{
291 7
		return sprintf( '[%s] %s [%s] %s',
292 7
			current_time( 'mysql' ),
293 7
			strtoupper( $levelName ),
294 7
			$backtraceLine,
295 7
			$message
296
		);
297
	}
298
299
	/**
300
	 * @param int $level
301
	 * @return bool
302
	 */
303 7
	protected function canLogEntry( $level, $backtraceLine )
304
	{
305 7
		$levelExists = array_key_exists( $level, $this->getLevels() );
306 7
		if( strpos( $backtraceLine, glsr()->path() ) === false ) {
307
			return $levelExists; // ignore level restriction if triggered outside of the plugin
308
		}
309 7
		return $levelExists && $level >= $this->getLevel();
310
	}
311
312
	/**
313
	 * @return void|string
314
	 */
315 7
	protected function getBacktraceLine()
316
	{
317 7
		$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 6 );
318 7
		$search = array_search( 'log', glsr_array_column( $backtrace, 'function' ));
319 7
		if( $search !== false ) {
320 7
			$index = glsr_get( $backtrace, ( $search + 2 ).'.function' ) == '{closure}'
321
				? $search + 4
322 7
				: $search + 1;
323 7
			return $this->buildBacktraceLine( $backtrace, $index );
324
		}
325
		return 'Unknown';
326
	}
327
328
	/**
329
	 * @param mixed $data
330
	 * @return string
331
	 */
332
	protected function getBacktraceLineFromData( $data )
333
	{
334
		$backtrace = $data instanceof Throwable
335
			? $data->getTrace()
336
			: debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1 );
337
		return $this->buildBacktraceLine( $backtrace, 0 );
338
	}
339
340
	/**
341
	 * @param mixed $data
342
	 * @return string
343
	 */
344
	protected function getMessageFromData( $data )
345
	{
346
		return $data instanceof Throwable
347
			? $this->normalizeThrowableMessage( $data->getMessage() )
348
			: print_r( $data, 1 );
349
	}
350
351
	/**
352
	 * Interpolates context values into the message placeholders
353
	 * @param mixed $message
354
	 * @param array $context
355
	 * @return string
356
	 */
357 7
	protected function interpolate( $message, $context = [] )
358
	{
359 7
		if( $this->isObjectOrArray( $message ) || !is_array( $context )) {
360
			return print_r( $message, true );
361
		}
362 7
		$replace = [];
363 7
		foreach( $context as $key => $value ) {
364
			$replace['{'.$key.'}'] = $this->normalizeValue( $value );
365
		}
366 7
		return strtr( $message, $replace );
367
	}
368
369
	/**
370
	 * @param mixed $value
371
	 * @return bool
372
	 */
373 7
	protected function isObjectOrArray( $value )
374
	{
375 7
		return is_object( $value ) || is_array( $value );
376
	}
377
378
	/**
379
	 * @param string $backtraceLine
380
	 * @return string
381
	 */
382 7
	protected function normalizeBacktraceLine( $backtraceLine )
383
	{
384
		$search = [
385 7
			glsr()->path( 'plugin/' ),
386 7
			glsr()->path( 'plugin/', false ),
387 7
			trailingslashit( glsr()->path() ),
388 7
			trailingslashit( glsr()->path( '', false )),
389 7
			WP_CONTENT_DIR,
390 7
			ABSPATH
391
		];
392 7
		return str_replace( array_unique( $search ), '', $backtraceLine );
393
	}
394
395
	/**
396
	 * @param string $message
397
	 * @return string
398
	 */
399
	protected function normalizeThrowableMessage( $message )
400
	{
401
		$calledIn = strpos( $message, ', called in' );
402
		return $calledIn !== false
403
			? substr( $message, 0, $calledIn )
404
			: $message;
405
	}
406
407
	/**
408
	 * @param mixed $value
409
	 * @return string
410
	 */
411
	protected function normalizeValue( $value )
412
	{
413
		if( $value instanceof DateTime ) {
414
			$value = $value->format( 'Y-m-d H:i:s' );
415
		}
416
		else if( $this->isObjectOrArray( $value )) {
417
			$value = json_encode( $value );
418
		}
419
		return (string)$value;
420
	}
421
422
	/**
423
	 * @return void
424
	 */
425 7
	protected function reset()
426
	{
427 7
		if( $this->size() <= pow( 1024, 2 ) / 8 )return;
428
		$this->clear();
429
		file_put_contents(
430
			$this->file,
431
			$this->buildLogEntry(
432
				static::NOTICE,
433
				__( 'Console was automatically cleared (128 KB maximum size)', 'site-reviews' )
434
			)
435
		);
436
	}
437
}
438