Browse Source

Modularization of the portal. The entry points for portals is now defined in XML, and thus can be altered by an extension.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3509 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 10 years ago
parent
commit
7ef64ebeb9

+ 22 - 0
application/datamodel.application.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<itop_design>
+  <portals>
+    <portal id="legacy_portal" _delta="define">
+      <url>portal/index.php</url>
+      <rank>1.0</rank>
+      <handler/>
+      <allow>
+      </allow>
+      <deny/>
+    </portal>
+    <portal id="backoffice" _delta="define">
+      <url>pages/UI.php</url>
+      <rank>2.0</rank>
+      <handler/>
+      <allow/>
+      <deny>
+        <profile id="Portal user"/>
+      </deny>
+    </portal>
+  </portals>
+</itop_design>

+ 110 - 38
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("<h1>".Dict::S('UI:Login:Error:AccessAdmin')."</h1>\n");
+					$oP->p("<a href=\"".utils::GetAbsoluteUrlAppRoot()."pages/logoff.php\">".Dict::S('UI:LogOffMenu')."</a>");
+					$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())
-			{
-				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("<h1>".Dict::S('UI:Login:Error:AccessAdmin')."</h1>\n");	
-					$oP->p("<a href=\"".utils::GetAbsoluteUrlAppRoot()."pages/logoff.php\">".Dict::S('UI:LogOffMenu')."</a>");
-					$oP->output();
-					exit;
-				}
-			}
-			$iRet = call_user_func(array(self::$sHandlerClass, 'ChangeLocation'), $bIsAllowedToPortalUsers, $iOnExit);
+			$sHandlerClass = $aConf['handler'];
+			$aDispatchers[$sPortalId] = new $sHandlerClass($sPortalId);
 		}
-		if ($iOnExit == self::EXIT_RETURN)
+		
+		if (array_key_exists($sRequestedPortalId, $aDispatchers) && $aDispatchers[$sRequestedPortalId]->IsUserAllowed())
 		{
-			return $iRet;
+			return true;
 		}
-		else
+		foreach($aDispatchers as $sPortalId => $oDispatcher)
 		{
-			return $sMessage;
+			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())
+			{
+				$aAllowedPortals[] = array(
+					'id' => $sPortalId,
+					'label' => $oDispatcher->GetLabel(),
+					'url' => $oDispatcher->GetUrl(),
+				);
+			}
+		}
+		return $aAllowedPortals;
+	}
 } // End of class

+ 63 - 0
application/portaldispatcher.class.inc.php

@@ -0,0 +1,63 @@
+<?php
+class PortalDispatcher
+{
+	protected $sPortalid;
+	protected $aData;
+	
+	public function __construct($sPortalId)
+	{
+		$this->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'];
+	}
+}

+ 5 - 1
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();

+ 3 - 0
core/datamodel.core.xml

@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<itop_design>
+</itop_design>

+ 1 - 1
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();

+ 2 - 0
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',
 ));
 ?>

+ 2 - 0
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',
 ));
 ?>

+ 1 - 1
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();

+ 14 - 0
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)

+ 66 - 1
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 <env-xxx>/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;
+	}
 }
 
 ?>

+ 34 - 0
setup/modelfactory.class.inc.php

@@ -172,6 +172,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
  */

+ 13 - 0
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)
 		{