Dispatcher.class.php (18643B)
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 ConfigurationLoader; 12 use database\Database; 13 use DbUpdate; 14 use Http; 15 use Logger; 16 use LogicException; 17 use OpenRatException; 18 use SecurityException; 19 use Session; 20 21 22 /** 23 * Dispatcher for all cms actions. 24 * 25 * @package cms 26 */ 27 class Dispatcher 28 { 29 /** 30 * @var RequestParams 31 */ 32 public $request; 33 34 /** 35 * Vollständige Abarbeitug einer Aktion. 36 * Führt die gesamte Abarbeitung einer Aktion durch, incl. Datenbank-Transaktionssteuerung. 37 * 38 * @return array data for the client 39 */ 40 public function doAction() 41 { 42 // Start the session. All classes should have been loaded up to now. 43 session_start(); 44 45 $this->checkConfiguration(); 46 47 // Vorhandene Konfiguration aus der Sitzung lesen. 48 global $conf; 49 $conf = Session::getConfig(); 50 51 define('PRODUCTION', Conf()->is('production',true)); 52 define('DEVELOPMENT', !PRODUCTION); 53 54 if( DEVELOPMENT) 55 { 56 ini_set('display_errors', 1); 57 ini_set('display_startup_errors', 1); 58 error_reporting(E_ALL); 59 }else { 60 ini_set('display_errors', 0); 61 ini_set('display_startup_errors', 0); 62 error_reporting(0); 63 } 64 65 $this->setContentLanguageHeader(); 66 67 // Nachdem die Konfiguration gelesen wurde, kann nun der Logger benutzt werden. 68 require_once(OR_MODULES_DIR . "logger/require." . PHP_EXT); 69 $this->initializeLogger(); 70 71 // Sollte nur 1x pro Sitzung ausgeführt werden. Wie ermitteln wir das? 72 //if ( DEVELOPMENT ) 73 // Logger::debug( "Effective configuration:\n".YAML::YAMLDump($conf) ); 74 75 if (!empty($conf['security']['umask'])) 76 umask(octdec($conf['security']['umask'])); 77 78 if (!empty($conf['interface']['timeout'])) 79 set_time_limit(intval($conf['interface']['timeout'])); 80 81 $this->checkPostToken(); 82 83 define('FILE_SEP', $conf['interface']['file_separator']); 84 85 $this->connectToDatabase(); 86 $this->startDatabaseTransaction(); 87 88 try{ 89 90 $result = $this->callActionMethod(); 91 } 92 catch(Exception $e) 93 { 94 // In case of exception, rolling back the transaction 95 try 96 { 97 $this->rollbackDatabaseTransaction(); 98 } 99 catch(Exception $re) 100 { 101 Logger::warn("rollback failed:".$e->getMessage()); 102 } 103 104 throw $e; 105 } 106 107 $this->writeAuditLog(); 108 $this->commitDatabaseTransaction(); 109 110 if ( DEVELOPMENT ) 111 Logger::trace('Output' . "\n" . print_r($result, true)); 112 113 // Weitere Variablen anreichern. 114 $result['session'] = array('name' => session_name(), 'id' => session_id(), 'token' => token()); 115 $result['version'] = OR_VERSION; 116 $result['api'] = '2'; 117 118 119 // Yes, closing the session flushes the session data and unlocks other waiting requests. 120 // Now another request is able to be executed. 121 Session::close(); 122 123 // Ablaufzeit für den Inhalt auf aktuelle Zeit setzen. 124 header('Expires: ' . substr(date('r', time() - date('Z')), 0, -5) . 'GMT', false); 125 126 return $result; 127 } 128 129 /** 130 * Prüft, ob die Actionklasse aufgerufen werden darf. 131 * 132 * @param $do Action 133 * @throws SecurityException falls der Aufruf nicht erlaubt ist. 134 */ 135 private function checkAccess($do) 136 { 137 switch (@$do->security) { 138 case Action::SECURITY_GUEST: 139 // Ok. 140 break; 141 case Action::SECURITY_USER: 142 if (!is_object($do->currentUser)) 143 throw new SecurityException('No user logged in, but this action requires a valid user'); 144 break; 145 case Action::SECURITY_ADMIN: 146 if (!is_object($do->currentUser) || !$do->currentUser->isAdmin) 147 throw new SecurityException('This action requires administration privileges, but user ' . @$do->currentUser->name . ' is not an admin'); 148 break; 149 default: 150 } 151 152 } 153 154 private function checkPostToken() 155 { 156 global $REQ; 157 if (config('security', 'use_post_token') && $_SERVER['REQUEST_METHOD'] == 'POST' && @$REQ[REQ_PARAM_TOKEN] != token()) { 158 Logger::error('Token mismatch: Needed ' . token() . ' but got ' . @$REQ[REQ_PARAM_TOKEN] . '. Maybe an attacker?'); 159 throw new SecurityException("Token mismatch"); 160 } 161 } 162 163 /** 164 * Logger initialisieren. 165 */ 166 private function initializeLogger() 167 { 168 169 $logConfig = config('log'); 170 171 $logFile = $logConfig['file']; 172 173 // Wenn Logfile relativ angegeben wurde, dann muss dies relativ zum Root der Anwendung sein. 174 if ( !empty($logFile) && $logFile[0] != '/' ) 175 $logFile = __DIR__.'/../../'.$logFile; 176 //$logFile = __DIR__.'/../../'.$logFile; 177 178 Logger::$messageFormat = $logConfig['format']; 179 Logger::$filename = $logFile; 180 Logger::$dateFormat = $logConfig['date_format']; 181 Logger::$nsLookup = $logConfig['ns_lookup']; 182 183 $cname = 'LOGGER_LOG_' . strtoupper($logConfig['level']); 184 if (defined($cname)) 185 Logger::$level = constant($cname); 186 187 188 Logger::$messageCallback = function () { 189 190 $action = Session::get('action'); 191 if (empty($action)) 192 $action = '-'; 193 194 $user = Session::getUser(); 195 if (is_object($user)) 196 $username = $user->name; 197 else 198 $username = '-'; 199 200 return array('user' => $username, 'action' => $action); 201 }; 202 Logger::init(); 203 } 204 205 private function checkConfiguration() 206 { 207 $conf = Session::getConfig(); 208 209 // Konfiguration lesen. 210 // Wenn Konfiguration noch nicht in Session vorhanden oder die Konfiguration geändert wurde (erkennbar anhand des Datei-Datums) 211 // dann die Konfiguration neu einlesen. 212 $configLoader = new ConfigurationLoader( __DIR__.'/../../config/config.yml' ); 213 214 if (!is_array($conf) || $conf['config']['auto_reload'] && $configLoader->lastModificationTime() > $conf['config']['last_modification_time']) { 215 216 // Da die Konfiguration neu eingelesen wird, sollten wir auch die Sitzung komplett leeren. 217 if (is_array($conf) && $conf['config']['session_destroy_on_config_reload']) 218 session_unset(); 219 220 // Fest eingebaute Standard-Konfiguration laden. 221 require(OR_MODULES_DIR . 'util/config-default.php'); 222 $conf = createDefaultConfig(); 223 224 $customConfig = $configLoader->load(); 225 $conf = array_replace_recursive($conf, $customConfig); 226 227 // Sprache lesen 228 229 if ($conf['i18n']['use_http']) 230 // Die vom Browser angeforderten Sprachen ermitteln 231 $languages = Http::getLanguages(); 232 else 233 // Nur Default-Sprache erlauben 234 $languages = array(); 235 236 if (isset($_COOKIE['or_language'])) 237 $languages = array($_COOKIE['or_language']) + $languages; 238 239 // Default-Sprache hinzufuegen. 240 // Wird dann verwendet, wenn die vom Browser angeforderten Sprachen 241 // nicht vorhanden sind 242 $languages[] = $conf['i18n']['default']; 243 $available = explode(',', $conf['i18n']['available']); 244 245 foreach ($languages as $l) { 246 if (!in_array($l, $available)) 247 continue; // language is not configured as available. 248 249 $isProduction = $conf['production']; 250 $language = new \language\Language(); 251 $lang = $language->getLanguage( $l,$isProduction); 252 $conf['language'] = $lang; 253 $conf['language']['language_code'] = $l; 254 break; 255 } 256 257 258 if (!isset($conf['language'])) 259 throw new \LogicException('no language found! (languages=' . implode(',', $languages) . ')'); 260 261 // Schreibt die Konfiguration in die Sitzung. Diese wird anschliessend nicht 262 // mehr veraendert. 263 Session::setConfig($conf); 264 } 265 266 } 267 268 /** 269 * Aufruf der Action-Methode. 270 * 271 * @return array Vollständige Rückgabe aller Daten als assoziatives Array 272 */ 273 private function callActionMethod() 274 { 275 global $REQ; 276 $actionClassName = ucfirst($this->request->action) . 'Action'; 277 $actionClassNameWithNamespace = 'cms\\action\\' . $actionClassName; 278 279 if (!class_exists($actionClassNameWithNamespace)) 280 { 281 // Laden der Action-Klasse. 282 $success = include_once(__DIR__. '/action/' . $actionClassName . '.class.php'); 283 284 if ( !$success) 285 throw new LogicException("Action '$this->request->action' is not available"); 286 } 287 288 // Erzeugen der Action-Klasse 289 /* @type $do \cms\action\Action */ 290 $do = new $actionClassNameWithNamespace; 291 292 $do->request = $this->request; 293 $do->init(); 294 295 if(!defined('OR_ID')) 296 //if (isset($REQ[REQ_PARAM_ID])) 297 define('OR_ID', $this->request->id); 298 //else 299 // define('OR_ID', ''); 300 301 $this->checkAccess($do); 302 303 // POST-Request => ...Post() wird aufgerufen. 304 // GET-Request => ...View() wird aufgerufen. 305 $methodSuffix = $this->request->isAction ? 'Post' : 'View'; 306 $subactionMethodName = $this->request->method . $methodSuffix; 307 308 // Daten werden nur angezeigt, die Sitzung kann also schon geschlossen werden. 309 // Halt! In Index-Action können Benutzer-Logins gesetzt werden. 310 if ( ! $this->request->isAction && $this->request->action != 'index' ) 311 Session::close(); 312 313 Logger::debug("Dispatcher executing {$this->request->action}/{$this->request->method}/" . @$REQ[REQ_PARAM_ID].' -> '.$actionClassName.'#'.$subactionMethodName.'()'); 314 315 316 try { 317 $method = new \ReflectionMethod($do,$subactionMethodName); 318 $declaredClassName = $method->getDeclaringClass()->getShortName(); 319 $declaredActionName = strtolower(substr($declaredClassName,0,strpos($declaredClassName,'Action'))); 320 321 $method->invoke($do); // <== Executing the Action 322 } 323 catch (\ValidationException $ve) 324 { 325 $do->addValidationError( $ve->fieldName ); 326 } 327 catch (\ReflectionException $re) 328 { 329 throw new BadMethodCallException("Method '$subactionMethodName' does not exist",0,$re); 330 } 331 332 // The action is able to change its method name. 333 $this->request = $do->request; 334 $this->request->action = $declaredActionName; 335 336 $result = $do->getOutputData(); 337 338 return $result; 339 } 340 341 /** 342 * Startet die Verbindung zur Datenbank. 343 */ 344 private function connectToDatabase() 345 { 346 $firstDbContact = ! Session::getDatabaseId() || $this->request->hasRequestVar('dbid'); 347 348 if ( $this->request->hasRequestVar('dbid') ) 349 $dbid = $this->request->getRequestVar('dbid',OR_FILTER_ALPHANUM); 350 elseif ( Session::getDatabaseId() ) 351 $dbid = Session::getDatabaseId(); 352 elseif ( isset($_COOKIE['or_dbid']) ) 353 $dbid = $_COOKIE['or_dbid']; 354 else { 355 $dbids = array(); 356 357 $databases = Conf()->get('database'); 358 359 if ( !is_array($databases)) 360 throw new \RuntimeException('Corrupt configuration: Database configuration must be a list'); 361 362 foreach( $databases as $key => $dbconf ) 363 { 364 if ( !is_array($dbconf)) 365 throw new \LogicException("Corrupt configuration: Database configuration '".$key."' must be an array.'"); 366 367 $dbconf += config('database-default','defaults'); // Add Default-Values 368 369 if ( is_array($dbconf) && $dbconf['enabled'] ) // Database-Connection is enabled 370 $dbids[] = $key; 371 } 372 373 $defaultDbId = config('database-default','default-id'); 374 if ( $defaultDbId && in_array($defaultDbId,$dbids) ) 375 // Default-Datenbankverbindung ist konfiguriert und vorhanden. 376 $dbid = $defaultDbId; 377 elseif ( count($dbids) > 0) 378 // Datenbankverbindungen sind vorhanden, wir nehmen die erste. 379 $dbid = $dbids[0]; 380 else 381 // Keine Datenbankverbindung vorhanden. Fallback: 382 throw new \RuntimeException('No database configured'); 383 } 384 385 386 $dbConfig = config()->subset('database'); 387 388 if ( ! $dbConfig->has( $dbid ) ) 389 throw new \LogicException( 'unknown DB-Id: '.$dbid ); 390 391 $dbConfig = $dbConfig->subset($dbid ); 392 393 if ( ! $dbConfig->is('enabled',true ) ) 394 throw new \RuntimeException('Database connection \''.$dbid.'\' is not enabled'); 395 396 try 397 { 398 $key = $this->request->isAction?'write':'read'; 399 400 $db = new Database( $dbConfig->subset($key)->getConfig() + $dbConfig->getConfig() ); 401 $db->id = $dbid; 402 403 Session::setDatabaseId( $dbid ); 404 Session::setDatabase( $db ); 405 }catch(\Exception $e) 406 { 407 throw new OpenRatException('DATABASE_ERROR_CONNECTION',$e->getMessage() ); 408 } 409 410 411 if ( $firstDbContact ) 412 // Test, if we should update the database schema. 413 $this->updateDatabase( $dbid ); 414 } 415 416 417 418 /** 419 * Updating the database. 420 * 421 * @param $dbid integer 422 * @throws OpenRatException 423 */ 424 private function updateDatabase($dbid) 425 { 426 $dbConfig = Conf()->subset('database')->subset($dbid); 427 428 if ( ! $dbConfig->is('check_version',true)) 429 return; // Check for DB version is disabled. 430 431 $updater = new DbUpdate(); 432 433 if ( ! $updater->isUpdateRequired( db() ) ) 434 return; 435 436 437 if ( ! $dbConfig->is('auto_update',true)) 438 throw new \LogicException('DB Update required, but auto-update is disabled. '.OR_TITLE." ".OR_VERSION." needs DB-version ".DbUpdate::SUPPORTED_VERSION ); 439 440 441 try { 442 $adminDb = new Database( $dbConfig->subset('admin')->getConfig() + $dbConfig->getConfig() ); 443 $adminDb->id = $dbid; 444 } catch (\Exception $e) { 445 446 throw new OpenRatException('DATABASE_ERROR_CONNECTION', $e->getMessage()); 447 } 448 449 $updater->update($adminDb); 450 451 // Try to close the PDO connection. PDO doc: 452 // To close the connection, you need to destroy the object by ensuring that all 453 // remaining references to it are deleted—you do this by assigning NULL to the variable that holds the object. 454 // If you don't do this explicitly, PHP will automatically close the connection when your script ends. 455 $adminDb = null; 456 unset($adminDb); 457 } 458 459 460 461 462 /** 463 * Eröffnet eine Transaktion. 464 */ 465 private function startDatabaseTransaction() 466 { 467 // Verbindung zur Datenbank 468 // 469 $db = Session::getDatabase(); 470 471 if (is_object($db)) { 472 // Transactions are only needed for POST-Request 473 // GET-Request do only read from the database and have no need for transactions. 474 if ( $this->request->isAction ) 475 { 476 $db->start(); 477 478 //register_shutdown_function( function() { 479 // $this->rollbackDatabaseTransaction(); 480 //}); 481 } 482 } 483 484 } 485 486 487 private function commitDatabaseTransaction() 488 { 489 $db = Session::getDatabase(); 490 491 if (is_object($db)) 492 // Transactions were only started for POST-Request 493 if($this->request->isAction) 494 $db->commit(); 495 } 496 497 498 499 private function rollbackDatabaseTransaction() 500 { 501 $db = Session::getDatabase(); 502 503 if (is_object($db)) 504 // Transactions were only started for POST-Request 505 if($this->request->isAction) 506 $db->rollback(); 507 } 508 509 510 /** 511 * Sets the "Content-Language"-HTTP-Header with the user language. 512 */ 513 private function setContentLanguageHeader() 514 { 515 header('Content-Language: ' . Conf()->subset('language')->get('language_code') ); 516 } 517 518 519 520 private function writeAuditLog() 521 { 522 // Only write Audit Log for POST requests. 523 if ( ! $this->request->isAction ) 524 return; 525 526 $auditConfig = config()->subset('audit-log'); 527 528 if ( $auditConfig->is('enabled',false)) 529 { 530 $dir = $auditConfig->get('directory','./audit-log' ); 531 532 if ( $dir[0] != '/' ) 533 $dir = __DIR__.'/../../'.$dir; 534 535 $micro_date = microtime(); 536 $date = explode(" ",$micro_date); 537 $filename = $dir.'/'.$auditConfig->get('prefix','audit' ).'-'.date('c',$date[1]).'-'.$date[0].'.json'; 538 539 $json = new \JSON(); 540 $user = Session::getUser(); 541 542 $data = array( 543 'database' => array( 544 'id' => db()->id ), 545 'user' => array( 546 'id' => @$user->userid, 547 'name' => @$user->name ), 548 'timestamp' => date('c'), 549 'action' => $this->request->action, 550 'method' => $this->request->method, 551 'remote-ip' => $_SERVER['REMOTE_ADDR'], 552 'request-time'=> $_SERVER['REQUEST_TIME'], 553 'data' => $this->filterCredentials( $_REQUEST ) 554 ); 555 556 // Write the file. 557 if ( file_put_contents( $filename, $json->encode($data) ) === FALSE ) 558 Logger::warn('Could not write audit log to file: '.$filename); 559 else 560 Logger::debug('Audit logfile: '.$filename); 561 } 562 563 } 564 565 566 /* 567 * Filter credentials from an array. 568 */ 569 private function filterCredentials( $input ) 570 { 571 foreach( array( 'login_password','password1','password2' ) as $cr ) 572 if ( isset($input[$cr])) 573 $input[$cr] = '***'; 574 575 return $input; 576 } 577 }