From 4919ca88ec9cb41479cd309954a35a4a809ea770 Mon Sep 17 00:00:00 2001 From: Denis Flaven Date: Mon, 23 Mar 2015 16:02:44 +0000 Subject: [PATCH] Modularization of the portal. The entry points for portals is now defined in XML, and thus can be altered by an extension. SVN:trunk[3509] --- application/datamodel.application.xml | 22 +++ application/loginwebpage.class.inc.php | 152 +++++++++++++----- application/portaldispatcher.class.inc.php | 63 ++++++++ core/config.class.inc.php | 6 +- core/datamodel.core.xml | 3 + .../2.x/itop-attachments/ajax.attachment.php | 2 +- dictionaries/dictionary.itop.ui.php | 2 + dictionaries/fr.dictionary.itop.ui.php | 2 + pages/ajax.render.php | 2 +- setup/applicationinstaller.class.inc.php | 14 ++ setup/compiler.class.inc.php | 67 +++++++- setup/modelfactory.class.inc.php | 34 ++++ setup/runtimeenv.class.inc.php | 13 ++ 13 files changed, 338 insertions(+), 44 deletions(-) create mode 100644 application/datamodel.application.xml create mode 100644 application/portaldispatcher.class.inc.php create mode 100644 core/datamodel.core.xml diff --git a/application/datamodel.application.xml b/application/datamodel.application.xml new file mode 100644 index 000000000..335cabd56 --- /dev/null +++ b/application/datamodel.application.xml @@ -0,0 +1,22 @@ + + + + + portal/index.php + 1.0 + + + + + + + pages/UI.php + 2.0 + + + + + + + + diff --git a/application/loginwebpage.class.inc.php b/application/loginwebpage.class.inc.php index cfa152e2d..84b4dbb2e 100644 --- a/application/loginwebpage.class.inc.php +++ b/application/loginwebpage.class.inc.php @@ -25,6 +25,7 @@ */ require_once(APPROOT."/application/nicewebpage.class.inc.php"); +require_once(APPROOT.'/application/portaldispatcher.class.inc.php'); /** * Web page used for displaying the login form */ @@ -428,6 +429,7 @@ EOF // Unset all of the session variables. unset($_SESSION['auth_user']); unset($_SESSION['login_mode']); + unset($_SESSION['profile_list']); // If it's desired to kill the session, also delete the session cookie. // Note: This will destroy the session, and not just the session data! } @@ -654,12 +656,22 @@ EOF /** * Overridable: depending on the user, head toward a dedicated portal - * @param bool $bIsAllowedToPortalUsers Whether or not the current page is considered as part of the portal + * @param string|null $sRequestedPortalId * @param int $iOnExit How to complete the call: redirect or return a code */ - protected static function ChangeLocation($bIsAllowedToPortalUsers, $iOnExit = self::EXIT_PROMPT) + protected static function ChangeLocation($sRequestedPortalId = null, $iOnExit = self::EXIT_PROMPT) { - if ( (!$bIsAllowedToPortalUsers) && (UserRights::IsPortalUser())) + $fStart = microtime(true); + $ret = call_user_func(array(self::$sHandlerClass, 'Dispatch'), $sRequestedPortalId); + if ($ret === true) + { + return self::EXIT_CODE_OK; + } + else if($ret === false) + { + throw new Exception('Nowhere to go??'); + } + else { if ($iOnExit == self::EXIT_RETURN) { @@ -668,16 +680,11 @@ EOF else { // No rights to be here, redirect to the portal - header('Location: '.utils::GetAbsoluteUrlAppRoot().'portal/index.php'); + header('Location: '.$ret); } } - else - { - return self::EXIT_CODE_OK; - } } - - + /** * Check if the user is already authentified, if yes, then performs some additional validations: * - if $bMustBeAdmin is true, then the user must be an administrator, otherwise an error is displayed @@ -688,9 +695,56 @@ EOF */ static function DoLogin($bMustBeAdmin = false, $bIsAllowedToPortalUsers = false, $iOnExit = self::EXIT_PROMPT) { - $sMessage = ''; // In case we need to return a message to the calling web page + $sRequestedPortalId = $bIsAllowedToPortalUsers ? 'legacy_portal' : 'backoffice'; + return self::DoLoginEx($sRequestedPortalId, $bMustBeAdmin, $iOnExit); + } + + /** + * Check if the user is already authentified, if yes, then performs some additional validations to redirect towards the desired "portal" + * @param string|null $sRequestedPortalId The requested "portal" interface, null for any + * @param bool $bMustBeAdmin Whether or not the user must be an admin to access the current page + * @param int iOnExit What action to take if the user is not logged on (one of the class constants EXIT_...) + */ + static function DoLoginEx($sRequestedPortalId = null, $bMustBeAdmin = false, $iOnExit = self::EXIT_PROMPT) + { $operation = utils::ReadParam('loginop', ''); - + + $sMessage = self::HandleOperations($operation); // May exit directly + + $iRet = self::Login($iOnExit); + + if ($iRet == self::EXIT_CODE_OK) + { + if ($bMustBeAdmin && !UserRights::IsAdministrator()) + { + if ($iOnExit == self::EXIT_RETURN) + { + return self::EXIT_CODE_MUSTBEADMIN; + } + else + { + require_once(APPROOT.'/setup/setuppage.class.inc.php'); + $oP = new SetupPage(Dict::S('UI:PageTitle:FatalError')); + $oP->add("

