Completed
Push — master ( d0941c...c232a4 )
by Richard
14s
created

Logger::addLogger()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 1
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
ccs 5
cts 5
cp 1
crap 3
1
<?php
2
/*
3
 You may not change or alter any portion of this comment or credits
4
 of supporting developers from this source code or any supporting source code
5
 which is considered copyrighted (c) material of the original comment or credit authors.
6
7
 This program is distributed in the hope that it will be useful,
8
 but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
*/
11
12
namespace Xoops\Core;
13
14
use Psr\Log\LogLevel;
15
use Psr\Log\LoggerInterface;
16
17
/**
18
 * Xoops\Core\Logger - dispatch log requests to any registered loggers.
19
 *
20
 * No logging is done in this class, but any logger, implemented as a
21
 * module or extension, can register as a logger using the addLogger()
22
 * method. Multiple loggers can be registered, and each will be
23
 * invoked in turn for each log() call.
24
 *
25
 * Such loggers are expected to implement the PSR-3 LoggerInterface.
26
 * In addition, any logger that generates output as part of the XOOPS
27
 * delivered page should implement the quiet() method, to disable output.
28
 *
29
 * Loggers are managed this way so that any routine may easily add a
30
 * log entry without needing to know any details of the implementation.
31
 *
32
 * Not all events are published through this mechanism, only specific requests
33
 * to log() or related methods. Individual loggers may listen for events (i.e.
34
 * preloads) or other sources and gain access to detailed debugging information.
35
 *
36
 * @category  Xoops\Core\Logger
37
 * @package   Logger
38
 * @author    Richard Griffith <[email protected]>
39
 * @copyright 2013 XOOPS Project (http://xoops.org)
40
 * @license   GNU GPL 2 or later (http://www.gnu.org/licenses/gpl-2.0.html)
41
 * @version   Release: 2.6.0
42
 * @link      http://xoops.org
43
 * @since     2.6.0
44
 */
