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