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)
{