Test Failed
Push — hotfix/fix-counts ( 1fe4ce...872cd6 )
by Paul
03:14
created

Console::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 0
dl 0
loc 7
ccs 6
cts 6
cp 1
crap 2
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 = 'debug';         // Detailed debug information
14
	const INFO_1 = 'info';           // Interesting events
15
	const NOTICE_2 = 'notice';       // Normal but significant events
16
	const WARNING_3 = 'warning';     // Exceptional occurrences that are not errors
17
	const ERROR_4 = 'error';         // Runtime errors that do not require immediate action
18
	const CRITICAL_5 = 'critical';   // Critical conditions
19
	const ALERT_6 = 'alert';         // Action must be taken immediately
20
	const EMERGENCY_7 = 'emergency'; // System is unusable
21
22
	protected $file;
23
	protected $log;
24
	protected $onceSessionKey = 'glsr_log_once';
25
26 6
	public function __construct()
27
	{
28 6
		$this->file = glsr()->path( 'console.log' );
29 6
		$this->log = file_exists( $this->file )
30 5
			? file_get_contents( $this->file )
31 1
			: '';
32 6
		$this->reset();
33 6
	}
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_6, $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_5, $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_0, $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_7, $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_4, $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 6
	public function getLevel()
123
	{
124 6
		return intval( apply_filters( 'site-reviews/console/level', 2 ));
125
	}
126
127
	/**
128
	 * @return array
129
	 */
130 6
	public function getLevels()
131
	{
132 6
		return array_values(( new ReflectionClass( __CLASS__ ))->getConstants() );
133
	}
134
135
	/**
136
	 * @return string
137
	 */
138
	public function humanLevel()
139
	{
140
		$level = $this->getLevel();
141
		return sprintf( '%s (%d)', glsr_get( $this->getLevels(), $level, 'unknown' ), $level );
142
	}
143
144
	/**
145
	 * @param null|string $valueIfEmpty
146
	 * @return string
147
	 */
148
	public function humanSize( $valueIfEmpty = null )
149
	{
150
		$bytes = $this->size();
151
		if( empty( $bytes ) && is_string( $valueIfEmpty )) {
152
			return $valueIfEmpty;
153
		}
154
		$exponent = floor( log( max( $bytes, 1 ), 1024 ));
155
		return round( $bytes / pow( 1024, $exponent ), 2 ).' '.['bytes','KB','MB','GB'][$exponent];
156
	}
157
158
	/**
159
	 * Interesting events
160
	 * Example: User logs in, SQL logs
161
	 * @param mixed $message
162
	 * @param array $context
163
	 * @return static
164
	 */
165 5
	public function info( $message, array $context = [] )
166
	{
167 5
		return $this->log( static::INFO_1, $message, $context );
168
	}
169
170
	/**
171
	 * @param mixed $level
172
	 * @param mixed $message
173
	 * @param array $context
174
	 * @param string $backtraceLine
175
	 * @return static
176
	 */
177 6
	public function log( $level, $message, $context = [], $backtraceLine = '' )
178
	{
179 6
		if( empty( $backtraceLine )) {
180 6
			$backtraceLine = $this->getBacktraceLine();
181
		}
182 6
		if( $this->canLogEntry( $level, $backtraceLine )) {
183 1
			$context = glsr( Helper::class )->consolidateArray( $context );
184 1
			$backtraceLine = $this->normalizeBacktraceLine( $backtraceLine );
185 1
			$message = $this->interpolate( $message, $context );
186 1
			$entry = $this->buildLogEntry( $level, $message, $backtraceLine );
187 1
			file_put_contents( $this->file, $entry.PHP_EOL, FILE_APPEND|LOCK_EX );
188 1
			apply_filters( 'console', $message, $level, $backtraceLine ); // Show in Blackbar plugin if installed
189 1
			$this->reset();
190
		}
191 6
		return $this;
192
	}
193
194
	/**
195
	 * @return void
196
	 */
