Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.43% covered (warning)
58.43%
52 / 89
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Logger
58.43% covered (warning)
58.43%
52 / 89
14.29% covered (danger)
14.29%
1 / 7
100.05
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 debugNeeded
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 log
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 getSeverityFromException
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
6.00
 logException
91.11% covered (success)
91.11%
41 / 45
0.00% covered (danger)
0.00%
0 / 1
9.06
 removeWriter
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 argumentToString
42.86% covered (danger)
42.86%
6 / 14
0.00% covered (danger)
0.00%
0 / 1
24.11
1<?php
2
3/**
4 * VuFind Logger
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  Error_Logging
25 * @author   Chris Hallberg <challber@villanova.edu>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org Main Site
28 */
29
30namespace VuFind\Log;
31
32use Laminas\Log\Logger as BaseLogger;
33use Laminas\Log\Writer\WriterInterface;
34use Laminas\Stdlib\SplPriorityQueue;
35use Traversable;
36use VuFind\Net\UserIpReader;
37
38use function in_array;
39use function is_array;
40use function is_bool;
41use function is_float;
42use function is_int;
43use function is_object;
44
45/**
46 * This class wraps the BaseLogger class to allow for log verbosity
47 *
48 * @category VuFind
49 * @package  Error_Logging
50 * @author   Chris Hallberg <challber@villanova.edu>
51 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
52 * @link     https://vufind.org Main Site
53 */
54class Logger extends BaseLogger
55{
56    /**
57     * Is debug logging enabled?
58     *
59     * @var bool
60     */
61    protected $debugNeeded = false;
62
63    /**
64     * User IP address reader
65     *
66     * @var UserIpReader
67     */
68    protected $userIpReader;
69
70    /**
71     * Constructor
72     *
73     * Set options for a logger. Accepted options are:
74     * - writers: array of writers to add to this logger
75     * - exceptionhandler: if true register this logger as exceptionhandler
76     * - errorhandler: if true register this logger as errorhandler
77     *
78     * @param UserIpReader      $userIpReader User IP reader
79     * @param array|Traversable $options      Configuration options
80     *
81     * @throws \Laminas\Log\Exception\InvalidArgumentException
82     */
83    public function __construct(UserIpReader $userIpReader, $options = null)
84    {
85        $this->userIpReader = $userIpReader;
86        parent::__construct($options);
87    }
88
89    /**
90     * Is one of the log writers listening for debug messages?  (This is useful to
91     * know, since some code can save time that would be otherwise wasted generating
92     * debug messages if we know that no one is listening).
93     *
94     * @param bool $newState New state (omit to leave current state unchanged)
95     *
96     * @return bool
97     */
98    public function debugNeeded($newState = null)
99    {
100        if (null !== $newState) {
101            $this->debugNeeded = $newState;
102        }
103        return $this->debugNeeded;
104    }
105
106    /**
107     * Add a message as a log entry
108     *
109     * @param int               $priority Priority
110     * @param mixed             $message  Message
111     * @param array|Traversable $extra    Extras
112     *
113     * @return Logger
114     */
115    public function log($priority, $message, $extra = [])
116    {
117        // Special case to handle arrays of messages (for multi-verbosity-level
118        // logging, not supported by base class):
119        if (is_array($message)) {
120            $timestamp = new \DateTime();
121            foreach ($this->writers->toArray() as $writer) {
122                $writer->write(
123                    [
124                        'timestamp'    => $timestamp,
125                        'priority'     => (int)$priority,
126                        'priorityName' => $this->priorities[$priority],
127                        'message'      => $message,
128                        'extra'        => $extra,
129                    ]
130                );
131            }
132            return $this;
133        }
134        return parent::log($priority, $message, $extra);
135    }
136
137    /**
138     * Given an exception, return a severity level for logging purposes.
139     *
140     * @param \Exception $error Exception to analyze
141     *
142     * @return int
143     */
144    protected function getSeverityFromException($error)
145    {
146        // If the exception provides the severity level, use it:
147        if ($error instanceof \VuFind\Exception\SeverityLevelInterface) {
148            return $error->getSeverityLevel();
149        }
150        // Treat unexpected or 5xx errors as more severe than 4xx errors.
151        if (
152            $error instanceof \VuFind\Exception\HttpStatusInterface
153            && in_array($error->getHttpStatus(), [403, 404])
154        ) {
155            return BaseLogger::WARN;
156        }
157        return BaseLogger::CRIT;
158    }
159
160    /**
161     * Log an exception triggered by the framework for administrative purposes.
162     *
163     * @param \Exception                 $error  Exception to log
164     * @param \Laminas\Stdlib\Parameters $server Server metadata
165     *
166     * @return void
167     */
168    public function logException($error, $server)
169    {
170        // We need to build a variety of pieces so we can supply
171        // information at five different verbosity levels:
172        $baseError = $error::class . ' : ' . $error->getMessage();
173        $prev = $error->getPrevious();
174        while ($prev) {
175            $baseError .= ' ; ' . $prev::class . ' : ' . $prev->getMessage();
176            $prev = $prev->getPrevious();
177        }
178        $referer = $server->get('HTTP_REFERER', 'none');
179        $ipAddr = $this->userIpReader->getUserIp();
180        $basicServer
181            = '(Server: IP = ' . $ipAddr . ', '
182            . 'Referer = ' . $referer . ', '
183            . 'User Agent = '
184            . $server->get('HTTP_USER_AGENT') . ', '
185            . 'Host = '
186            . $server->get('HTTP_HOST') . ', '
187            . 'Request URI = '
188            . $server->get('REQUEST_URI') . ')';
189        $detailedServer = "\nServer Context:\n"
190            . print_r($server->toArray(), true);
191        $basicBacktrace = $detailedBacktrace = "\nBacktrace:\n";
192        if (is_array($error->getTrace())) {
193            foreach ($error->getTrace() as $line) {
194                if (!isset($line['file'])) {
195                    $line['file'] = 'unlisted file';
196                }
197                if (!isset($line['line'])) {
198                    $line['line'] = 'unlisted';
199                }
200                $basicBacktraceLine = $detailedBacktraceLine = $line['file'] .
201                    ' line ' . $line['line'] . ' - ' .
202                    (isset($line['class']) ? 'class = ' . $line['class'] . ', ' : '')
203                    . 'function = ' . $line['function'];
204                $basicBacktrace .= "{$basicBacktraceLine}\n";
205                if (!empty($line['args'])) {
206                    $args = [];
207                    foreach ($line['args'] as $i => $arg) {
208                        $args[] = $i . ' = ' . $this->argumentToString($arg);
209                    }
210                    $detailedBacktraceLine .= ', args: ' . implode(', ', $args);
211                } else {
212                    $detailedBacktraceLine .= ', args: none.';
213                }
214                $detailedBacktrace .= "{$detailedBacktraceLine}\n";
215            }
216        }
217
218        $errorDetails = [
219            1 => $baseError,
220            2 => $baseError . $basicServer,
221            3 => $baseError . $basicServer . $basicBacktrace,
222            4 => $baseError . $detailedServer . $basicBacktrace,
223            5 => $baseError . $detailedServer . $detailedBacktrace,
224        ];
225
226        $this->log($this->getSeverityFromException($error), $errorDetails);
227    }
228
229    /**
230     * Remove a writer.
231     *
232     * @param WriterInterface $writer Writer to remove
233     *
234     * @return void
235     */
236    public function removeWriter(WriterInterface $writer): void
237    {
238        $newQueue = new SplPriorityQueue();
239        foreach ($this->getWriters() as $i => $current) {
240            if ($current !== $writer) {
241                $newQueue->insert($current, $i);
242            }
243        }
244        $this->setWriters($newQueue);
245    }
246
247    /**
248     * Convert function argument to a loggable string
249     *
250     * @param mixed $arg Argument
251     *
252     * @return string
253     */
254    protected function argumentToString($arg)
255    {
256        if (is_object($arg)) {
257            return $arg::class . ' Object';
258        }
259        if (is_array($arg)) {
260            $args = [];
261            foreach ($arg as $key => $item) {
262                $args[] = "$key => " . $this->argumentToString($item);
263            }
264            return 'array(' . implode(', ', $args) . ')';
265        }
266        if (is_bool($arg)) {
267            return $arg ? 'true' : 'false';
268        }
269        if (is_int($arg) || is_float($arg)) {
270            return (string)$arg;
271        }
272        if (null === $arg) {
273            return 'null';
274        }
275        return "'$arg'";
276    }
277}