45
class Logger implements LoggerInterface
46
{
47
    /**
48
     * @var LoggerInterface[] chain of PSR-3 compatible loggers to call
49
     */
50
    private $loggers = array();
51
52
    /**
53
     * @var boolean do we have active loggers?
54
     */
55
    private $logging_active = false;
56
57
    /**
58
     * @var boolean just to prevent fatal legacy errors. Does nothing. Stop it!
59
     */
60
    //public $activated = false;
61
62
    /**
63
     * Get the Xoops\Core\Logger instance
64
     *
65
     * @return Logger object
66
     */
67 18
    public static function getInstance()
68
    {
69 18
        static $instance;
70 18
        if (!isset($instance)) {
71
            $class = __CLASS__;
72
            $instance = new $class();
73
            // Always catch errors, for security reasons
74
            set_error_handler(array($instance, 'handleError'));
75
            // grab any uncaught exception
76
            set_exception_handler(array($instance, 'handleException'));
77
        }
78
79 18
        return $instance;
80
    }
81
82
    /**
83
     * Error handling callback.
84
     *
85
     * This will
86
     *
87
     * @param integer $errorNumber error number
88
     * @param string  $errorString error message
89
     * @param string  $errorFile   file
90
     * @param integer $errorLine   line number
91
     *
92
     * @return void
93
     */
94 58
    public function handleError($errorNumber, $errorString, $errorFile, $errorLine)
95
    {
96 58
        if ($this->logging_active && ($errorNumber & error_reporting())) {
97
98
            // if an error occurs before a locale is established,
99
            // we still need messages, so check and deal with it
100
101 5
            $msg = ': ' . sprintf(
102 5
                (class_exists('\XoopsLocale', false) ? \XoopsLocale::EF_LOGGER_FILELINE : "%s in file %s line %s"),
103 5
                $this->sanitizePath($errorString),
104 5
                $this->sanitizePath($errorFile),
105 5
                $errorLine
106
            );
107
108
            switch ($errorNumber) {
109 5 View Code Duplication
                case E_USER_NOTICE:
110 1
                    $msg = (class_exists('\XoopsLocale', false) ? \XoopsLocale::E_LOGGER_ERROR : '*Error:') . $msg;
111 1
                    $this->log(LogLevel::NOTICE, $msg);
112 1
                    break;
113 4 View Code Duplication
                case E_NOTICE:
114 1
                    $msg = (class_exists('\XoopsLocale', false) ? \XoopsLocale::E_LOGGER_NOTICE : '*Notice:') . $msg;
115 1
                    $this->log(LogLevel::NOTICE, $msg);
116 1
                    break;
117 3 View Code Duplication
                case E_WARNING:
118 1
                    $msg = (class_exists('\XoopsLocale', false) ? \XoopsLocale::E_LOGGER_WARNING : '*Warning:') . $msg;
119 1
                    $this->log(LogLevel::WARNING, $msg);
120 1
                    break;
121 2 View Code Duplication
                case E_STRICT:
122 1
                    $msg = (class_exists('\XoopsLocale', false) ? \XoopsLocale::E_LOGGER_STRICT : '*Strict:') . $msg;
123 1
                    $this->log(LogLevel::WARNING, $msg);
124 1
                    break;
125 1 View Code Duplication
                case E_USER_ERROR:
126
                    $msg = (class_exists('\XoopsLocale', false) ? \XoopsLocale::E_LOGGER_ERROR : '*Error:') . $msg;
127
                    @$this->log(LogLevel::CRITICAL, $msg);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
128
                    break;
129 View Code Duplication
                default:
130 1
                    $msg = (class_exists('\XoopsLocale', false) ? \XoopsLocale::E_LOGGER_UNKNOWN : '*Unknown:') . $msg;
131 1
                    $this->log(LogLevel::ERROR, $msg);
132 1
                    break;
133
            }
134
        }
135
136 58
        if ($errorNumber == E_USER_ERROR) {
137
            $trace = true;
138
            if (substr($errorString, 0, '8') === 'notrace:') {
139
                $trace = false;
140
                $errorString = substr($errorString, 8);
141
            }
142
            $this->reportFatalError($errorString);
143
            if ($trace) {
144
                $trace = debug_backtrace();
145
                array_shift($trace);
146
                if ('cli' === php_sapi_name()) {
147
                    foreach ($trace as $step) {
148
                        if (isset($step['file'])) {
149
                            fprintf(STDERR, "%s (%d)\n", $this->sanitizePath($step['file']), $step['line']);
150
                        }
151
                    }
152
                } else {
153
                    echo "<div style='color:#f0f0f0;background-color:#f0f0f0'>" . _XOOPS_FATAL_BACKTRACE . ":<br />";
154
                    foreach ($trace as $step) {
155
                        if (isset($step['file'])) {
156
                            printf("%s (%d)\n<br />", $this->sanitizePath($step['file']), $step['line']);
157
                        }
158
                    }
159
                    echo '</div>';
160
                }
161
            }
162
            exit();
0 ignored issues
show
Coding Style Compatibility introduced by
The method handleError() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
163
        }
164 58
    }
165
166
    /**
167
     * Exception handling callback.
168
     *
169
     * @param \Exception|\Throwable $e uncaught Exception or Error
170
     *
171
     * @return void
172
     */
173
    public function handleException($e)
174
    {
175
        if ($this->isThrowable($e)) {
176
            $msg = $e->getMessage();
0 ignored issues
show
Bug introduced by
The method getMessage does only exist in Exception, but not in Throwable.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
177
            $this->reportFatalError($msg);
178
        }
179
    }
180
181
    /**
182
     * Determine if an object implements Throwable (or is an Exception that would under PHP 7.)
183
     *
184
     * @param mixed $e Expected to be an object related to Exception or Throwable
185
     *
186
     * @return bool true if related to Throwable or Exception, otherwise false
187
     */
188
    protected function isThrowable($e)
189
    {
190
        $type = interface_exists('\Throwable', false) ? '\Throwable' : '\Exception';
191
        return $e instanceof $type;
192
    }
193
194
    /**
195
     * Announce fatal error, attempt to log
196
     *
197
     * @param string $msg error message to report
198
     *
199
     * @return void
200
     */
201
    private function reportFatalError($msg)
202
    {
203
        $msg=$this->sanitizePath($msg);
204
        if ('cli' === php_sapi_name()) {
205
            fprintf(STDERR, "\nError : %s\n", $msg);
206
        } else {
207
            if (defined('_XOOPS_FATAL_MESSAGE')) {
208
                printf(_XOOPS_FATAL_MESSAGE, XOOPS_URL, $msg);
209
            } else {
210
                printf("\nError : %s\n", $msg);
211
            }
212
        }
213
        @$this->log(LogLevel::CRITICAL, $msg);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
214
    }
215
216
    /**
217
     * clean a path to remove sensitive details
218
     *
219
     * @param string $message text to sanitize
220
     *
221
     * @return string sanitized message
222
     */
223 21
    public function sanitizePath($message)
224
    {
225
        $cleaners = [
226 21
            ['\\', '/',],
227 21
            [\XoopsBaseConfig::get('var-path'), 'VAR',],
228 21
            [str_replace('\\', '/', realpath(\XoopsBaseConfig::get('var-path'))), 'VAR',],
229 21
            [\XoopsBaseConfig::get('lib-path'), 'LIB',],
230 21
            [str_replace('\\', '/', realpath(\XoopsBaseConfig::get('lib-path'))), 'LIB',],
231 21
            [\XoopsBaseConfig::get('root-path'), 'ROOT',],
232 21
            [str_replace('\\', '/', realpath(\XoopsBaseConfig::get('root-path'))), 'ROOT',],
233 21
            [\XoopsBaseConfig::get('db-name') . '.', '',],
234 21
            [\XoopsBaseConfig::get('db-name'), '',],
235 21
            [\XoopsBaseConfig::get('db-prefix') . '_', '',],
236 21
            [\XoopsBaseConfig::get('db-user'), '***',],
237 21
            [\XoopsBaseConfig::get('db-pass'), '***',],
238
        ];
239 21
        $stringsToClean = array_column($cleaners, 0);
240 21
        $replacementStings = array_column($cleaners, 1);
241
242 21
        $message = str_replace($stringsToClean, $replacementStings, $message);
243
244 21
        return $message;
245
    }
246
247
    /**
248
     * add a PSR-3 compatible logger to the chain
249
     *
250
     * @param object $logger a PSR-3 compatible logger object
251
     *
252
     * @return void
253
     */
254 22
    public function addLogger($logger)
255
    {
256 22
        if (is_object($logger) && method_exists($logger, 'log')) {
257 22
                $this->loggers[] = $logger;
258 22
                $this->logging_active = true;
259
        }
260 22
    }
261
262
    /**
263
     * System is unusable.
264
     *
265
     * @param string $message message
266
     * @param array  $context array of context data for this log entry
267
     *
268
     * @return void
269
     */
270 1
    public function emergency($message, array $context = array())
271
    {
272 1
        $this->log(LogLevel::EMERGENCY, $message, $context);
273 1
    }
274
275
    /**
276
     * Action must be taken immediately.
277
     *
278
     * Example: Entire website down, database unavailable, etc. This should
279
     * trigger the SMS alerts and wake you up.
280
     *
281
     * @param string $message message
282
     * @param array  $context array of context data for this log entry
283
     *
284
     * @return void
285
     */
286 1
    public function alert($message, array $context = array())
287
    {
288 1
        $this->log(LogLevel::ALERT, $message, $context);
289 1
    }
290
291
    /**
292
     * Critical conditions.
293
     *
294
     * Example: Application component unavailable, unexpected exception.
295
     *
296
     * @param string $message message
297
     * @param array  $context array of context data for this log entry
298
     *
299
     * @return void
300
     */
301 1
    public function critical($message, array $context = array())
302
    {
303 1
        $this->log(LogLevel::CRITICAL, $message, $context);
304 1
    }
305
306
    /**
307
     * Runtime errors that do not require immediate action but should typically
308
     * be logged and monitored.
309
     *
310
     * @param string $message message
311
     * @param array  $context array of context data for this log entry
312
     *
313
     * @return void
314
     */
315 1
    public function error($message, array $context = array())
316
    {
317 1
        $this->log(LogLevel::ERROR, $message, $context);
318 1
    }
319
320
    /**
321
     * Exceptional occurrences that are not errors.
322
     *
323
     * Example: Use of deprecated APIs, poor use of an API, undesirable things
324
     * that are not necessarily wrong.
325
     *
326
     * @param string $message message
327
     * @param array  $context array of context data for this log entry
328
     *
329
     * @return void
330
     */
331 1
    public function warning($message, array $context = array())
332
    {
333 1
        $this->log(LogLevel::WARNING, $message, $context);
334 1
    }
335
336
    /**
337
     * Normal but significant events.
338
     *
339
     * @param string $message message
340
     * @param array  $context array of context data for this log entry
341
     *
342
     * @return void
343
     */
344 1
    public function notice($message, array $context = array())
345
    {
346 1
        $this->log(LogLevel::NOTICE, $message, $context);
347 1
    }
348
349
    /**
350
     * Interesting events.
351
     *
352
     * Example: User logs in, SQL logs.
353
     *
354
     * @param string $message message
355
     * @param array  $context array of context data for this log entry
356
     *
357
     * @return void
358
     */
359 1
    public function info($message, array $context = array())
360
    {
361 1
        $this->log(LogLevel::INFO, $message, $context);
362 1
    }
363
364
    /**
365
     * Detailed debug information.
366
     *
367
     * @param string $message message
368
     * @param array  $context array of context data for this log entry
369
     *
370
     * @return void
371
     */
372 1
    public function debug($message, array $context = array())
373
    {
374 1
        $this->log(LogLevel::DEBUG, $message, $context);
375 1
    }
376
377
    /**
378
     * Logs with an arbitrary level.
379
     *
380
     * @param mixed  $level   PSR-3 LogLevel constant
381
     * @param string $message message
382
     * @param array  $context array of context data for this log entry
383
     *
384
     * @return void
385
     */
386 14
    public function log($level, $message, array $context = array())
387
    {
388 14
        if (!empty($this->loggers)) {
389 13
            foreach ($this->loggers as $logger) {
390 13
                if (is_object($logger)) {
391
                    try {
392 13
                        $logger->log($level, $message, $context);
393 13
                    } catch (\Exception $e) {
394
                        // just ignore, as we can't do anything, not even log it.
395
                    }
396
                }
397
            }
398
        }
399 14
    }
400
401
    /**
402
     * quiet - turn off output if output is rendered in XOOPS page output.
403
     * This is intended to assist ajax code that may fail with any extra
404
     * content the logger may introduce.
405
     *
406
     * It should have no effect on loggers using other methods, such a write
407
     * to file.
408
     *
409
     * @return void
410
     */
411 2
    public function quiet()
412
    {
413 2
        if (!empty($this->loggers)) {
414 2
            foreach ($this->loggers as $logger) {
415 2
                if (is_object($logger) && method_exists($logger, 'quiet')) {
416
                    try {
417 2
                        $logger->quiet();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Psr\Log\LoggerInterface as the method quiet() does only exist in the following implementations of said interface: DebugbarLogger, LegacyLogger, MonologLogger, Xoops\Core\Logger.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
418 2
                    } catch (\Exception $e) {
419
                        // just ignore, as we can't do anything, not even log it.
420
                    }
421
                }
422
            }
423
        }
424 2
    }
425
426
    // Deprecated uses
427
428
    /**
429
     * Keep deprecated calls from failing
430
     *
431
     * @param string $var property
432
     * @param string $val value
433
     *
434
     * @return void
435
     *
436
     * @deprecated
437
     */
438 1
    public function __set($var, $val)
439
    {
440 1
        $this->deprecatedMessage();
441
        // legacy compatibility: turn off logger display for $xoopsLogger->activated = false; usage
442 1
        if ($var==='activated' && !$val) {
443 1
            $this->quiet();
444
        }
445
446 1
    }
447
448
    /**
449
     * Keep deprecated calls from failing
450
     *
451
     * @param string $var property
452
     *
453
     * @return void
454
     *
455
     * @deprecated
456
     */
457 1
    public function __get($var)
458
    {
459 1
        $this->deprecatedMessage();
460 1
    }
461
462
    /**
463
     * Keep deprecated calls from failing
464
     *
465
     * @param string $method method
466
     * @param string $args   arguments
467
     *
468
     * @return void
469
     *
470
     * @deprecated
471
    */
472 1
    public function __call($method, $args)
473
    {
474 1
        $this->deprecatedMessage();
475 1
    }
476
477
    /**
478
     * issue a deprecated warning
479
     *
480
     * @return void
481
     */
482 3
    private function deprecatedMessage()
483
    {
484 3
        $xoops = \Xoops::getInstance();
485 3
        $xoops->deprecated('This use of XoopsLogger is deprecated since 2.6.0.');
486 3
    }
487
}
488