197
	public function logOnce()
198
	{
199
		$once = glsr( Session::class )->get( $this->onceSessionKey, [], true );
200
		$once = glsr( Helper::class )->consolidateArray( $once );
201
		$levels = $this->getLevels();
202
		foreach( $once as $entry ) {
203
			if( !in_array( glsr_get( $entry, 'level' ), $levels ))continue;
204
			$level = glsr_get( $entry, 'level' );
205
			$message = glsr_get( $entry, 'message' );
206
			$backtraceLine = glsr_get( $entry, 'backtrace' );
207
			$this->log( $level, $message, [], $backtraceLine );
208
		}
209
	}
210
211
	/**
212
	 * Normal but significant events
213
	 * @param mixed $message
214
	 * @param array $context
215
	 * @return static
216
	 */
217 1
	public function notice( $message, array $context = [] )
218
	{
219 1
		return $this->log( static::NOTICE_2, $message, $context );
220
	}
221
222
	/**
223
	 * @param string $level
224
	 * @param string $handle
225
	 * @param mixed $data
226
	 * @return void
227
	 */
228
	public function once( $level, $handle, $data )
229
	{
230
		$once = glsr( Session::class )->get( $this->onceSessionKey, [] );
231
		$once = glsr( Helper::class )->consolidateArray( $once );
232
		$filtered = array_filter( $once, function( $entry ) use( $level, $handle ) {
233
			return glsr_get( $entry, 'level' ) == $level
234
				&& glsr_get( $entry, 'handle' ) == $handle;
235
		});
236
		if( !empty( $filtered ))return;
237
		$once[] = [
238
			'backtrace' => $this->getBacktraceLineFromData( $data ),
239
			'handle' => $handle,
240
			'level' => $level,
241
			'message' => '[RECURRING] '.$this->getMessageFromData( $data ),
242
		];
243
		glsr( Session::class )->set( $this->onceSessionKey, $once );
244
	}
245
246
	/**
247
	 * @return int
248
	 */
249 6
	public function size()
250
	{
251 6
		return file_exists( $this->file )
252 6
			? filesize( $this->file )
253 6
			: 0;
254
	}
255
256
	/**
257
	 * Exceptional occurrences that are not errors
258
	 * Example: Use of deprecated APIs, poor use of an API, undesirable things that are not necessarily wrong
259
	 * @param mixed $message
260
	 * @param array $context
261
	 * @return static
262
	 */
263
	public function warning( $message, array $context = [] )
264
	{
265
		return $this->log( static::WARNING_3, $message, $context );
266
	}
267
268
	/**
269
	 * @param array $backtrace
270
	 * @param int $index
271
	 * @return string
272
	 */
273 6
	protected function buildBacktraceLine( $backtrace, $index )
274
	{
275 6
		return sprintf( '%s:%s',
276 6
			glsr_get( $backtrace, $index.'.file' ), // realpath
277 6
			glsr_get( $backtrace, $index.'.line' )
278
		);
279
	}
280
281
	/**
282
	 * @param string $level
283
	 * @param mixed $message
284
	 * @param string $backtraceLine
285
	 * @return string
286
	 */
287 1
	protected function buildLogEntry( $level, $message, $backtraceLine = '' )
288
	{
289 1
		return sprintf( '[%s] %s [%s] %s',
290 1
			current_time( 'mysql' ),
291 1
			strtoupper( $level ),
292 1
			$backtraceLine,
293 1
			$message
294
		);
295
	}
296
297
	/**
298
	 * @param string $level
299
	 * @return bool
300
	 */
301 6
	protected function canLogEntry( $level, $backtraceLine )
302
	{
303 6
		$levelIndex = array_search( $level, $this->getLevels(), true );
304 6
		$result = $levelIndex !== false;
305 6
		if( strpos( $backtraceLine, glsr()->path() ) === false ) {
306
			return $result; // triggered outside of the plugin
307
		}
308 6
		return $result && $levelIndex >= $this->getLevel();
309
	}
