xref: /plugin/combo/ComboStrap/LogUtility.php (revision 39c00e7ee80f440398e043c1b028639b65574701)
1<?php
2/**
3 * Copyright (c) 2020. ComboStrap, Inc. and its affiliates. All Rights Reserved.
4 *
5 * This source code is licensed under the GPL license found in the
6 * COPYING  file in the root directory of this source tree.
7 *
8 * @license  GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
9 * @author   ComboStrap <support@combostrap.com>
10 *
11 */
12
13namespace ComboStrap;
14
15use dokuwiki\Logger;
16use Throwable;
17
18require_once(__DIR__ . '/PluginUtility.php');
19
20class LogUtility
21{
22
23    /**
24     * Constant for the function {@link msg()}
25     * -1 = error, 0 = info, 1 = success, 2 = notify
26     * (Not even in order of importance)
27     */
28    const LVL_MSG_ABOVE_ERROR = 5; // a level to disable the error to thrown in test
29    const LVL_MSG_ERROR = 4; //-1;
30    const LVL_MSG_WARNING = 3; //2;
31    const LVL_MSG_SUCCESS = 2; //1;
32    const LVL_MSG_INFO = 1; //0;
33    const LVL_MSG_DEBUG = 0; //3;
34
35
36    /**
37     * Id level to name
38     */
39    const LVL_NAME = array(
40        0 => "debug",
41        1 => "info",
42        3 => "warning",
43        2 => "success",
44        4 => "error"
45    );
46
47    /**
48     * Id level to name
49     * {@link msg()} constant
50     */
51    const LVL_TO_MSG_LEVEL = array(
52        0 => 3,
53        1 => 0,
54        2 => 1,
55        3 => 2,
56        4 => -1
57    );
58
59
60    const LOGLEVEL_URI_QUERY_PROPERTY = "loglevel";
61    const SUPPORT_CANONICAL = "support";
62
63    /**
64     *
65     * @var bool
66     */
67    private static bool $throwExceptionOnDevTest = true;
68    /**
69     * @var int
70     */
71    const DEFAULT_THROW_LEVEL = self::LVL_MSG_WARNING;
72
73    /**
74     * Send a message to a manager and log it
75     * Fail if in test
76     * @param string $message
77     * @param int $level - the level see LVL constant
78     * @param string $canonical - the canonical
79     * @param \Exception|null $e
80     */
81    public static function msg(string $message, int $level = self::LVL_MSG_ERROR, string $canonical = self::SUPPORT_CANONICAL, \Exception $e = null)
82    {
83
84        try {
85            self::messageNotEmpty($message);
86        } catch (ExceptionCompile $e) {
87            self::log2file($e->getMessage(), LogUtility::LVL_MSG_ERROR, $canonical);
88        }
89
90        /**
91         * Log to frontend
92         */
93        self::log2FrontEnd($message, $level, $canonical);
94
95        /**
96         * Log level passed for a page (only for file used)
97         * to not allow an attacker to see all errors in frontend
98         */
99        global $INPUT;
100        $loglevelProp = $INPUT->str(self::LOGLEVEL_URI_QUERY_PROPERTY, null);
101        if (!empty($loglevelProp)) {
102            $level = $loglevelProp;
103        }
104        /**
105         * TODO: Make it a configuration ?
106         */
107        if ($level >= self::LVL_MSG_WARNING) {
108            self::log2file($message, $level, $canonical, $e);
109        }
110
111        /**
112         * If test, we throw an error
113         */
114        self::throwErrorIfTest($level, $message, $e);
115    }
116
117    /**
118     * Print log to a  file
119     *
120     * Adapted from {@link dbglog}
121     * Note: {@link dbg()} dbg print to the web page
122     *
123     * @param null|string $msg - may be null always this is the default if a variable is not initialized.
124     * @param int $logLevel
125     * @param string|null $canonical
126     * @param \Exception|null $e
127     */
128    static function log2file(?string $msg, int $logLevel = self::LVL_MSG_ERROR, ?string $canonical = self::SUPPORT_CANONICAL, \Exception $e = null)
129    {
130
131        try {
132            self::messageNotEmpty($msg);
133        } catch (ExceptionCompile $e) {
134            $msg = $e->getMessage();
135            $logLevel = self::LVL_MSG_ERROR;
136        }
137
138        if (PluginUtility::isTest() || $logLevel >= self::LVL_MSG_WARNING) {
139
140            $prefix = PluginUtility::$PLUGIN_NAME;
141            if (!empty($canonical)) {
142                $prefix .= ' - ' . $canonical;
143            }
144            $msg = $prefix . ' - ' . $msg;
145
146            global $INPUT;
147
148            /**
149             * Adding page - context information
150             * We are not using {@link MarkupPath::createFromRequestedPage()}
151             * because it throws an error message when the environment
152             * is not good, creating a recursive call.
153             */
154            $id = $INPUT->str("id");
155            $sep = " - ";
156            $messageWritten = date('c') . $sep . self::LVL_NAME[$logLevel] . $sep . $msg . $sep . $INPUT->server->str('REMOTE_ADDR') . $sep . $id . "\n";
157            // dokuwiki does not have the warning level
158            Logger::error($messageWritten);
159            self::throwErrorIfTest($logLevel, $msg, $e);
160
161
162        }
163
164    }
165
166    /**
167     * @param $message
168     * @param $level
169     * @param string $canonical
170     * @param bool $publicMessage
171     */
172    public static function log2FrontEnd($message, $level, string $canonical = self::SUPPORT_CANONICAL, bool $publicMessage = false)
173    {
174
175        try {
176            self::messageNotEmpty($message);
177        } catch (ExceptionCompile $e) {
178            $message = $e->getMessage();
179            $level = self::LVL_MSG_ERROR;
180        }
181
182        /**
183         * If we are not in the console
184         * and not in test
185         * we test that the message comes in the front end
186         * (example {@link \plugin_combo_frontmatter_test}
187         */
188        $isTerminal = Console::isConsoleRun();
189        if ($isTerminal) {
190            if (!defined('DOKU_UNITTEST')) {
191                /**
192                 * such as {@link cli_plugin_combo}
193                 */
194                $userAgent = "cli";
195            } else {
196                $userAgent = "phpunit";
197            }
198        } else {
199            $userAgent = "browser";
200        }
201
202        switch ($userAgent) {
203            case "cli":
204                echo "$message\n";
205                break;
206            case "phpunit":
207            case "browser":
208            default:
209                if ($canonical !== null) {
210                    $label = ucfirst(str_replace(":", " ", $canonical));
211                    $htmlMsg = PluginUtility::getDocumentationHyperLink($canonical, $label, false);
212                } else {
213                    $htmlMsg = PluginUtility::getDocumentationHyperLink("", PluginUtility::$PLUGIN_NAME, false);
214                }
215
216
217                /**
218                 * Adding page - context information
219                 * We are not creating the page
220                 * direction from {@link MarkupPath::createFromRequestedPage()}
221                 * because it throws an error message when the environment
222                 * is not good, creating a recursive call.
223                 */
224                global $INPUT;
225                $id = $INPUT->str("id");
226                if ($id != null) {
227
228                    /**
229                     * We don't use any Page object to not
230                     * create a cycle while building it
231                     */
232                    $url = wl($id, [], true);
233                    $htmlMsg .= " - <a href=\"$url\">$id</a>";
234
235                }
236
237                /**
238                 *
239                 */
240                $htmlMsg .= " - " . $message;
241                if ($level > self::LVL_MSG_DEBUG) {
242                    $dokuWikiLevel = self::LVL_TO_MSG_LEVEL[$level];
243                    if ($publicMessage) {
244                        $allow = MSG_PUBLIC;
245                    } else {
246                        $allow = MSG_USERS_ONLY;
247                    }
248                    msg($htmlMsg, $dokuWikiLevel, '', '', $allow);
249                }
250        }
251    }
252
253    /**
254     * Log a message to the browser console
255     * @param $message
256     */
257    public static function log2BrowserConsole($message)
258    {
259        // TODO
260    }
261
262
263    /**
264     * @param $level
265     * @param $message
266     * @param $e - the original exception for chaining
267     * @return void
268     */
269    private static function throwErrorIfTest($level, $message, \Exception $e = null)
270    {
271        if (PluginUtility::isTest() && self::$throwExceptionOnDevTest) {
272            try {
273                $actualLevel = ExecutionContext::getExecutionContext()->getConfig()->getLogExceptionLevel();
274            } catch (ExceptionNotFound $e) {
275                // In context creation
276                return;
277            }
278            if ($level >= $actualLevel) {
279                throw new LogException($message, $level, $e);
280            }
281        }
282    }
283
284    /**
285     * @param string|null $message
286     * @throws ExceptionCompile
287     */
288    private static function messageNotEmpty(?string $message)
289    {
290        $message = trim($message);
291        if ($message === null || $message === "") {
292            $newMessage = "The passed message to the log was empty or null. BackTrace: \n";
293            $newMessage .= LogUtility::getCallStack();
294            throw new ExceptionCompile($newMessage);
295        }
296    }
297
298    public static function disableThrowExceptionOnDevTest()
299    {
300        self::$throwExceptionOnDevTest = false;
301    }
302
303    public static function enableThrowExceptionOnDevTest()
304    {
305        self::$throwExceptionOnDevTest = true;
306    }
307
308    public static function wrapInRedForHtml(string $message): string
309    {
310        return "<span class=\"text-danger\">$message</span>";
311    }
312
313    /**
314     * @return false|string - the actual php call stack (known as backtrace)
315     */
316    public static function getCallStack()
317    {
318        ob_start();
319        $limit = 10;
320        /**
321         * DEBUG_BACKTRACE_IGNORE_ARGS options to avoid
322         * PHP Fatal error:  Allowed memory size of 2147483648 bytes exhausted (tried to allocate 1876967424 bytes)
323         */
324        debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $limit); // It prints also the data passed :)
325        $trace = ob_get_contents();
326        ob_end_clean();
327        return $trace;
328    }
329
330    /**
331     * @param string $message the message
332     * @param string $canonical the page
333     * @param \Exception|null $e the original exception for trace chaining
334     * @return void
335     */
336    public static function error(string $message, string $canonical = self::SUPPORT_CANONICAL, \Exception $e = null)
337    {
338        self::msg($message, LogUtility::LVL_MSG_ERROR, $canonical, $e);
339    }
340
341    public static function warning(string $message, string $canonical = self::SUPPORT_CANONICAL, \Exception $e = null)
342    {
343        self::msg($message, LogUtility::LVL_MSG_WARNING, $canonical, $e);
344    }
345
346    public static function info(string $message, string $canonical = self::SUPPORT_CANONICAL, \Exception $e = null)
347    {
348        self::msg($message, LogUtility::LVL_MSG_INFO, $canonical, $e);
349    }
350
351    /**
352     * @param int $level
353     * @return void
354     * @deprecated use {@link SiteConfig::setLogExceptionLevel()}
355     */
356    public static function setTestExceptionLevel(int $level)
357    {
358        ExecutionContext::getActualOrCreateFromEnv()->getConfig()->setLogExceptionLevel($level);
359    }
360
361    public static function setTestExceptionLevelToDefault()
362    {
363        ExecutionContext::getActualOrCreateFromEnv()->getConfig()->setLogExceptionLevel(self::LVL_MSG_WARNING);
364    }
365
366    public static function errorIfDevOrTest($message, $canonical = "support")
367    {
368        if (PluginUtility::isDevOrTest()) {
369            LogUtility::error($message, $canonical);
370        }
371    }
372
373    /**
374     * @return void
375     * @deprecated use the config object instead
376     */
377    public static function setTestExceptionLevelToError()
378    {
379        ExecutionContext::getActualOrCreateFromEnv()->getConfig()->setLogExceptionToError();
380    }
381
382    /**
383     * Advertise an error that should not take place if the code was
384     * written properly
385     * @param string $message
386     * @param string $canonical
387     * @param Throwable|null $previous
388     * @return void
389     */
390    public static function internalError(string $message, string $canonical = "support", Throwable $previous = null)
391    {
392        $internalErrorMessage = "Sorry. An internal error has occurred";
393        if (PluginUtility::isDevOrTest()) {
394            throw new ExceptionRuntimeInternal("$internalErrorMessage - $message", $canonical, 1, $previous);
395        } else {
396            $errorPreviousMessage = "";
397            if ($previous !== null) {
398                $errorPreviousMessage = " Error: {$previous->getMessage()}";
399            }
400            self::error("{$internalErrorMessage}: $message.$errorPreviousMessage", $canonical);
401        }
402    }
403
404    /**
405     * @param string $message
406     * @param string $canonical
407     * @param $e
408     * @return void
409     * Debug, trace
410     */
411    public static function debug(string $message, string $canonical = self::SUPPORT_CANONICAL, $e = null)
412    {
413        self::msg($message, LogUtility::LVL_MSG_DEBUG, $canonical, $e);
414    }
415
416    public static function infoToPublic(string $html, string $canonical)
417    {
418        self::log2FrontEnd($html, LogUtility::LVL_MSG_INFO, $canonical, true);
419    }
420
421}
422