".Dict::S('UI:Login:Error:AccessAdmin')."

\n"); + $oP->p("".Dict::S('UI:LogOffMenu').""); + $oP->output(); + exit; + } + } + $iRet = call_user_func(array(self::$sHandlerClass, 'ChangeLocation'), $sRequestedPortalId, $iOnExit); + } + if ($iOnExit == self::EXIT_RETURN) + { + return $iRet; + } + else + { + return $sMessage; + } + } + protected static function HandleOperations($operation) + { + $sMessage = ''; // most of the operations never return, but some can return a message to be displayed if ($operation == 'logoff') { if (isset($_SESSION['login_mode'])) @@ -714,7 +768,7 @@ EOF $oPage->DisplayLoginForm( $sLoginMode, false /* not a failed attempt */); $oPage->output(); exit; - } + } else if ($operation == 'forgot_pwd') { $oPage = self::NewLoginWebPage(); @@ -767,36 +821,54 @@ EOF } $sMessage = Dict::S('UI:Login:PasswordChanged'); } + return $sMessage; + } + + protected static function Dispatch($sRequestedPortalId) + { + if ($sRequestedPortalId === null) return true; // allowed to any portal => return true - $iRet = self::Login($iOnExit); - - if ($iRet == self::EXIT_CODE_OK) + $aPortalsConf = PortalDispatcherData::GetData(); + $aDispatchers = array(); + foreach($aPortalsConf as $sPortalId => $aConf) { - if ($bMustBeAdmin && !UserRights::IsAdministrator()) + $sHandlerClass = $aConf['handler']; + $aDispatchers[$sPortalId] = new $sHandlerClass($sPortalId); + } + + if (array_key_exists($sRequestedPortalId, $aDispatchers) && $aDispatchers[$sRequestedPortalId]->IsUserAllowed()) + { + return true; + } + foreach($aDispatchers as $sPortalId => $oDispatcher) + { + if ($oDispatcher->IsUserAllowed()) return $oDispatcher->GetUrl(); + } + return false; // nothing matched !! + } + + public static function GetAllowedPortals() + { + $aAllowedPortals = array(); + $aPortalsConf = PortalDispatcherData::GetData(); + $aDispatchers = array(); + foreach($aPortalsConf as $sPortalId => $aConf) + { + $sHandlerClass = $aConf['handler']; + $aDispatchers[$sPortalId] = new $sHandlerClass($sPortalId); + } + + foreach($aDispatchers as $sPortalId => $oDispatcher) + { + if ($oDispatcher->IsUserAllowed()) { - if ($iOnExit == self::EXIT_RETURN) - { - return self::EXIT_CODE_MUSTBEADMIN; - } - else - { - require_once(APPROOT.'/setup/setuppage.class.inc.php'); - $oP = new SetupPage(Dict::S('UI:PageTitle:FatalError')); - $oP->add("

".Dict::S('UI:Login:Error:AccessAdmin')."

