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 $sep = " - "; 156 $messageWritten = date('c') . $sep . self::LVL_NAME[$logLevel] . $sep . $msg . $sep . $INPUT->server->str('REMOTE_ADDR') . $sep . $id . "\n"; 157 // dokuwiki does not have the warning level 158 Logger::error($messageWritten); 159 self::throwErrorIfTest($logLevel, $msg, $e); 160 161 162 } 163 164 } 165 166 /** 167 * @param $message 168 * @param $level 169 * @param string $canonical 170 * @param bool $publicMessage 171 */ 172 public static function log2FrontEnd($message, $level, string $canonical = self::SUPPORT_CANONICAL, bool $publicMessage = false) 173 { 174 175 try { 176 self::messageNotEmpty($message); 177 } catch (ExceptionCompile $e) { 178 $message = $e->getMessage(); 179 $level = self::LVL_MSG_ERROR; 180 } 181 182 /** 183 * If we are not in the console 184 * and not in test 185 * we test that the message comes in the front end 186 * (example {@link \plugin_combo_frontmatter_test} 187 */ 188 $isTerminal = Console::isConsoleRun(); 189 if ($isTerminal) { 190 if (!defined('DOKU_UNITTEST')) { 191 /** 192 * such as {@link cli_plugin_combo} 193 */ 194 $userAgent = "cli"; 195 } else { 196 $userAgent = "phpunit"; 197 } 198 } else { 199 $userAgent = "browser"; 200 } 201 202 switch ($userAgent) { 203 case "cli": 204 echo "$message\n"; 205 break; 206 case "phpunit": 207 case "browser": 208 default: 209 if ($canonical !== null) { 210 $label = ucfirst(str_replace(":", " ", $canonical)); 211 $htmlMsg = PluginUtility::getDocumentationHyperLink($canonical, $label, false); 212 } else { 213 $htmlMsg = PluginUtility::getDocumentationHyperLink("", PluginUtility::$PLUGIN_NAME, false); 214 } 215 216 217 /** 218 * Adding page - context information 219 * We are not creating the page 220 * direction from {@link MarkupPath::createFromRequestedPage()} 221 * because it throws an error message when the environment 222 * is not good, creating a recursive call. 223 */ 224 global $INPUT; 225 $id = $INPUT->str("id"); 226 if ($id != null) { 227 228 /** 229 * We don't use any Page object to not 230 * create a cycle while building it 231 */ 232 $url = wl($id, [], true); 233 $htmlMsg .= " - <a href=\"$url\">$id</a>"; 234 235 } 236 237 /** 238 * 239 */ 240 $htmlMsg .= " - " . $message; 241 if ($level > self::LVL_MSG_DEBUG) { 242 $dokuWikiLevel = self::LVL_TO_MSG_LEVEL[$level]; 243 if ($publicMessage) { 244 $allow = MSG_PUBLIC; 245 } else { 246 $allow = MSG_USERS_ONLY; 247 } 248 msg($htmlMsg, $dokuWikiLevel, '', '', $allow); 249 } 250 } 251 } 252 253 /** 254 * Log a message to the browser console 255 * @param $message 256 */ 257 public static function log2BrowserConsole($message) 258 { 259 // TODO 260 } 261 262 263 /** 264 * @param $level 265 * @param $message 266 * @param $e - the original exception for chaining 267 * @return void 268 */ 269 private static function throwErrorIfTest($level, $message, \Exception $e = null) 270 { 271 if (PluginUtility::isTest() && self::$throwExceptionOnDevTest) { 272 try { 273 $actualLevel = ExecutionContext::getExecutionContext()->getConfig()->getLogExceptionLevel(); 274 } catch (ExceptionNotFound $e) { 275 // In context creation 276 return; 277 } 278 if ($level >= $actualLevel) { 279 throw new LogException($message, $level, $e); 280 } 281 } 282 } 283 284 /** 285 * @param string|null $message 286 * @throws ExceptionCompile 287 */ 288 private static function messageNotEmpty(?string $message) 289 { 290 $message = trim($message); 291 if ($message === null || $message === "") { 292 $newMessage = "The passed message to the log was empty or null. BackTrace: \n"; 293 $newMessage .= LogUtility::getCallStack(); 294 throw new ExceptionCompile($newMessage); 295 } 296 } 297 298 public static function disableThrowExceptionOnDevTest() 299 { 300 self::$throwExceptionOnDevTest = false; 301 } 302 303 public static function enableThrowExceptionOnDevTest() 304 { 305 self::$throwExceptionOnDevTest = true; 306 } 307 308 public static function wrapInRedForHtml(string $message): string 309 { 310 return "<span class=\"text-danger\">$message</span>"; 311 } 312 313 /** 314 * @return false|string - the actual php call stack (known as backtrace) 315 */ 316 public static function getCallStack() 317 { 318 ob_start(); 319 $limit = 10; 320 /** 321 * DEBUG_BACKTRACE_IGNORE_ARGS options to avoid 322 * PHP Fatal error: Allowed memory size of 2147483648 bytes exhausted (tried to allocate 1876967424 bytes) 323 */ 324 debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $limit); // It prints also the data passed :) 325 $trace = ob_get_contents(); 326 ob_end_clean(); 327 return $trace; 328 } 329 330 /** 331 * @param string $message the message 332 * @param string $canonical the page 333 * @param \Exception|null $e the original exception for trace chaining 334 * @return void 335 */ 336 public static function error(string $message, string $canonical = self::SUPPORT_CANONICAL, \Exception $e = null) 337 { 338 self::msg($message, LogUtility::LVL_MSG_ERROR, $canonical, $e); 339 } 340 341 public static function warning(string $message, string $canonical = self::SUPPORT_CANONICAL, \Exception $e = null) 342 { 343 self::msg($message, LogUtility::LVL_MSG_WARNING, $canonical, $e); 344 } 345 346 public static function info(string $message, string $canonical = self::SUPPORT_CANONICAL, \Exception $e = null) 347 { 348 self::msg($message, LogUtility::LVL_MSG_INFO, $canonical, $e); 349 } 350 351 /** 352 * @param int $level 353 * @return void 354 * @deprecated use {@link SiteConfig::setLogExceptionLevel()} 355 */ 356 public static function setTestExceptionLevel(int $level) 357 { 358 ExecutionContext::getActualOrCreateFromEnv()->getConfig()->setLogExceptionLevel($level); 359 } 360 361 public static function setTestExceptionLevelToDefault() 362 { 363 ExecutionContext::getActualOrCreateFromEnv()->getConfig()->setLogExceptionLevel(self::LVL_MSG_WARNING); 364 } 365 366 public static function errorIfDevOrTest($message, $canonical = "support") 367 { 368 if (PluginUtility::isDevOrTest()) { 369 LogUtility::error($message, $canonical); 370 } 371 } 372 373 /** 374 * @return void 375 * @deprecated use the config object instead 376 */ 377 public static function setTestExceptionLevelToError() 378 { 379 ExecutionContext::getActualOrCreateFromEnv()->getConfig()->setLogExceptionToError(); 380 } 381 382 /** 383 * Advertise an error that should not take place if the code was 384 * written properly 385 * @param string $message 386 * @param string $canonical 387 * @param Throwable|null $previous 388 * @return void 389 */ 390 public static function internalError(string $message, string $canonical = "support", Throwable $previous = null) 391 { 392 $internalErrorMessage = "Sorry. An internal error has occurred"; 393 if (PluginUtility::isDevOrTest()) { 394 throw new ExceptionRuntimeInternal("$internalErrorMessage - $message", $canonical, 1, $previous); 395 } else { 396 $errorPreviousMessage = ""; 397 if ($previous !== null) { 398 $errorPreviousMessage = " Error: {$previous->getMessage()}"; 399 } 400 self::error("{$internalErrorMessage}: $message.$errorPreviousMessage", $canonical); 401 } 402 } 403 404 /** 405 * @param string $message 406 * @param string $canonical 407 * @param $e 408 * @return void 409 * Debug, trace 410 */ 411 public static function debug(string $message, string $canonical = self::SUPPORT_CANONICAL, $e = null) 412 { 413 self::msg($message, LogUtility::LVL_MSG_DEBUG, $canonical, $e); 414 } 415 416 public static function infoToPublic(string $html, string $canonical) 417 { 418 self::log2FrontEnd($html, LogUtility::LVL_MSG_INFO, $canonical, true); 419 } 420 421} 422