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