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        if (PluginUtility::isTest() && self::$throwExceptionOnDevTest) {
277            try {
278                $actualLevel = ExecutionContext::getExecutionContext()->getConfig()->getLogExceptionLevel();
279            } catch (ExceptionNotFound $e) {
280                // In context creation
281                return;
282            }
283            if ($level >= $actualLevel) {
284                throw new LogException($message, $level, $e);
285            }
286        }
287    }
288
289    /**
290     * @param string|null $message
291     * @throws ExceptionCompile
292     */
293    private static function messageNotEmpty(?string $message)
294    {
295        $message = trim($message);
296        if ($message === null || $message === "") {
297            $newMessage = "The passed message to the log was empty or null. BackTrace: \n";
298            $newMessage .= LogUtility::getCallStack();
299            throw new ExceptionCompile($newMessage);
300        }
301    }
302
303    public static function disableThrowExceptionOnDevTest()
304    {
305        self::$throwExceptionOnDevTest = false;
306    }
307
308    public static function enableThrowExceptionOnDevTest()
309    {
310        self::$throwExceptionOnDevTest = true;
311    }
312
313    public static function wrapInRedForHtml(string $message): string
314    {
315        return "<span class=\"text-danger\">$message</span>";
316    }
317
318    /**
319     * @return false|string - the actual php call stack (known as backtrace)
320     */
321    public static function getCallStack()
322    {
323        ob_start();
324        $limit = 10;
325        /**
326         * DEBUG_BACKTRACE_IGNORE_ARGS options to avoid
327         * PHP Fatal error:  Allowed memory size of 2147483648 bytes exhausted (tried to allocate 1876967424 bytes)
328         */
329        debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $limit); // It prints also the data passed :)
330        $trace = ob_get_contents();
331        ob_end_clean();
332        return $trace;
333    }
334
335    /**
336     * @param string $message the message
337     * @param string $canonical the page
338     * @param \Exception|null $e the original exception for trace chaining
339     * @return void
340     */
341    public static function error(string $message, string $canonical = self::SUPPORT_CANONICAL, \Exception $e = null)
342    {
343        self::msg($message, LogUtility::LVL_MSG_ERROR, $canonical, $e);
344    }
345
346    public static function warning(string $message, string $canonical = "support", \Exception $e = null)
347    {
348        self::msg($message, LogUtility::LVL_MSG_WARNING, $canonical, $e);
349    }
350
351    public static function info(string $message, string $canonical = "support", \Exception $e = null)
352    {
353        self::msg($message, LogUtility::LVL_MSG_INFO, $canonical, $e);
354    }
355
356    /**
357     * @param int $level
358     * @return void
359     * @deprecated use {@link SiteConfig::setLogExceptionLevel()}
360     */
361    public static function setTestExceptionLevel(int $level)
362    {
363        ExecutionContext::getActualOrCreateFromEnv()->getConfig()->setLogExceptionLevel($level);
364    }
365
366    public static function setTestExceptionLevelToDefault()
367    {
368        ExecutionContext::getActualOrCreateFromEnv()->getConfig()->setLogExceptionLevel(self::LVL_MSG_WARNING);
369    }
370
371    public static function errorIfDevOrTest($message, $canonical = "support")
372    {
373        if (PluginUtility::isDevOrTest()) {
374            LogUtility::error($message, $canonical);
375        }
376    }
377
378    /**
379     * @return void
380     * @deprecated use the config object instead
381     */
382    public static function setTestExceptionLevelToError()
383    {
384        ExecutionContext::getActualOrCreateFromEnv()->getConfig()->setLogExceptionToError();
385    }
386
387    /**
388     * Advertise an error that should not take place if the code was
389     * written properly
390     * @param string $message
391     * @param string $canonical
392     * @param Throwable|null $previous
393     * @return void
394     */
395    public static function internalError(string $message, string $canonical = "support", Throwable $previous = null)
396    {
397        $internalErrorMessage = "Sorry. An internal error has occurred";
398        if (PluginUtility::isDevOrTest()) {
399            throw new ExceptionRuntimeInternal("$internalErrorMessage - $message", $canonical, 1, $previous);
400        } else {
401            $errorPreviousMessage = "";
402            if ($previous !== null) {
403                $errorPreviousMessage = " Error: {$previous->getMessage()}";
404            }
405            self::error("{$internalErrorMessage}: $message.$errorPreviousMessage", $canonical);
406        }
407    }
408
409    /**
410     * @param string $message
411     * @param string $canonical
412     * @param $e
413     * @return void
414     * Debug, trace
415     */
416    public static function debug(string $message, string $canonical = self::SUPPORT_CANONICAL, $e = null)
417    {
418        self::msg($message, LogUtility::LVL_MSG_DEBUG, $canonical, $e);
419    }
420
421    public static function infoToPublic(string $html, string $canonical)
422    {
423        self::log2FrontEnd($html, LogUtility::LVL_MSG_INFO, $canonical, true);
424    }
425
426}
427