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