310
311
	/**
312
	 * @return void|string
313
	 */
314 6
	protected function getBacktraceLine()
315
	{
316 6
		$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 6 );
317 6
		$search = array_search( 'log', glsr_array_column( $backtrace, 'function' ));
318 6
		if( $search !== false ) {
319 6
			$index = glsr_get( $backtrace, ( $search + 2 ).'.function' ) == '{closure}'
320
				? $search + 4
321 6
				: $search + 1;
322 6
			return $this->buildBacktraceLine( $backtrace, $index );
323
		}
324
		return 'Unknown';
325
	}
326
327
	/**
328
	 * @param mixed $data
329
	 * @return string
330
	 */
331
	protected function getBacktraceLineFromData( $data )
332
	{
333
		$backtrace = $data instanceof Throwable
334
			? $data->getTrace()
335
			: debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1 );
336
		return $this->buildBacktraceLine( $backtrace, 0 );
337
	}
338
339
	/**
340
	 * @param mixed $data
341
	 * @return string
342
	 */
343
	protected function getMessageFromData( $data )
344
	{
345
		return $data instanceof Throwable
346
			? $this->normalizeThrowableMessage( $data->getMessage() )
347
			: print_r( $data, 1 );
348
	}
349
350
	/**
351
	 * Interpolates context values into the message placeholders
352
	 * @param mixed $message
353
	 * @param array $context
354
	 * @return string
355
	 */
356 1
	protected function interpolate( $message, $context = [] )
357
	{
358 1
		if( $this->isObjectOrArray( $message ) || !is_array( $context )) {
359
			return print_r( $message, true );
360
		}
361 1
		$replace = [];
362 1
		foreach( $context as $key => $value ) {
363
			$replace['{'.$key.'}'] = $this->normalizeValue( $value );
364
		}
365 1
		return strtr( $message, $replace );
366
	}
367
368
	/**
369
	 * @param mixed $value
370
	 * @return bool
371
	 */
372 1
	protected function isObjectOrArray( $value )
373
	{
374 1
		return is_object( $value ) || is_array( $value );
375
	}
376
377
	/**
378
	 * @param string $backtraceLine
379
	 * @return string
380
	 */
381 1
	protected function normalizeBacktraceLine( $backtraceLine )
382
	{
383
		$search = [
384 1
			glsr()->path( 'plugin/' ),
385 1
			glsr()->path( 'plugin/', false ),
386 1
			trailingslashit( glsr()->path() ),
387 1
			trailingslashit( glsr()->path( '', false )),
388 1
			WP_CONTENT_DIR,
389 1
			ABSPATH
390
		];
391 1
		return str_replace( array_unique( $search ), '', $backtraceLine );
392
	}
393
394
	/**
395
	 * @param string $message
396
	 * @return string
397
	 */
398
	protected function normalizeThrowableMessage( $message )
399
	{
400
		$calledIn = strpos( $message, ', called in' );
401
		return $calledIn !== false
402
			? substr( $message, 0, $calledIn )
403
			: $message;
404
	}
405
406
	/**
407
	 * @param mixed $value
408
	 * @return string
409
	 */
410
	protected function normalizeValue( $value )
411
	{
412
		if( $value instanceof DateTime ) {
413
			$value = $value->format( 'Y-m-d H:i:s' );
414
		}
415
		else if( $this->isObjectOrArray( $value )) {
416
			$value = json_encode( $value );
417
		}
418
		return (string)$value;
419
	}
420
421
	/**
422
	 * @return void
423
	 */
424 6
	protected function reset()
425
	{
426 6
		if( $this->size() <= pow( 1024, 2 ) / 8 )return;
427
		$this->clear();
428
		file_put_contents(
429
			$this->file,
430
			$this->buildLogEntry(
431
				static::INFO_1,
432
				__( 'Console was automatically cleared (128 KB maximum size)', 'site-reviews' )
433
			)
434
		);
435
	}
436
}
437