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