Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
n/a
0 / 0
n/a
0 / 0
CRAP
n/a
0 / 0
LoggerFactory
n/a
0 / 0
n/a
0 / 0
45
n/a
0 / 0
 addDbWriters
n/a
0 / 0
n/a
0 / 0
1
 addEmailWriters
n/a
0 / 0
n/a
0 / 0
1
 addFileWriters
n/a
0 / 0
n/a
0 / 0
2
 addSlackWriters
n/a
0 / 0
n/a
0 / 0
4
 addOffice365Writers
n/a
0 / 0
n/a
0 / 0
2
 hasDynamicDebug
n/a
0 / 0
n/a
0 / 0
3
 configureLogger
n/a
0 / 0
n/a
0 / 0
14
 addDebugWriter
n/a
0 / 0
n/a
0 / 0
4
 addWriters
n/a
0 / 0
n/a
0 / 0
10
 getProxyClassName
n/a
0 / 0
n/a
0 / 0
2
 __invoke
n/a
0 / 0
n/a
0 / 0
2
1<?php
2
3/**
4 * Factory for instantiating Logger
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2017.
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   Demian Katz <demian.katz@villanova.edu>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org/wiki/development Wiki
28 */
29
30namespace VuFind\Log;
31
32use Laminas\Config\Config;
33use Laminas\Log\Writer\WriterInterface;
34use Laminas\ServiceManager\Exception\ServiceNotCreatedException;
35use Laminas\ServiceManager\Exception\ServiceNotFoundException;
36use Laminas\ServiceManager\Factory\FactoryInterface;
37use Psr\Container\ContainerExceptionInterface as ContainerException;
38use Psr\Container\ContainerInterface;
39
40use function count;
41use function is_array;
42use function is_int;
43
44/**
45 * Factory for instantiating Logger
46 *
47 * @category VuFind
48 * @package  Error_Logging
49 * @author   Demian Katz <demian.katz@villanova.edu>
50 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
51 * @link     https://vufind.org/wiki/development Wiki
52 *
53 * @codeCoverageIgnore
54 */
55class LoggerFactory implements FactoryInterface
56{
57    /**
58     * Configure database writers.
59     *
60     * @param Logger             $logger    Logger object
61     * @param ContainerInterface $container Service manager
62     * @param string             $config    Configuration
63     *
64     * @return void
65     */
66    protected function addDbWriters(
67        Logger $logger,
68        ContainerInterface $container,
69        $config
70    ) {
71        $parts = explode(':', $config);
72        $table_name = $parts[0];
73        $error_types = $parts[1] ?? '';
74
75        $columnMapping = [
76            'priority' => 'priority',
77            'message' => 'message',
78            'logtime' => 'timestamp',
79            'ident' => 'ident',
80        ];
81
82        // Make Writers
83        $filters = explode(',', $error_types);
84        $writer = new Writer\Db(
85            $container->get(\Laminas\Db\Adapter\Adapter::class),
86            $table_name,
87            $columnMapping
88        );
89        $this->addWriters($logger, $writer, $filters);
90    }
91
92    /**
93     * Configure email writers.
94     *
95     * @param Logger             $logger    Logger object
96     * @param ContainerInterface $container Service manager
97     * @param Config             $config    Configuration
98     *
99     * @return void
100     */
101    protected function addEmailWriters(
102        Logger $logger,
103        ContainerInterface $container,
104        Config $config
105    ) {
106        // Set up the logger's mailer to behave consistently with VuFind's
107        // general mailer:
108        $parts = explode(':', $config->Logging->email);
109        $email = $parts[0];
110        $error_types = $parts[1] ?? '';
111
112        // use smtp
113        $mailer = $container->get(\VuFind\Mailer\Mailer::class);
114        $msg = $mailer->getNewMessage()
115            ->addFrom($config->Site->email)
116            ->addTo($email)
117            ->setSubject('VuFind Log Message');
118
119        // Make Writers
120        $filters = explode(',', $error_types);
121        $writer = new Writer\Mail($msg, $mailer->getTransport());
122        $this->addWriters($logger, $writer, $filters);
123    }
124
125    /**
126     * Configure File writers.
127     *
128     * @param Logger $logger Logger object
129     * @param string $config Configuration
130     *
131     * @return void
132     */
133    protected function addFileWriters(Logger $logger, $config)
134    {
135        // Make sure to use only the last ':' after second character to avoid trouble
136        // with Windows drive letters (e.g. "c:\something\logfile:error-5")
137        $pos = strrpos($config, ':', 2);
138        if ($pos > 0) {
139            $file = substr($config, 0, $pos);
140            $error_types = substr($config, $pos + 1);
141        } else {
142            $file = $config;
143            $error_types = '';
144        }
145
146        // Make Writers
147        $filters = explode(',', $error_types);
148        $writer = new Writer\Stream($file);
149        $this->addWriters($logger, $writer, $filters);
150    }
151
152    /**
153     * Configure Slack writers.
154     *
155     * @param Logger             $logger    Logger object
156     * @param ContainerInterface $container Service manager
157     * @param Config             $config    Configuration
158     *
159     * @return void
160     */
161    protected function addSlackWriters(
162        Logger $logger,
163        ContainerInterface $container,
164        Config $config
165    ) {
166        $options = [];
167        // Get config
168        [$channel, $error_types] = explode(':', $config->Logging->slack);
169        if ($error_types == null) {
170            $error_types = $channel;
171            $channel = null;
172        }
173        if ($channel) {
174            $options['channel'] = $channel;
175        }
176        if (isset($config->Logging->slackname)) {
177            $options['name'] = $config->Logging->slackname;
178        }
179        $filters = explode(',', $error_types);
180        // Make Writers
181        $writer = new Writer\Slack(
182            $config->Logging->slackurl,
183            $container->get(\VuFindHttp\HttpService::class)->createClient(),
184            $options
185        );
186        $writer->setContentType('application/json');
187        $formatter = new \Laminas\Log\Formatter\Simple(
188            '*%priorityName%*: %message%'
189        );
190        $writer->setFormatter($formatter);
191        $this->addWriters($logger, $writer, $filters);
192    }
193
194    /**
195     * Configure Office365 writers.
196     *
197     * @param Logger             $logger    Logger object
198     * @param ContainerInterface $container Service manager
199     * @param Config             $config    Configuration
200     *
201     * @return void
202     */
203    protected function addOffice365Writers(
204        Logger $logger,
205        ContainerInterface $container,
206        Config $config
207    ) {
208        $options = [];
209        // Get config
210        $error_types = $config->Logging->office365;
211        if (isset($config->Logging->office365_title)) {
212            $options['title'] = $config->Logging->office365_title;
213        }
214        $filters = explode(',', $error_types);
215        // Make Writers
216        $writer = new Writer\Office365(
217            $config->Logging->office365_url,
218            $container->get(\VuFindHttp\HttpService::class)->createClient(),
219            $options
220        );
221        $writer->setContentType('application/json');
222        $formatter = new \Laminas\Log\Formatter\Simple(
223            '*%priorityName%*: %message%'
224        );
225        $writer->setFormatter($formatter);
226        $this->addWriters($logger, $writer, $filters);
227    }
228
229    /**
230     * Is dynamic debug mode enabled?
231     *
232     * @param ContainerInterface $container Service manager
233     *
234     * @return bool
235     */
236    protected function hasDynamicDebug(ContainerInterface $container): bool
237    {
238        // Query parameters do not apply in console mode; if we do have a debug
239        // query parameter, and the appropriate permission is set, activate dynamic
240        // debug:
241        if (
242            PHP_SAPI !== 'cli'
243            && $container->get('Request')->getQuery()->get('debug')
244        ) {
245            return $container->get(\LmcRbacMvc\Service\AuthorizationService::class)
246                ->isGranted('access.DebugMode');
247        }
248        return false;
249    }
250
251    /**
252     * Set configuration
253     *
254     * @param ContainerInterface $container Service manager
255     * @param Logger             $logger    Logger to configure
256     *
257     * @return void
258     */
259    protected function configureLogger(ContainerInterface $container, Logger $logger)
260    {
261        $config = $container->get(\VuFind\Config\PluginManager::class)
262            ->get('config');
263
264        // Add a no-op writer so fatal errors are not triggered if log messages are
265        // sent during the initialization process.
266        $noOpWriter = new \Laminas\Log\Writer\Noop();
267        $logger->addWriter($noOpWriter);
268
269        // DEBUGGER
270        if (!$config->System->debug == false || $this->hasDynamicDebug($container)) {
271            $this->addDebugWriter($logger, $config->System->debug);
272        }
273
274        // Activate database logging, if applicable:
275        if (isset($config->Logging->database)) {
276            $this->addDbWriters($logger, $container, $config->Logging->database);
277        }
278
279        // Activate file logging, if applicable:
280        if (isset($config->Logging->file)) {
281            $this->addFileWriters($logger, $config->Logging->file);
282        }
283
284        // Activate email logging, if applicable:
285        if (isset($config->Logging->email)) {
286            $this->addEmailWriters($logger, $container, $config);
287        }
288
289        // Activate Office365 logging, if applicable:
290        if (
291            isset($config->Logging->office365)
292            && isset($config->Logging->office365_url)
293        ) {
294            $this->addOffice365Writers($logger, $container, $config);
295        }
296
297        // Activate slack logging, if applicable:
298        if (isset($config->Logging->slack) && isset($config->Logging->slackurl)) {
299            $this->addSlackWriters($logger, $container, $config);
300        }
301
302        // We're done now -- clean out the no-op writer if any other writers
303        // are found.
304        if (count($logger->getWriters()) > 1) {
305            $logger->removeWriter($noOpWriter);
306        }
307
308        // Add ReferenceId processor, if applicable:
309        if ($referenceId = $config->Logging->reference_id ?? false) {
310            if ('username' === $referenceId) {
311                $authManager = $container->get(\VuFind\Auth\Manager::class);
312                if ($user = $authManager->getUserObject()) {
313                    $processor = new \Laminas\Log\Processor\ReferenceId();
314                    $processor->setReferenceId($user->username);
315                    $logger->addProcessor($processor);
316                }
317            }
318        }
319    }
320
321    /**
322     * Add the standard debug stream writer.
323     *
324     * @param Logger   $logger Logger object
325     * @param bool|int $debug  Debug mode/level
326     *
327     * @return void
328     */
329    protected function addDebugWriter(Logger $logger, $debug)
330    {
331        // Only add debug writer ONCE!
332        static $hasDebugWriter = false;
333        if ($hasDebugWriter) {
334            return;
335        }
336
337        $hasDebugWriter = true;
338        $writer = new Writer\Stream('php://output');
339        $formatter = new \Laminas\Log\Formatter\Simple(
340            PHP_SAPI === 'cli'
341                ? '%timestamp% %priorityName%: %message%'
342                : '<pre>%timestamp% %priorityName%: %message%</pre>' . PHP_EOL
343        );
344        $writer->setFormatter($formatter);
345        $level = (is_int($debug) ? $debug : '5');
346        $this->addWriters(
347            $logger,
348            $writer,
349            "debug-$level,notice-$level,error-$level,alert-$level"
350        );
351    }
352
353    /**
354     * Applies an array of filters to a writer
355     *
356     * Filter keys: alert, error, notice, debug
357     *
358     * @param Logger          $logger  Logger object
359     * @param WriterInterface $writer  The writer to apply the
360     * filters to
361     * @param string|array    $filters An array or comma-separated
362     * string of
363     * logging levels
364     *
365     * @return void
366     */
367    protected function addWriters(Logger $logger, WriterInterface $writer, $filters)
368    {
369        if (!is_array($filters)) {
370            $filters = explode(',', $filters);
371        }
372
373        foreach ($filters as $filter) {
374            $parts = explode('-', $filter);
375            $priority = $parts[0];
376            $verbosity = $parts[1] ?? false;
377
378            // VuFind's configuration provides four priority options, each
379            // combining two of the standard Laminas levels.
380            switch (trim($priority)) {
381                case 'debug':
382                    // Set static flag indicating that debug is turned on:
383                    $logger->debugNeeded(true);
384
385                    $max = Logger::INFO;  // Informational: informational messages
386                    $min = Logger::DEBUG; // Debug: debug messages
387                    break;
388                case 'notice':
389                    $max = Logger::WARN;  // Warning: warning conditions
390                    $min = Logger::NOTICE;// Notice: normal but significant condition
391                    break;
392                case 'error':
393                    $max = Logger::CRIT;  // Critical: critical conditions
394                    $min = Logger::ERR;   // Error: error conditions
395                    break;
396                case 'alert':
397                    $max = Logger::EMERG; // Emergency: system is unusable
398                    $min = Logger::ALERT; // Alert: action must be taken immediately
399                    break;
400                default:
401                    // INVALID FILTER, so skip it. We must continue 2 levels, so we
402                    // continue the foreach loop instead of just breaking the switch.
403                    continue 2;
404            }
405
406            // Clone the submitted writer since we'll need a separate instance of the
407            // writer for each selected priority level.
408            $newWriter = clone $writer;
409
410            // verbosity
411            if ($verbosity) {
412                if (method_exists($newWriter, 'setVerbosity')) {
413                    $newWriter->setVerbosity($verbosity);
414                } else {
415                    throw new \Exception(
416                        $newWriter::class . ' does not support verbosity.'
417                    );
418                }
419            }
420
421            // filtering -- only log messages between the min and max priority levels
422            $filter1 = new \Laminas\Log\Filter\Priority($min, '<=');
423            $filter2 = new \Laminas\Log\Filter\Priority($max, '>=');
424            $newWriter->addFilter($filter1);
425            $newWriter->addFilter($filter2);
426
427            // add the writer
428            $logger->addWriter($newWriter);
429        }
430    }
431
432    /**
433     * Get proxy class to instantiate from the requested class name
434     *
435     * @param string $requestedName Service being created
436     *
437     * @return string
438     */
439    protected function getProxyClassName(string $requestedName): string
440    {
441        $className = $requestedName . 'Proxy';
442        // Fall back to default if the class doesn't exist:
443        if (!class_exists($className)) {
444            return LoggerProxy::class;
445        }
446        return $className;
447    }
448
449    /**
450     * Create an object
451     *
452     * @param ContainerInterface $container     Service manager
453     * @param string             $requestedName Service being created
454     * @param null|array         $options       Extra options (optional)
455     *
456     * @return object
457     *
458     * @throws ServiceNotFoundException if unable to resolve the service.
459     * @throws ServiceNotCreatedException if an exception is raised when
460     * creating a service.
461     * @throws ContainerException&\Throwable if any other error occurs
462     */
463    public function __invoke(
464        ContainerInterface $container,
465        $requestedName,
466        array $options = null
467    ) {
468        if (!empty($options)) {
469            throw new \Exception('Unexpected options passed to factory.');
470        }
471
472        // Construct the logger as a lazy loading proxy so that the object is not
473        // instantiated until it is called. This helps break potential circular
474        // dependencies with other services.
475        $callback = function (&$wrapped, $proxy) use ($container, $requestedName) {
476            // Now build the actual service:
477            $wrapped = new $requestedName(
478                $container->get(\VuFind\Net\UserIpReader::class)
479            );
480            $this->configureLogger($container, $wrapped);
481        };
482
483        $proxyClass = $this->getProxyClassName($requestedName);
484        return new $proxyClass($callback);
485    }
486}