File modules/cms/Dispatcher.class.php

Last commit: Thu Feb 16 22:57:35 2023 +0100	Jan Dankert	New: More functions (adding, deleting) for tags.
1 <?php 2 3 /* 4 * Loading and calling the action class (the "controller"). 5 */ 6 namespace cms; 7 8 use BadMethodCallException; 9 use cms\action\Action; 10 use cms\action\RequestParams; 11 use cms\action\Response; 12 use cms\auth\Auth; 13 use cms\auth\InternalAuth; 14 use cms\base\Configuration; 15 use cms\base\DB; 16 use cms\base\DefaultConfig; 17 use cms\base\Startup; 18 use cms\base\Version; 19 use cms\model\User; 20 use configuration\Config; 21 use configuration\ConfigurationLoader; 22 use database\Database; 23 use cms\update\Update; 24 use Exception; 25 use language\Language; 26 use language\Messages; 27 use util\Cookie; 28 use util\ClassName; 29 use util\ClassUtils; 30 use util\exception\DatabaseException; 31 use util\exception\ObjectNotFoundException; 32 use util\exception\ValidationException; 33 use util\FileUtils; 34 use util\Http; 35 use logger\Logger; 36 use LogicException; 37 use util\exception\UIException; 38 use util\exception\SecurityException; 39 use util\json\JSON; 40 use util\Request; 41 use util\Session; 42 use util\Text; 43 use util\text\TextMessage; 44 45 46 /** 47 * Dispatcher for all cms actions. 48 * 49 * @package cms 50 */ 51 class Dispatcher 52 { 53 /** 54 * @var RequestParams 55 */ 56 private $request; 57 58 /** 59 * @var Response 60 */ 61 private $response; 62 63 /** 64 * Vollständige Abarbeitug einer Aktion. 65 * Führt die gesamte Abarbeitung einer Aktion durch, incl. Datenbank-Transaktionssteuerung. 66 */ 67 public function doAction() 68 { 69 if ( $this->request->withAuthorization ) 70 ; // No session required (otherwise every API request would create a new session) 71 else 72 Session::start(); 73 74 $this->checkConfiguration(); 75 76 define('PRODUCTION' , Configuration::Conf()->is('production',true)); 77 define('DEVELOPMENT', !PRODUCTION); 78 79 if( DEVELOPMENT) 80 { 81 ini_set('display_errors' , 1); 82 ini_set('display_startup_errors', 1); 83 error_reporting(E_ALL); 84 }else { 85 ini_set('display_errors' , 0); 86 ini_set('display_startup_errors', 0); 87 error_reporting(0); 88 } 89 90 $this->setContentLanguageHeader(); 91 92 // Nachdem die Konfiguration gelesen wurde, kann nun der Logger benutzt werden. 93 $this->initializeLogger(); 94 95 // Sollte nur 1x pro Sitzung ausgeführt werden. Wie ermitteln wir das? 96 //if ( DEVELOPMENT ) 97 // Logger::debug( "Effective configuration:\n".YAML::YAMLDump($conf) ); 98 99 $umask = Configuration::subset('security')->get('umask',''); 100 if ( $umask ) 101 umask(octdec($umask)); 102 103 $timeout = Configuration::subset('interface')->get('timeout',0); 104 if ( $timeout ) 105 set_time_limit($timeout); 106 107 $this->checkPostToken(); 108 109 $this->connectToDatabase(); 110 $this->startDatabaseTransaction(); 111 $this->checkLogin(); 112 113 try{ 114 $this->callActionMethod(); 115 } 116 catch(Exception $e) 117 { 118 // In case of exception, rolling back the transaction 119 try 120 { 121 $this->rollbackDatabaseTransaction(); 122 } 123 catch(Exception $re) 124 { 125 Logger::warn("rollback failed:".$e->getMessage()); 126 } 127 128 throw $e; 129 } 130 131 $this->writeAuditLog(); 132 $this->commitDatabaseTransaction(); 133 134 if ( DEVELOPMENT ) 135 Logger::trace('Output' . "\n" . print_r( $this->response->getOutputData(),true)); 136 137 138 // Ablaufzeit für den Inhalt auf aktuelle Zeit setzen. 139 if ( !$this->response->hasHeader('Expires') ) 140 $this->response->addHeader('Expires', substr(date('r', time() - date('Z')), 0, -5) . 'GMT', false); 141 } 142 143 144 /** 145 * @param $request RequestParams 146 * @param $response Response 147 */ 148 public function setRequestAndResponse($request, $response) 149 { 150 $this->request = $request; 151 $this->response = $response; 152 } 153 154 155 156 /** 157 * Clear up after the work is done. 158 */ 159 private function clear() { 160 // Yes, closing the session flushes the session data and unlocks other waiting requests. 161 // Now another request is able to be executed. 162 Session::close(); 163 if ( $this->request->authUser ) { 164 session_destroy(); 165 setcookie('or_sid','',time()); 166 } 167 } 168 169 170 /** 171 * Make an authentication, if there is a HTTP authorization. 172 */ 173 private function checkLogin() { 174 175 if ( $this->request->withAuthorization ) { 176 $securityConfig = Configuration::subset('security'); 177 $authConfig = $securityConfig->subset('authorization'); 178 179 if ( $this->request->authUser ) { 180 if ( ! $authConfig->is('basic')) 181 throw new SecurityException('Basic Authorization is disabled'); 182 183 $userAuth = new InternalAuth(); 184 $status = $userAuth->login( $this->request->authUser,$this->request->authPassword,'' ); 185 if ( ! ($status & Auth::STATUS_SUCCESS) ) 186 throw new SecurityException('user cannot be authenticated'); 187 188 $user = User::loadWithName( $this->request->authUser,User::AUTH_TYPE_INTERNAL ); 189 Request::setUser( $user ); 190 } 191 elseif ( $authToken = $this->request->authToken ) { 192 if ( ! $authConfig->is('bearer')) 193 throw new SecurityException('Bearer Authorization is disabled'); 194 195 // We are expecting a JSON webtoken here 196 function base64url_decode( $data ){ 197 return base64_decode( strtr( $data, '-_', '+/') . str_repeat('=', 3 - ( 3 + strlen( $data )) % 4 )); 198 } 199 200 list( $b64Header,$b64Payload,$signature ) = array_pad(explode('.',$authToken),3,''); 201 $header = JSON::decode( base64url_decode($b64Header) ); 202 $supportedAlgos = [ 203 'HS256' => 'sha256', 204 'HS384' => 'sha384', 205 'HS512' => 'sha512', 206 ]; 207 if ( ! $algo = @$supportedAlgos[ @$header['alg'] ] ) 208 throw new SecurityException('Unknown algorithm in JWT, only supporting: '.implode(',',array_keys($supportedAlgos) )); 209 elseif( ! in_array( $algo,hash_algos() ) ) 210 throw new SecurityException('Unsupported algorithm'); 211 elseif( hash_hmac( $algo,$b64Header.'.'.$b64Payload,$securityConfig->get('key'),true) != base64url_decode($signature) ) 212 throw new SecurityException('Signature does not match'); 213 214 $payload = JSON::decode( base64url_decode($b64Payload)); 215 216 if ( $notBefore = @$payload['nbf'] ) 217 if ( $notBefore < Startup::getStartTime() ) 218 throw new SecurityException('token is not valid'); 219 220 if ( $expires = @$payload['exp'] ) 221 if ( $expires > Startup::getStartTime() ) 222 throw new SecurityException('token is not valid'); 223 224 if ( $subject = @$payload['sub'] ) { 225 if ( $user = User::loadWithName( $subject,User::AUTH_TYPE_INTERNAL ) ) 226 Request::setUser( $user ); 227 else 228 throw new SecurityException('User not found'); 229 } 230 else 231 throw new SecurityException('User not found'); 232 } 233 } 234 } 235 236 237 /** 238 * checks if the request contains a pleasant CSRF token. 239 * 240 * @return void 241 */ 242 private function checkPostToken() 243 { 244 if ( $this->request->withAuthorization ) 245 return; // no CSRF token necessary if there are no cookies used for authentication. 246 247 if ( Configuration::subset('security')->is('use_post_token',true) && 248 $this->request->isAction && 249 $this->request->getToken() != Session::token() ) { 250 Logger::warn( TextMessage::create( 251 'Token mismatch: Needed ${expected}), but got ${actual} Maybe an attacker?', 252 [ 253 'expected' => Session::token(), 254 'actual' => $this->request->getToken() 255 ]) 256 ); 257 throw new SecurityException("Token mismatch"); 258 } 259 } 260 261 /** 262 * Logger initialisieren. 263 */ 264 private function initializeLogger() 265 { 266 $logConfig = Configuration::subset('log'); 267 268 Logger::$messageFormat = $logConfig->get('format',['time','level','host','text']); 269 270 Logger::$logto = 0; // initially disable all logging endpoints. 271 272 $logFile = $logConfig->get('file',''); 273 if ( $logFile ) { 274 // Write to a logfile 275 if ( FileUtils::isRelativePath($logFile) ) 276 $logFile = __DIR__ . '/../../' . $logFile; // prepend relativ path to app root 277 Logger::$filename = $logFile; 278 Logger::$logto = Logger::$logto |= Logger::LOG_TO_FILE; 279 } 280 if ( $logConfig->is('syslog')) // write to syslog 281 Logger::$logto = Logger::$logto |= Logger::LOG_TO_ERROR_LOG; 282 if ( $logConfig->is('stdout')) // write to standard out 283 Logger::$logto = Logger::$logto |= Logger::LOG_TO_STDOUT; 284 if ( $logConfig->is('stderr')) // write to standard error 285 Logger::$logto = Logger::$logto |= Logger::LOG_TO_STDERR; 286 287 Logger::$dateFormat = $logConfig->get('date_format','r'); // 'M j H:i:s' 288 Logger::$nsLookup = $logConfig->is('ns_lookup',false); 289 290 Logger::$outputType = (int) @constant('\\logger\\Logger::OUTPUT_' . strtoupper($logConfig->get('output','PLAIN'))); 291 Logger::$level = (int) @constant('\\logger\\Logger::LEVEL_' . strtoupper($logConfig->get('level' ,'WARN' ))); 292 293 Logger::$messageCallback = function ( $key ) { 294 295 switch( $key) { 296 case 'action': 297 return Session::get('action'); 298 299 case 'user': 300 $user = Request::getUser(); 301 if (is_object($user)) 302 return $user->name; 303 else 304 return ''; 305 default: 306 return ''; 307 } 308 }; 309 310 Logger::init(); 311 } 312 313 private function checkConfiguration() 314 { 315 $conf = Request::getConfig(); 316 317 $configFile = getenv( 'CMS_CONFIG_FILE' ) ?: Startup::DEFAULT_CONFIG_FILE; 318 319 // Konfiguration lesen. 320 // Wenn Konfiguration noch nicht in Session vorhanden oder die Konfiguration geändert wurde (erkennbar anhand des Datei-Datums) 321 // dann die Konfiguration neu einlesen. 322 $configLoader = new ConfigurationLoader( $configFile ); 323 324 if (!is_array($conf) || @$conf['config']['auto_reload'] && $configLoader->lastModificationTime() > @$conf['config']['last_modification_time']) { 325 326 // Da die Konfiguration neu eingelesen wird, sollten wir auch die Sitzung komplett leeren. 327 if (is_array($conf) && $conf['config']['session_destroy_on_config_reload']) 328 session_unset(); 329 330 // Fest eingebaute Standard-Konfiguration laden. 331 $conf = DefaultConfig::get(); 332 333 $customConfig = $configLoader->load(); 334 $conf = array_replace_recursive($conf, $customConfig); 335 336 // Sprache lesen 337 $languages = []; 338 339 if ( $user = Request::getUser() ) 340 $languages[] = $user->language; // user language has precedence. 341 else { 342 $i18nConfig = (new Config($conf))->subset('i18n'); 343 344 $languages[] = $i18nConfig->get('default','en'); 345 346 if ( $i18nConfig->is('use_http',true ) ) 347 // Die vom Browser angeforderten Sprachen ermitteln 348 $languages = array_merge( $languages,Http::getLanguages() ); 349 } 350 351 // Default-Sprache hinzufuegen. 352 // Wird dann verwendet, wenn die vom Browser angeforderten Sprachen 353 // nicht vorhanden sind 354 $languages[] = 'en'; // last fallback. 355 356 357 foreach ($languages as $l) { 358 if (!in_array($l, Messages::$AVAILABLE_LANGUAGES)) 359 continue; // language is not available. 360 361 $language = new Language(); 362 $lang = $language->getLanguage( $l ); 363 $conf['language'] = $lang; 364 $conf['language']['language_code'] = $l; 365 break; 366 } 367 368 369 if (!isset($conf['language'])) 370 throw new \LogicException('no language found! (languages=' . implode(',', $languages) . ')'); 371 372 // Schreibt die Konfiguration in die Sitzung. Diese wird anschliessend nicht 373 // mehr veraendert. 374 Request::setConfig($conf); 375 } 376 377 } 378 379 /** 380 * Aufruf der Action-Methode. 381 * 382 * @return Response Vollständige Rückgabe aller Daten 383 */ 384 private function callActionMethod() 385 { 386 $action = $this->request->action; 387 $method = $this->request->method; 388 389 while( true ) { 390 $actionClassName = new ClassName( ucfirst($action) . ucfirst($method) . 'Action'); 391 $actionClassName->addNamespace( 'cms\\' . ($this->request->isUIAction ? 'ui\\' : '') . 'action\\' . $action ); 392 393 if ( $actionClassName->exists() ) 394 break; 395 396 $baseActionClassName = new ClassName( ucfirst($action) . 'Action' ); 397 $baseActionClassName->addNamespace( 'cms\\' . ($this->request->isUIAction ? 'ui\\' : '') . 'action' ); 398 399 if ( ! $baseActionClassName->exists() ) 400 throw new LogicException('Action \''.$action.'\' is not available, class not found: '.$baseActionClassName->get() ); 401 402 if ( ! $baseActionClassName->getParent()->exists() ) 403 throw new BadMethodCallException($baseActionClassName->get().' does not exist.'); 404 405 $action = strtolower( $baseActionClassName->dropNamespace()->dropSuffix('Action')->get() ); 406 407 if ( ! $action ) { 408 throw new BadMethodCallException( TextMessage::create( 'No action found for action ${0} and method ${1}',[$this->request->action,$this->request->method] ) ); 409 } 410 } 411 412 413 // Erzeugen der Action-Klasse 414 $class = $actionClassName->get(); 415 /* @type $do Action */ 416 $do = new $class; 417 418 $do->request = $this->request; 419 $do->response = $this->response; 420 $do->init(); 421 422 $do->checkAccess(); 423 424 // POST-Request => ...Post() wird aufgerufen. 425 // GET-Request => ...View() wird aufgerufen. 426 $subactionMethodName = $this->request->isAction ? 'post' : 'view';; 427 428 // Daten werden nur angezeigt, die Sitzung kann also schon geschlossen werden. 429 // Halt! In Index-Action können Benutzer-Logins gesetzt werden. 430 if ( ! $this->request->isAction && $this->request->action != 'index' && $this->request->method != 'oidc' ) 431 Session::close(); 432 433 Logger::debug("Dispatcher executing {$action}/{$method}/" . $this->request->getId().' -> '.$actionClassName->get().'#'.$subactionMethodName.'()'); 434 435 436 try { 437 $method = new \ReflectionMethod($do,$subactionMethodName); 438 $params = []; 439 foreach( $method->getParameters() as $parameter ) { 440 $params[ $parameter->getName() ] = $this->request->getRequiredRaw($parameter->getName()); 441 } 442 443 $method->invokeArgs($do,$params); // <== Executing the Action 444 } 445 catch (ValidationException $ve) 446 { 447 // The validation exception is catched here 448 $do->addValidationError( $ve->fieldName,$ve->key,$ve->params ); 449 450 if ( !$this->request->isAction ) 451 // Validation exceptions should only be thrown in POST requests. 452 throw new BadMethodCallException("Validation error in GET request",0,$ve); 453 } 454 catch (\ReflectionException $re) 455 { 456 throw new BadMethodCallException("Method '$subactionMethodName' does not exist",0,$re); 457 } 458 459 // The action is able to change its method name. 460 $this->request = $do->request; 461 $this->request->action = $action; 462 } 463 464 /** 465 * Startet die Verbindung zur Datenbank. 466 */ 467 private function connectToDatabase() 468 { 469 $allDbConfig = Configuration::subset('database'); 470 471 // Filter all enabled databases 472 $enabledDatabases = array_filter($allDbConfig->subsets(), function ($dbConfig) { 473 return $dbConfig->is('enabled',true); 474 }); 475 476 $enabledDbids = array_keys( $enabledDatabases ); 477 478 if ( ! $enabledDbids ) 479 throw new UIException(Messages::DATABASE_CONNECTION_ERROR, 'No database configured.', [], new DatabaseException('No database configured')); 480 481 $possibleDbIds = []; 482 483 if ( $databaseId = $this->request->getDatabaseId() ) 484 $possibleDbIds[] = $databaseId; 485 486 if ( $dbId = Request::getDatabaseId() ) 487 $possibleDbIds[] = $dbId; 488 489 if ( Cookie::has(Action::COOKIE_DB_ID) ) 490 $possibleDbIds[] = Cookie::get(Action::COOKIE_DB_ID); 491 492 $possibleDbIds[] = Configuration::subset('database-default')->get('default-id' ); 493 494 $possibleDbIds[] = $enabledDbids[0]; 495 496 foreach( $possibleDbIds as $dbId ) { 497 if ( in_array($dbId,$enabledDbids) ) { 498 499 $dbConfig = $allDbConfig->subset( $dbId ); 500 501 try 502 { 503 $key = $this->request->isAction && !Startup::readonly() ?'write':'read'; 504 505 $db = new Database( $dbConfig->merge( $dbConfig->subset($key))->getConfig() ); 506 $db->id = $dbId; 507 508 } 509 catch(\Exception $e) { 510 throw new UIException(Messages::DATABASE_CONNECTION_ERROR, "Could not connect to DB " . $dbId, [], $e); 511 } 512 513 // Is this the first time we are connected to this database in this session? 514 $firstDbContact = Request::getDatabaseId() != $dbId; 515 516 Request::setDatabase ( $db ); 517 518 if ( $firstDbContact ) 519 // Test, if we must install/update the database scheme. 520 $this->updateDatabase( $dbId ); 521 522 return; 523 } 524 } 525 526 throw new LogicException('Unreachable code'); // at least the first db connection should be found 527 } 528 529 530 531 /** 532 * Updating the database. 533 * 534 * @param $dbid integer 535 * @throws UIException 536 */ 537 private function updateDatabase($dbid) 538 { 539 $dbConfig = Configuration::Conf()->subset('database')->subset($dbid); 540 541 if ( ! $dbConfig->is('check_version',true)) 542 return; // Check for DB version is disabled. 543 544 $updater = new Update(); 545 546 if ( ! $updater->isUpdateRequired( DB::get() ) ) 547 return; 548 549 Logger::error("Database update required. Please call /status/?upgrade"); 550 throw new LogicException("Database update required, try calling /status/?upgrade"); 551 } 552 553 554 555 556 /** 557 * Eröffnet eine Transaktion. 558 */ 559 private function startDatabaseTransaction() 560 { 561 // Verbindung zur Datenbank 562 // 563 $db = Request::getDatabase(); 564 565 if (is_object($db)) { 566 // Transactions are only needed for POST-Request 567 // GET-Request do only read from the database and have no need for transactions. 568 if ( $this->request->isAction ) 569 { 570 $db->start(); 571 572 //register_shutdown_function( function() { 573 // $this->rollbackDatabaseTransaction(); 574 //}); 575 } 576 } 577 578 } 579 580 581 private function commitDatabaseTransaction() 582 { 583 $db = Request::getDatabase(); 584 585 if (is_object($db)) 586 // Transactions were only started for POST-Request 587 if($this->request->isAction) 588 $db->commit(); 589 } 590 591 592 593 private function rollbackDatabaseTransaction() 594 { 595 $db = Request::getDatabase(); 596 597 if (is_object($db)) 598 // Transactions were only started for POST-Request 599 if($this->request->isAction) 600 $db->rollback(); 601 } 602 603 604 /** 605 * Sets the "Content-Language"-HTTP-Header with the user language. 606 */ 607 private function setContentLanguageHeader() 608 { 609 header('Content-Language: ' . Configuration::Conf()->subset('language')->get('language_code') ); 610 } 611 612 613 614 private function writeAuditLog() 615 { 616 // Only write Audit Log for POST requests. 617 if ( ! $this->request->isAction ) 618 return; 619 620 $auditConfig = Configuration::subset('audit-log'); 621 622 if ( $auditConfig->is('enabled',false)) 623 { 624 $dir = $auditConfig->get('directory','./audit-log' ); 625 626 if ( $dir[0] != '/' ) 627 $dir = __DIR__ . '/../../' .$dir; 628 629 $micro_date = microtime(); 630 $date = explode(" ",$micro_date); 631 $filename = $dir.'/'.$auditConfig->get('prefix','audit' ).'-'.date('c',$date[1]).'-'.$date[0].'.json'; 632 633 $user = Request::getUser(); 634 635 $data = array( 636 'database' => array( 637 'id' => DB::get()->id ), 638 'user' => array( 639 'id' => @$user->userid, 640 'name' => @$user->name ), 641 'timestamp' => date('c'), 642 'action' => $this->request->action, 643 'method' => $this->request->method, 644 'remote-ip' => $_SERVER['REMOTE_ADDR'], 645 'request-time'=> $_SERVER['REQUEST_TIME'], 646 'data' => $this->filterCredentials( $_REQUEST ) 647 ); 648 649 // Write the file. 650 if ( file_put_contents( $filename, JSON::encode($data) ) === FALSE ) 651 Logger::warn('Could not write audit log to file: '.$filename); 652 else 653 Logger::debug('Audit logfile: '.$filename); 654 } 655 656 } 657 658 659 /* 660 * Filter credentials from an array. 661 */ 662 private function filterCredentials( $input ) 663 { 664 foreach( array( 'login_password','password1','password2' ) as $cr ) 665 if ( isset($input[$cr])) 666 $input[$cr] = '***'; 667 668 return $input; 669 } 670 }
Download modules/cms/Dispatcher.class.php
History Thu, 16 Feb 2023 22:57:35 +0100 Jan Dankert New: More functions (adding, deleting) for tags. Thu, 28 Apr 2022 00:28:24 +0200 Jan Dankert New: Login with Json webtoken (JWT) Fri, 15 Apr 2022 14:51:22 +0200 dankert Refactoring: User,Config and Database info is now stored in the Request, because so there is no session required for clients which are using Basic Authorization. Fri, 15 Apr 2022 12:50:31 +0200 dankert Code cleanup... Wed, 9 Mar 2022 13:28:52 +0100 dankert Refactoring: Checkbox values are always sent to the server. In the actions we must test the value with 'isTrue()' Wed, 9 Mar 2022 01:57:45 +0100 dankert Fix: Do not write the language to a cookie. Sun, 13 Feb 2022 23:52:02 +0100 dankert Fix: Output-data only in TRACE mode Sun, 13 Feb 2022 23:35:26 +0100 dankert Refactoring: New class "Response" which stores all output information. Mon, 7 Feb 2022 21:44:42 +0100 dankert New: Authenticate API users with the HTTP authorization header. Tue, 30 Nov 2021 00:26:25 +0100 Jan Dankert Shit, that was a pity. Tue, 30 Nov 2021 00:25:32 +0100 Jan Dankert Cleanup the notices. Thu, 7 Oct 2021 23:54:09 +0200 Jan Dankert New: Location of config file is able to be overwritten by environment. Sun, 14 Mar 2021 23:51:49 +0100 Jan Dankert Refactoring: Using the ValidationException where possible. Sun, 14 Mar 2021 22:29:56 +0100 Jan Dankert Refactoring: Clearer access check. Thu, 11 Mar 2021 00:01:47 +0100 Jan Dankert Refactoring: Cleaned the Request params. Wed, 10 Mar 2021 23:51:22 +0100 Jan Dankert Refactoring: Cleaned the Request params. Fri, 26 Feb 2021 01:06:01 +0100 Jan Dankert Refactoring accessing the request parameter values. Fri, 26 Feb 2021 00:04:49 +0100 Jan Dankert New: Request may contain JSON,XML in POST data. This is good for API clients. Thu, 18 Feb 2021 01:55:01 +0100 Jan Dankert New: Action for displaying a navigation while no other action is selected. Sun, 29 Nov 2020 21:46:57 +0100 Jan Dankert Auth modules should only use the Auth::STATUS_* constants as return value. Thu, 19 Nov 2020 23:27:20 +0100 Jan Dankert Fix: Updating database was partially broken. Thu, 19 Nov 2020 22:43:33 +0100 Jan Dankert New: Configure separate logging endpoints (file, syslog, stdout, stderr), so docker container may write directly to stdout. Thu, 19 Nov 2020 19:51:26 +0100 Jan Dankert Fix: Using a stream for log output (like php://stdout) Thu, 19 Nov 2020 14:53:31 +0100 Jan Dankert Using warn() instead of error() Thu, 19 Nov 2020 11:16:41 +0100 Jan Dankert Fix for the fix: DB connections must be enabled. Thu, 19 Nov 2020 11:12:31 +0100 Jan Dankert Fix: DB connections must be enabled. Thu, 19 Nov 2020 10:45:05 +0100 Jan Dankert Fix: Default database. Thu, 19 Nov 2020 00:45:44 +0100 Jan Dankert Security fix: We must update the login token on every login; Administrators are able to see the login tokens of users. Wed, 18 Nov 2020 20:42:57 +0100 Jan Dankert Getting/Setting cookies with constants, this is more safe. Wed, 18 Nov 2020 20:02:35 +0100 Jan Dankert Fix: Only follow parent classes up to the "Action" Baseclass. Tue, 17 Nov 2020 23:51:00 +0100 Jan Dankert Refactoring: Every Actionmethod has now its own class. Sun, 1 Nov 2020 00:36:50 +0100 Jan Dankert Refactoring: Only using the configuration object. Sat, 31 Oct 2020 03:48:03 +0100 Jan Dankert Some bad fixes for OIDC to work properly. Sat, 31 Oct 2020 01:19:06 +0100 Jan Dankert Better logging in the dispatcher. Mon, 26 Oct 2020 22:21:42 +0100 Jan Dankert Refactoring: Using TextMessage for creating Messages with user content. Mon, 26 Oct 2020 09:48:01 +0100 Jan Dankert No trace-output in the API in production mode. Mon, 26 Oct 2020 09:08:56 +0100 Jan Dankert Fix: Missing return statement :-O Sun, 25 Oct 2020 02:51:56 +0200 Jan Dankert Using the object-based configuration. Fri, 23 Oct 2020 23:09:52 +0200 Jan Dankert Refactoring: Using the new config classes. Wed, 7 Oct 2020 23:01:31 +0200 Jan Dankert Cleanup: Refactored file seperator char with an unicode char. Sun, 4 Oct 2020 21:24:40 +0200 Jan Dankert Fix: Throw correct DatabaseException Tue, 29 Sep 2020 22:17:11 +0200 Jan Dankert Refactoring: Do not use global constants. Sun, 27 Sep 2020 00:48:43 +0200 Jan Dankert Fix: Treeaction is an UI action, so ist is not available via the API. Now there is an ugly workaround for that, we have to create a template for this calls. Sat, 26 Sep 2020 22:07:53 +0200 Jan Dankert Removing superfluous code. Sat, 26 Sep 2020 21:42:51 +0200 Jan Dankert Refactoring: The UI Actions are now in their own namespace. No need for a confusing require file. Sat, 26 Sep 2020 18:46:36 +0200 Jan Dankert Now compatible with PHP 5.4 again. Sat, 26 Sep 2020 13:11:23 +0200 Jan Dankert Refactoring: No global variables any more. All constants are capsulated by classes. Sat, 26 Sep 2020 12:20:43 +0200 Jan Dankert Refactoring: No global variables like $SESS any more. All constants are capsulated by classes. Sat, 26 Sep 2020 10:32:02 +0200 Jan Dankert Refactoring: No global $conf array any more. Sat, 26 Sep 2020 04:26:55 +0200 Jan Dankert Refactoring: read configuration values with a class. Sat, 26 Sep 2020 03:03:47 +0200 Jan Dankert Refactoring: less global functions. Sat, 26 Sep 2020 02:26:39 +0200 Jan Dankert Refactoring: No global functions any more, the database object is read from the Db class. Sat, 26 Sep 2020 01:41:20 +0200 Jan Dankert Refactoring: Removing old require.php files. With class autoloading, they are not necessary any more. Fri, 25 Sep 2020 23:59:08 +0200 Jan Dankert Refactoring: capsulate the default config in a class. Fri, 25 Sep 2020 23:37:38 +0200 Jan Dankert Refactoring: The logger is able to output json format (for cloud installations) Thu, 10 Sep 2020 18:02:54 +0200 Jan Dankert The dispatcher is now able to call the action methods with parameters. Sat, 29 Aug 2020 03:23:06 +0200 Jan Dankert Refactoring: Improved Exception-Handling; New: Generating pages using a page context which considers page aliases. Sat, 22 Aug 2020 23:13:01 +0200 Jan Dankert Security: Configuration-setting for the SameSite-Cookie-Policy. Fri, 21 Aug 2020 00:22:13 +0200 Jan Dankert Refactoring: Collect all frontend compiler scripts in update.php. Compiling of CSS and JS was extracted to a new TemplateCompiler. JS and CSS is now collected in a new openrat.[min.][js|css]. Tue, 18 Aug 2020 23:27:37 +0200 Jan Dankert Security: Sanitize user input while logging (no logfile injection with potentially dangerous data) Sat, 16 May 2020 01:08:40 +0200 Jan Dankert Refactoring: Switching the ValueExpressions in the templates to the new VariableResolver for supporting nested variables like ${message:prefix_${key}}. Mon, 24 Feb 2020 22:57:16 +0100 Jan Dankert Fix: Broken paths. Sun, 23 Feb 2020 04:49:34 +0100 Jan Dankert Refactoring with Namespaces for the cms modules, part 2. Sun, 23 Feb 2020 04:01:30 +0100 Jan Dankert Refactoring with Namespaces for the cms modules, part 1: moving.