\n"); - $oP->p("".Dict::S('UI:LogOffMenu').""); - $oP->output(); - exit; - } + $aAllowedPortals[] = array( + 'id' => $sPortalId, + 'label' => $oDispatcher->GetLabel(), + 'url' => $oDispatcher->GetUrl(), + ); } - $iRet = call_user_func(array(self::$sHandlerClass, 'ChangeLocation'), $bIsAllowedToPortalUsers, $iOnExit); } - if ($iOnExit == self::EXIT_RETURN) - { - return $iRet; - } - else - { - return $sMessage; - } - } + return $aAllowedPortals; + } } // End of class diff --git a/application/portaldispatcher.class.inc.php b/application/portaldispatcher.class.inc.php new file mode 100644 index 000000000..4f80c605e --- /dev/null +++ b/application/portaldispatcher.class.inc.php @@ -0,0 +1,63 @@ +sPortalid = $sPortalId; + $this->aData = PortalDispatcherData::GetData($sPortalId); + } + + public function IsUserAllowed() + { + if (array_key_exists('profile_list', $_SESSION)) + { + $aProfiles = $_SESSION['profile_list']; + } + else + { + $oUser = UserRights::GetUserObject(); + $oSet = $oUser->Get('profile_list'); + while(($oLnkUserProfile = $oSet->Fetch()) !== null) + { + $aProfiles[] = $oLnkUserProfile->Get('profileid_friendlyname'); + } + $_SESSION['profile_list'] = $aProfiles; + } + + foreach($this->aData['deny'] as $sDeniedProfile) + { + // If one denied profile is present, it's enough => return false + if (in_array($sDeniedProfile, $aProfiles)) + { + return false; + } + } + foreach($this->aData['allow'] as $sAllowProfile) + { + // if one required profile is missing, it's enough => return false + if (!in_array($sAllowProfile, $aProfiles)) + { + return false; + } + } + return true; + } + + public function GetURL() + { + return utils::GetAbsoluteUrlAppRoot().$this->aData['url']; + } + + public function GetLabel() + { + return Dict::S('portal:'.$this->sPortalid); + } + + public function GetRank() + { + return $this->aData['rank']; + } +} \ No newline at end of file diff --git a/core/config.class.inc.php b/core/config.class.inc.php index bcf24aab9..06965f6c3 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -1730,7 +1730,7 @@ class Config $this->SetDBName($sDBName); $this->SetDBSubname($aParamValues['db_prefix']); } - + if (!is_null($sModulesDir)) { if (isset($aParamValues['selected_modules'])) @@ -1746,6 +1746,10 @@ class Config $oEmptyConfig = new Config('dummy_file', false); // Do NOT load any config file, just set the default values $aAddOns = $oEmptyConfig->GetAddOns(); $aAppModules = $oEmptyConfig->GetAppModules(); + if (file_exists(APPROOT.$sModulesDir.'/core/main.php')) + { + $aAppModules[] = $sModulesDir.'/core/main.php'; + } $aDataModels = $oEmptyConfig->GetDataModels(); $aWebServiceCategories = $oEmptyConfig->GetWebServiceCategories(); $aDictionaries = $oEmptyConfig->GetDictionaries(); diff --git a/core/datamodel.core.xml b/core/datamodel.core.xml new file mode 100644 index 000000000..3fa7cce3e --- /dev/null +++ b/core/datamodel.core.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/datamodels/2.x/itop-attachments/ajax.attachment.php b/datamodels/2.x/itop-attachments/ajax.attachment.php index cbbaf2495..5f261f9a8 100755 --- a/datamodels/2.x/itop-attachments/ajax.attachment.php +++ b/datamodels/2.x/itop-attachments/ajax.attachment.php @@ -35,7 +35,7 @@ try // require_once(APPROOT.'/application/user.preferences.class.inc.php'); require_once(APPROOT.'/application/loginwebpage.class.inc.php'); - LoginWebPage::DoLogin(false /* bMustBeAdmin */, true /* IsAllowedToPortalUsers */); // Check user rights and prompt if needed + LoginWebPage::DoLoginEx(null /* any portal */, false); $oPage = new ajax_page(""); $oPage->no_cache(); diff --git a/dictionaries/dictionary.itop.ui.php b/dictionaries/dictionary.itop.ui.php index e88e8e54f..66b859ea3 100644 --- a/dictionaries/dictionary.itop.ui.php +++ b/dictionaries/dictionary.itop.ui.php @@ -1224,5 +1224,7 @@ When associated with a trigger, each action is given an "order" number, specifyi 'ExcelExport:AutoDownload' => 'Start the download automatically when the export is ready', 'ExcelExport:PreparingExport' => 'Preparing the export...', 'ExcelExport:Statistics' => 'Statistics', + 'portal:legacy_portal' => 'End-User Portal', + 'portal:backoffice' => 'iTop Back-Office User Interface', )); ?> diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php index 66418f1a5..8808c9d86 100644 --- a/dictionaries/fr.dictionary.itop.ui.php +++ b/dictionaries/fr.dictionary.itop.ui.php @@ -1065,5 +1065,7 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé 'ExcelExport:AutoDownload' => 'Téléchargement automatique dès que le fichier est prêt', 'ExcelExport:PreparingExport' => 'Préparation de l\'export...', 'ExcelExport:Statistics' => 'Statistiques', + 'portal:legacy_portal' => 'Portail Utilisateurs', + 'portal:backoffice' => 'Console iTop', )); ?> diff --git a/pages/ajax.render.php b/pages/ajax.render.php index 60faee1a2..31aeb5141 100644 --- a/pages/ajax.render.php +++ b/pages/ajax.render.php @@ -40,7 +40,7 @@ try require_once(APPROOT.'/application/user.preferences.class.inc.php'); require_once(APPROOT.'/application/loginwebpage.class.inc.php'); - LoginWebPage::DoLogin(false /* bMustBeAdmin */, true /* IsAllowedToPortalUsers */); // Check user rights and prompt if needed + LoginWebPage::DoLoginEx(null /* any portal */, false); $oPage = new ajax_page(""); $oPage->no_cache(); diff --git a/setup/applicationinstaller.class.inc.php b/setup/applicationinstaller.class.inc.php index 17e7f07bd..30a603125 100644 --- a/setup/applicationinstaller.class.inc.php +++ b/setup/applicationinstaller.class.inc.php @@ -465,6 +465,20 @@ class ApplicationInstaller } $oFactory = new ModelFactory($aDirsToScan); + + $sDeltaFile = APPROOT.'core/datamodel.core.xml'; + if (file_exists($sDeltaFile)) + { + $oCoreModule = new MFCoreModule('core', 'Core Module', $sDeltaFile); + $oFactory->LoadModule($oCoreModule); + } + $sDeltaFile = APPROOT.'application/datamodel.application.xml'; + if (file_exists($sDeltaFile)) + { + $oApplicationModule = new MFCoreModule('application', 'Application Module', $sDeltaFile); + $oFactory->LoadModule($oApplicationModule); + } + $aModules = $oFactory->FindModules(); foreach($aModules as $foo => $oModule) diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index cc8326353..1c4be393f 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -32,12 +32,14 @@ class MFCompiler protected $aRootClasses; protected $aLog; + protected $sMainPHPCode; // Code that goes into core/main.php public function __construct($oModelFactory) { $this->oFactory = $oModelFactory; $this->aLog = array(); + $this->sMainPHPCode = '<'.'?'."php\n"; } protected function Log($sText) @@ -377,7 +379,16 @@ EOF; // $oBrandingNode = $this->oFactory->GetNodes('branding')->item(0); $this->CompileBranding($oBrandingNode, $sTempTargetDir, $sFinalTargetDir); - + + // Compile the portals + $oPortalsNode = $this->oFactory->GetNodes('/itop_design/portals')->item(0); + $this->CompilePortals($oPortalsNode, $sTempTargetDir, $sFinalTargetDir); + + // Write core/main.php + SetupUtils::builddir($sTempTargetDir.'/core'); + $sPHPFile = $sTempTargetDir.'/core/main.php'; + file_put_contents($sPHPFile, $this->sMainPHPCode); + } // DoCompile() /** @@ -1929,6 +1940,60 @@ EOF; } } } + + protected function CompilePortals($oPortalsNode, $sTempTargetDir, $sFinalTargetDir) + { + if ($oPortalsNode) + { + // Create some static PHP data in /core/main.php + $oPortals = $oPortalsNode->GetNodes('portal'); + $aPortalsConfig = array(); + foreach($oPortals as $oPortal) + { + $sPortalId = $oPortal->getAttribute('id'); + $aPortalsConfig[$sPortalId] = array(); + $aPortalsConfig[$sPortalId]['rank'] = (float)$oPortal->GetChildText('rank', 0); + $aPortalsConfig[$sPortalId]['handler'] = $oPortal->GetChildText('handler', 'PortalDispatcher'); + $aPortalsConfig[$sPortalId]['url'] = $oPortal->GetChildText('url', 'portal/index.php'); + $oAllow = $oPortal->GetOptionalElement('allow'); + $aPortalsConfig[$sPortalId]['allow'] = array(); + if ($oAllow) + { + foreach($oAllow->GetNodes('profile') as $oProfile) + { + $aPortalsConfig[$sPortalId]['allow'][] = $oProfile->getAttribute('id'); + } + } + $oDeny = $oPortal->GetOptionalElement('deny'); + $aPortalsConfig[$sPortalId]['deny'] = array(); + if ($oDeny) + { + foreach($oDeny->GetNodes('profile') as $oProfile) + { + $aPortalsConfig[$sPortalId]['deny'][] = $oProfile->getAttribute('id'); + } + } + } + + uasort($aPortalsConfig, array(get_class($this), 'SortOnRank')); + + $this->sMainPHPCode .= "class PortalDispatcherData\n"; + $this->sMainPHPCode .= "{\n"; + $this->sMainPHPCode .= "\tprotected static \$aData = ".str_replace("\n", "\n\t", var_export($aPortalsConfig, true)).";\n\n"; + $this->sMainPHPCode .= "\tpublic static function GetData(\$sPortalId = null)\n"; + $this->sMainPHPCode .= "\t{\n"; + $this->sMainPHPCode .= "\t\tif (\$sPortalId === null) return self::\$aData;\n"; + $this->sMainPHPCode .= "\t\tif (!array_key_exists(\$sPortalId, self::\$aData)) return array();\n"; + $this->sMainPHPCode .= "\t\treturn self::\$aData[\$sPortalId];\n"; + $this->sMainPHPCode .= "\t}\n"; + $this->sMainPHPCode .= "}\n"; + } + } + + public static function SortOnRank($aConf1, $aConf2) + { + return ($aConf1['rank'] < $aConf2['rank']) ? -1 : 1; + } } ?> diff --git a/setup/modelfactory.class.inc.php b/setup/modelfactory.class.inc.php index 1912d9bc8..af19d6bfa 100644 --- a/setup/modelfactory.class.inc.php +++ b/setup/modelfactory.class.inc.php @@ -171,6 +171,40 @@ class MFDeltaModule extends MFModule } } +/** + * MFDeltaModule: an optional module, made of a single file + * @package ModelFactory + */ +class MFCoreModule extends MFModule +{ + public function __construct($sName, $sLabel, $sDeltaFile) + { + $this->sId = $sName; + + $this->sName = $sName; + $this->sVersion = '1.0'; + + $this->sRootDir = ''; + $this->sLabel = $sLabel; + $this->aDataModels = array($sDeltaFile); + } + + public function GetRootDir() + { + return ''; + } + + public function GetModuleDir() + { + return ''; + } + + public function GetDictionaryFiles() + { + return array(); + } +} + /** * ModelFactory: the class that manages the in-memory representation of the XML MetaModel * @package ModelFactory diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 224cf68de..399fa4674 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -335,6 +335,19 @@ class RunTimeEnvironment // Do load the required modules // $oFactory = new ModelFactory($aDirsToCompile); + $sDeltaFile = APPROOT.'core/datamodel.core.xml'; + if (file_exists($sDeltaFile)) + { + $oCoreModule = new MFCoreModule('core', 'Core Module', $sDeltaFile); + $aRet[] = $oCoreModule; + } + $sDeltaFile = APPROOT.'application/datamodel.application.xml'; + if (file_exists($sDeltaFile)) + { + $oApplicationModule = new MFCoreModule('application', 'Application Module', $sDeltaFile); + $aRet[] = $oApplicationModule; + } + $aModules = $oFactory->FindModules(); foreach($aModules as $foo => $oModule) {