openrat-cms

OpenRat Content Management System
git clone http://git.code.weiherhei.de/openrat-cms.git
Log | Files | Refs | README

Dispatcher.class.php (19677B)


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