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
15require_once(__DIR__ . '/PluginUtility.php');
16
17class LogUtility
18{
19
20    /**
21     * Constant for the function {@link msg()}
22     * -1 = error, 0 = info, 1 = success, 2 = notify
23     * (Not even in order of importance)
24     */
25    const LVL_MSG_ERROR = 4; //-1;
26    const LVL_MSG_WARNING = 3; //2;
27    const LVL_MSG_SUCCESS = 2; //1;
28    const LVL_MSG_INFO = 1; //0;
29    const LVL_MSG_DEBUG = 0; //3;
30
31
32    /**
33     * Id level to name
34     */
35    const LVL_NAME = array(
36        0 => "debug",
37        1 => "info",
38        3 => "warning",
39        2 => "success",
40        4 => "error"
41    );
42
43    /**
44     * Id level to name
45     * {@link msg()} constant
46     */
47    const LVL_TO_MSG_LEVEL = array(
48        0 => 3,
49        1 => 0,
50        2 => 1,
51        3 => 2,
52        4 => -1
53    );
54
55
56    const LOGLEVEL_URI_QUERY_PROPERTY = "loglevel";
57    /**
58     *
59     * @var bool
60     */
61    private static $throwExceptionOnDevTest = true;
62
63    /**
64     * Send a message to a manager and log it
65     * Fail if in test
66     * @param string $message
67     * @param int $level - the level see LVL constant
68     * @param string $canonical - the canonical
69     */
70    public static function msg(string $message, int $level = self::LVL_MSG_ERROR, string $canonical = "support")
71    {
72
73        try {
74            self::messageNotEmpty($message);
75        } catch (ExceptionCombo $e) {
76            self::log2file($e->getMessage(), LogUtility::LVL_MSG_ERROR, $canonical);
77        }
78
79        /**
80         * Log to frontend
81         */
82        self::log2FrontEnd($message, $level, $canonical);
83
84        /**
85         * Log level passed for a page (only for file used)
86         * to not allow an attacker to see all errors in frontend
87         */
88        global $INPUT;
89        $loglevelProp = $INPUT->str(self::LOGLEVEL_URI_QUERY_PROPERTY, null);
90        if (!empty($loglevelProp)) {
91            $level = $loglevelProp;
92        }
93        /**
94         * TODO: Make it a configuration ?
95         */
96        if ($level >= self::LVL_MSG_WARNING) {
97            self::log2file($message, $level, $canonical);
98        }
99
100        /**
101         * If test, we throw an error
102         */
103        self::throwErrorIfTest($level, $message);
104    }
105
106    /**
107     * Print log to a  file
108     *
109     * Adapted from {@link dbglog}
110     * Note: {@link dbg()} dbg print to the web page
111     *
112     * @param null|string $msg - may be null always this is the default if a variable is not initialized.
113     * @param int $logLevel
114     * @param null $canonical
115     */
116    static function log2file(?string $msg, int $logLevel = self::LVL_MSG_ERROR, $canonical = null)
117    {
118
119        try {
120            self::messageNotEmpty($msg);
121        } catch (ExceptionCombo $e) {
122            $msg = $e->getMessage();
123            $logLevel = self::LVL_MSG_ERROR;
124        }
125
126        if (PluginUtility::isTest() || $logLevel >= self::LVL_MSG_WARNING) {
127
128            $prefix = PluginUtility::$PLUGIN_NAME;
129            if (!empty($canonical)) {
130                $prefix .= ' - ' . $canonical;
131            }
132            $msg = $prefix . ' - ' . $msg;
133
134            global $INPUT;
135            global $conf;
136
137            /**
138             * Adding page - context information
139             * We are not using {@link Page::createPageFromRequestedPage()}
140             * because it throws an error message when the environment
141             * is not good, creating a recursive call.
142             */
143            $id = PluginUtility::getRequestedWikiId();
144
145            $file = $conf['cachedir'] . '/debug.log';
146            $fh = fopen($file, 'a');
147            if ($fh) {
148                $sep = " - ";
149                fwrite($fh, date('c') . $sep . self::LVL_NAME[$logLevel] . $sep . $msg . $sep . $INPUT->server->str('REMOTE_ADDR') . $sep . $id . "\n");
150                fclose($fh);
151            }
152
153
154            self::throwErrorIfTest($logLevel, $msg);
155
156
157        }
158
159    }
160
161    /**
162     * @param $message
163     * @param $level
164     * @param string $canonical
165     * @param bool $withIconURL
166     */
167    public static function log2FrontEnd($message, $level, $canonical = "support", $withIconURL = true)
168    {
169
170        try {
171            self::messageNotEmpty($message);
172        } catch (ExceptionCombo $e) {
173            $message = $e->getMessage();
174            $level = self::LVL_MSG_ERROR;
175        }
176
177        /**
178         * If we are not in the console
179         * and not in test
180         * we test that the message comes in the front end
181         * (example {@link \plugin_combo_frontmatter_test}
182         */
183        $isTerminal = Console::isConsoleRun();
184        if ($isTerminal) {
185            if (!defined('DOKU_UNITTEST')) {
186                /**
187                 * such as {@link cli_plugin_combo}
188                 */
189                $userAgent = "cli";
190            } else {
191                $userAgent = "phpunit";
192            }
193        } else {
194            $userAgent = "browser";
195        }
196
197        switch ($userAgent) {
198            case "cli":
199                echo "$message\n";
200                break;
201            case "phpunit":
202            case "browser":
203            default:
204                $htmlMsg = PluginUtility::getDocumentationHyperLink("", PluginUtility::$PLUGIN_NAME, $withIconURL);
205                if ($canonical != null) {
206                    $htmlMsg = PluginUtility::getDocumentationHyperLink($canonical, ucfirst(str_replace(":", " ", $canonical)));
207                }
208
209                /**
210                 * Adding page - context information
211                 * We are not creating the page
212                 * direction from {@link Page::createPageFromRequestedPage()}
213                 * because it throws an error message when the environment
214                 * is not good, creating a recursive call.
215                 */
216                $id = PluginUtility::getRequestedWikiId();
217                if ($id != null) {
218
219                    /**
220                     * We don't use any Page object to not
221                     * create a cycle while building it
222                     */
223                    $url = wl($id, [], true);
224                    $htmlMsg .= " - <a href=\"$url\">$id</a>";
225
226                }
227
228                /**
229                 *
230                 */
231                $htmlMsg .= " - " . $message;
232                if ($level > self::LVL_MSG_DEBUG) {
233                    $dokuWikiLevel = self::LVL_TO_MSG_LEVEL[$level];
234                    msg($htmlMsg, $dokuWikiLevel, '', '', MSG_USERS_ONLY);
235                }
236        }
237    }
238
239    /**
240     * Log a message to the browser console
241     * @param $message
242     */
243    public static function log2BrowserConsole($message)
244    {
245        // TODO
246    }
247
248    private static function throwErrorIfTest($level, $message)
249    {
250        if (PluginUtility::isTest()
251            && ($level >= self::LVL_MSG_WARNING)
252            && self::$throwExceptionOnDevTest
253        ) {
254            throw new LogException($message);
255        }
256    }
257
258    /**
259     * @param string|null $message
260     * @throws ExceptionCombo
261     */
262    private static function messageNotEmpty(?string $message)
263    {
264        $message = trim($message);
265        if ($message === null || $message === "") {
266            $newMessage = "The passed message to the log was empty or null. BackTrace: \n";
267            $newMessage .= LogUtility::getCallStack();
268            throw new ExceptionCombo($newMessage);
269        }
270    }
271
272    public static function disableThrowExceptionOnDevTest()
273    {
274        self::$throwExceptionOnDevTest = false;
275    }
276
277    public static function enableThrowExceptionOnDevTest()
278    {
279        self::$throwExceptionOnDevTest = true;
280    }
281
282    public static function wrapInRedForHtml(string $message): string
283    {
284        return "<span class=\"text-alert\">$message</span>";
285    }
286
287    /**
288     * @return false|string - the actual php call stack (known as backtrace)
289     */
290    public static function getCallStack()
291    {
292        ob_start();
293        $limit = 10;
294        /**
295         * DEBUG_BACKTRACE_IGNORE_ARGS options to avoid
296         * PHP Fatal error:  Allowed memory size of 2147483648 bytes exhausted (tried to allocate 1876967424 bytes)
297         */
298        debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $limit); // It prints also the data passed :)
299        $trace = ob_get_contents();
300        ob_end_clean();
301        return $trace;
302    }
303}
304