openrat-cms

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

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 }