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            $messageWritten = self::LVL_NAME[$logLevel] . " - $msg - (Page: $id, IP: {$INPUT->server->str('REMOTE_ADDR')})\n";
156            // dokuwiki does not have the warning level
157            Logger::error($messageWritten);
158            self::throwErrorIfTest($logLevel, $msg, $e);
159
160        }
161
162    }
163
164    /**
165     * @param $message
166     * @param $level
167     * @param string $canonical
168     * @param bool $publicMessage
169     */
170    public static function log2FrontEnd($message, $level, string $canonical = self::SUPPORT_CANONICAL, bool $publicMessage = false)
171    {
172
173        try {
174            self::messageNotEmpty($message);
175        } catch (ExceptionCompile $e) {
176            $message = $e->getMessage();
177            $level = self::LVL_MSG_ERROR;
178        }
179
180        /**
181         * If we are not in the console
182         * and not in test
183         * we test that the message comes in the front end
184         * (example {@link \plugin_combo_frontmatter_test}
185         */
186        $isTerminal = Console::isConsoleRun();
187        if ($isTerminal) {
188            if (!defined('DOKU_UNITTEST')) {
189                /**
190                 * such as {@link cli_plugin_combo}
191                 */
192                $userAgent = "cli";
193            } else {
194                $userAgent = "phpunit";
195            }
196        } else {
197            $userAgent = "browser";
198        }
199
200        switch ($userAgent) {
201            case "cli":
202                echo "$message\n";
203                break;
204            case "phpunit":
205            case "browser":
206            default:
207                if ($canonical !== null) {
208                    $label = ucfirst(str_replace(":", " ", $canonical));
209                    $htmlMsg = PluginUtility::getDocumentationHyperLink($canonical, $label, false);
210                } else {
211                    $htmlMsg = PluginUtility::getDocumentationHyperLink("", PluginUtility::$PLUGIN_NAME, false);
212                }
213
214
215                /**
216                 * Adding page - context information
217                 * We are not creating the page
218                 * direction from {@link MarkupPath::createFromRequestedPage()}
219                 * because it throws an error message when the environment
220                 * is not good, creating a recursive call.
221                 */
222                global $INPUT;
223                $id = $INPUT->str("id");
224                if ($id != null) {
225
226                    /**
227                     * We don't use any Page object to not
228                     * create a cycle while building it
229                     */
230                    $url = wl($id, [], true);
231                    $htmlMsg .= " - <a href=\"$url\">$id</a>";
232
233                }
234
235                /**
236                 *
237                 */
238                $htmlMsg .= " - " . $message;
239                if ($level > self::LVL_MSG_DEBUG) {
240                    $dokuWikiLevel = self::LVL_TO_MSG_LEVEL[$level];
241                    if ($publicMessage) {
242                        $allow = MSG_PUBLIC;
243                    } else {
244                        $allow = MSG_USERS_ONLY;
245                    }
246                    msg($htmlMsg, $dokuWikiLevel, '', '', $allow);
247                }
248        }
249    }
250
251    /**
252     * Log a message to the browser console
253     * @param $message
254     */
255    public static function log2BrowserConsole($message)
256    {
257        // TODO
258    }
259
260
261    /**
262     * @param $level
263     * @param $message
264     * @param $e - the original exception for chaining
265     * @return void
266     */
267    private static function throwErrorIfTest($level, $message, \Exception $e = null)
268    {
269        if (PluginUtility::isTest() && self::$throwExceptionOnDevTest) {
270            try {
271                $actualLevel = ExecutionContext::getExecutionContext()->getConfig()->getLogExceptionLevel();
272            } catch (ExceptionNotFound $e) {
273                // In context creation
274                return;
275            }
276            if ($level >= $actualLevel) {
277                throw new LogException($message, $level, $e);
278            }
279        }
280    }
281
282    /**
283     * @param string|null $message
284     * @throws ExceptionCompile
285     */
286    private static function messageNotEmpty(?string $message)
287    {
288        $message = trim($message);
289        if ($message === null || $message === "") {
290            $newMessage = "The passed message to the log was empty or null. BackTrace: \n";
291            $newMessage .= LogUtility::getCallStack();
292            throw new ExceptionCompile($newMessage);
293        }
294    }
295
296    public static function disableThrowExceptionOnDevTest()
297    {
298        self::$throwExceptionOnDevTest = false;
299    }
300
301    public static function enableThrowExceptionOnDevTest()
302    {
303        self::$throwExceptionOnDevTest = true;
304    }
305
306    public static function wrapInRedForHtml(string $message): string
307    {
308        return "<span class=\"text-danger\">$message</span>";
309    }
310
311    /**
312     * @return false|string - the actual php call stack (known as backtrace)
313     */
314    public static function getCallStack()
315    {
316        ob_start();
317        $limit = 10;
318        /**
319         * DEBUG_BACKTRACE_IGNORE_ARGS options to avoid
320         * PHP Fatal error:  Allowed memory size of 2147483648 bytes exhausted (tried to allocate 1876967424 bytes)
321         */
322        debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $limit); // It prints also the data passed :)
323        $trace = ob_get_contents();
324        ob_end_clean();
325        return $trace;
326    }
327
328    /**
329     * @param string $message the message
330     * @param string $canonical the page
331     * @param \Exception|null $e the original exception for trace chaining
332     * @return void
333     */
334    public static function error(string $message, string $canonical = self::SUPPORT_CANONICAL, \Exception $e = null)
335    {
336        self::msg($message, LogUtility::LVL_MSG_ERROR, $canonical, $e);
337    }
338
339    public static function warning(string $message, string $canonical = self::SUPPORT_CANONICAL, \Exception $e = null)
340    {
341        self::msg($message, LogUtility::LVL_MSG_WARNING, $canonical, $e);
342    }
343
344    public static function info(string $message, string $canonical = self::SUPPORT_CANONICAL, \Exception $e = null)
345    {
346        self::msg($message, LogUtility::LVL_MSG_INFO, $canonical, $e);
347    }
348
349    /**
350     * @param int $level
351     * @return void
352     * @deprecated use {@link SiteConfig::setLogExceptionLevel()}
353     */
354    public static function setTestExceptionLevel(int $level)
355    {
356        ExecutionContext::getActualOrCreateFromEnv()->getConfig()->setLogExceptionLevel($level);
357    }
358
359    public static function setTestExceptionLevelToDefault()
360    {
361        ExecutionContext::getActualOrCreateFromEnv()->getConfig()->setLogExceptionLevel(self::LVL_MSG_WARNING);
362    }
363
364    public static function errorIfDevOrTest($message, $canonical = "support")
365    {
366        if (PluginUtility::isDevOrTest()) {
367            LogUtility::error($message, $canonical);
368        }
369    }
370
371    /**
372     * @return void
373     * @deprecated use the config object instead
374     */
375    public static function setTestExceptionLevelToError()
376    {
377        ExecutionContext::getActualOrCreateFromEnv()->getConfig()->setLogExceptionToError();
378    }
379
380    /**
381     * Advertise an error that should not take place if the code was
382     * written properly
383     * @param string $message
384     * @param string $canonical
385     * @param Throwable|null $previous
386     * @return void
387     */
388    public static function internalError(string $message, string $canonical = "support", Throwable $previous = null)
389    {
390        $internalErrorMessage = "Sorry. An internal error has occurred";
391        if (PluginUtility::isDevOrTest()) {
392            throw new ExceptionRuntimeInternal("$internalErrorMessage - $message", $canonical, 1, $previous);
393        } else {
394            $errorPreviousMessage = "";
395            if ($previous !== null) {
396                $errorPreviousMessage = " Error: {$previous->getMessage()}";
397            }
398            self::error("{$internalErrorMessage}: $message.$errorPreviousMessage", $canonical);
399        }
400    }
401
402    /**
403     * @param string $message
404     * @param string $canonical
405     * @param $e
406     * @return void
407     * Debug, trace
408     */
409    public static function debug(string $message, string $canonical = self::SUPPORT_CANONICAL, $e = null)
410    {
411        self::msg($message, LogUtility::LVL_MSG_DEBUG, $canonical, $e);
412    }
413
414    public static function infoToPublic(string $html, string $canonical)
415    {
416        self::log2FrontEnd($html, LogUtility::LVL_MSG_INFO, $canonical, true);
417    }
418
419}
420