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