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 if (PluginUtility::isTest() && self::$throwExceptionOnDevTest) { 277 try { 278 $actualLevel = ExecutionContext::getExecutionContext()->getConfig()->getLogExceptionLevel(); 279 } catch (ExceptionNotFound $e) { 280 // In context creation 281 return; 282 } 283 if ($level >= $actualLevel) { 284 throw new LogException($message, $level, $e); 285 } 286 } 287 } 288 289 /** 290 * @param string|null $message 291 * @throws ExceptionCompile 292 */ 293 private static function messageNotEmpty(?string $message) 294 { 295 $message = trim($message); 296 if ($message === null || $message === "") { 297 $newMessage = "The passed message to the log was empty or null. BackTrace: \n"; 298 $newMessage .= LogUtility::getCallStack(); 299 throw new ExceptionCompile($newMessage); 300 } 301 } 302 303 public static function disableThrowExceptionOnDevTest() 304 { 305 self::$throwExceptionOnDevTest = false; 306 } 307 308 public static function enableThrowExceptionOnDevTest() 309 { 310 self::$throwExceptionOnDevTest = true; 311 } 312 313 public static function wrapInRedForHtml(string $message): string 314 { 315 return "<span class=\"text-danger\">$message</span>"; 316 } 317 318 /** 319 * @return false|string - the actual php call stack (known as backtrace) 320 */ 321 public static function getCallStack() 322 { 323 ob_start(); 324 $limit = 10; 325 /** 326 * DEBUG_BACKTRACE_IGNORE_ARGS options to avoid 327 * PHP Fatal error: Allowed memory size of 2147483648 bytes exhausted (tried to allocate 1876967424 bytes) 328 */ 329 debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $limit); // It prints also the data passed :) 330 $trace = ob_get_contents(); 331 ob_end_clean(); 332 return $trace; 333 } 334 335 /** 336 * @param string $message the message 337 * @param string $canonical the page 338 * @param \Exception|null $e the original exception for trace chaining 339 * @return void 340 */ 341 public static function error(string $message, string $canonical = self::SUPPORT_CANONICAL, \Exception $e = null) 342 { 343 self::msg($message, LogUtility::LVL_MSG_ERROR, $canonical, $e); 344 } 345 346 public static function warning(string $message, string $canonical = "support", \Exception $e = null) 347 { 348 self::msg($message, LogUtility::LVL_MSG_WARNING, $canonical, $e); 349 } 350 351 public static function info(string $message, string $canonical = "support", \Exception $e = null) 352 { 353 self::msg($message, LogUtility::LVL_MSG_INFO, $canonical, $e); 354 } 355 356 /** 357 * @param int $level 358 * @return void 359 * @deprecated use {@link SiteConfig::setLogExceptionLevel()} 360 */ 361 public static function setTestExceptionLevel(int $level) 362 { 363 ExecutionContext::getActualOrCreateFromEnv()->getConfig()->setLogExceptionLevel($level); 364 } 365 366 public static function setTestExceptionLevelToDefault() 367 { 368 ExecutionContext::getActualOrCreateFromEnv()->getConfig()->setLogExceptionLevel(self::LVL_MSG_WARNING); 369 } 370 371 public static function errorIfDevOrTest($message, $canonical = "support") 372 { 373 if (PluginUtility::isDevOrTest()) { 374 LogUtility::error($message, $canonical); 375 } 376 } 377 378 /** 379 * @return void 380 * @deprecated use the config object instead 381 */ 382 public static function setTestExceptionLevelToError() 383 { 384 ExecutionContext::getActualOrCreateFromEnv()->getConfig()->setLogExceptionToError(); 385 } 386 387 /** 388 * Advertise an error that should not take place if the code was 389 * written properly 390 * @param string $message 391 * @param string $canonical 392 * @param Throwable|null $previous 393 * @return void 394 */ 395 public static function internalError(string $message, string $canonical = "support", Throwable $previous = null) 396 { 397 $internalErrorMessage = "Sorry. An internal error has occurred"; 398 if (PluginUtility::isDevOrTest()) { 399 throw new ExceptionRuntimeInternal("$internalErrorMessage - $message", $canonical, 1, $previous); 400 } else { 401 $errorPreviousMessage = ""; 402 if ($previous !== null) { 403 $errorPreviousMessage = " Error: {$previous->getMessage()}"; 404 } 405 self::error("{$internalErrorMessage}: $message.$errorPreviousMessage", $canonical); 406 } 407 } 408 409 /** 410 * @param string $message 411 * @param string $canonical 412 * @param $e 413 * @return void 414 * Debug, trace 415 */ 416 public static function debug(string $message, string $canonical = self::SUPPORT_CANONICAL, $e = null) 417 { 418 self::msg($message, LogUtility::LVL_MSG_DEBUG, $canonical, $e); 419 } 420 421 public static function infoToPublic(string $html, string $canonical) 422 { 423 self::log2FrontEnd($html, LogUtility::LVL_MSG_INFO, $canonical, true); 424 } 425 426} 427