mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 15:34:12 +01:00
N°4762 - Designer customization of menus moved in itop-structure crashs in iTop 3.0
This commit is contained in:
@@ -333,6 +333,14 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'set_menu_compilation_algorithm' => [
|
||||
'type' => 'string',
|
||||
'description' => 'setup menu compilation algorithm version (N°4762)',
|
||||
'default' => 'v1',
|
||||
'value' => 'v1',
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'allow_target_creation' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'Displays the + button on external keys to create target objects',
|
||||
|
||||
@@ -24,14 +24,14 @@ use Combodo\iTop\DesignElement;
|
||||
require_once(APPROOT.'setup/setuputils.class.inc.php');
|
||||
require_once(APPROOT.'setup/modelfactory.class.inc.php');
|
||||
require_once(APPROOT.'core/moduledesign.class.inc.php');
|
||||
|
||||
require_once(APPROOT.'setup/parentmenunodecompiler.class.inc.php');
|
||||
|
||||
class DOMFormatException extends Exception
|
||||
{
|
||||
/**
|
||||
* Overrides the Exception default constructor to automatically add informations about the concerned node (path and
|
||||
* line number)
|
||||
*
|
||||
*
|
||||
* @param string $message
|
||||
* @param $code
|
||||
* @param $previous
|
||||
@@ -49,7 +49,7 @@ class DOMFormatException extends Exception
|
||||
|
||||
/**
|
||||
* Compiler class
|
||||
*/
|
||||
*/
|
||||
class MFCompiler
|
||||
{
|
||||
const DATA_PRECOMPILED_FOLDER = 'data'.DIRECTORY_SEPARATOR.'precompiled_styles'.DIRECTORY_SEPARATOR;
|
||||
@@ -274,7 +274,11 @@ class MFCompiler
|
||||
|
||||
try
|
||||
{
|
||||
$this->DoCompile($sTempTargetDir, $sFinalTargetDir, $oP = null, $bUseSymbolicLinks);
|
||||
if ($oConfig->Get('set_menu_compilation_algorithm') === 'v2'){
|
||||
$this->DoNewCompile($sTempTargetDir, $sFinalTargetDir, $oP = null, $bUseSymbolicLinks);
|
||||
} else {
|
||||
$this->DoCompile($sTempTargetDir, $sFinalTargetDir, $oP = null, $bUseSymbolicLinks);
|
||||
}
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
@@ -315,7 +319,7 @@ class MFCompiler
|
||||
apc_clear_cache();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Perform the actual "Compilation" of all modules
|
||||
@@ -418,7 +422,7 @@ class MFCompiler
|
||||
$sCompiledCode .= $this->CompileConstant($oConstant)."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (array_key_exists($sModuleName, $this->aSnippets))
|
||||
{
|
||||
foreach( $this->aSnippets[$sModuleName]['before'] as $aSnippet)
|
||||
@@ -581,7 +585,7 @@ EOF;
|
||||
$sCompiledCode .= $aSnippet['content']."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create (overwrite if existing) the compiled file
|
||||
//
|
||||
if (strlen($sCompiledCode) > 0)
|
||||
@@ -625,7 +629,7 @@ EOF;
|
||||
$aWebservicesFiles[] = "MetaModel::IncludeModule(MODULESROOT.'/$sRelativeDir/$sRelFileName');";
|
||||
}
|
||||
} // foreach module
|
||||
|
||||
|
||||
// Compile the dictionaries -out of the modules
|
||||
//
|
||||
$sDictDir = $sTempTargetDir.'/dictionaries';
|
||||
@@ -654,7 +658,7 @@ EOF;
|
||||
$this->sMainPHPCode .= $aSnippet['content']."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Compile the portals
|
||||
$oPortalsNode = $this->oFactory->GetNodes('/itop_design/portals')->item(0);
|
||||
$this->CompilePortals($oPortalsNode, $sTempTargetDir, $sFinalTargetDir);
|
||||
@@ -666,7 +670,7 @@ EOF;
|
||||
// Compile the XML parameters
|
||||
$oParametersNode = $this->oFactory->GetNodes('/itop_design/module_parameters')->item(0);
|
||||
$this->CompileParameters($oParametersNode, $sTempTargetDir, $sFinalTargetDir);
|
||||
|
||||
|
||||
if (array_key_exists('_core_', $this->aSnippets))
|
||||
{
|
||||
foreach( $this->aSnippets['_core_']['after'] as $aSnippet)
|
||||
@@ -700,7 +704,7 @@ EOF;
|
||||
$sCurrDate = date(DATE_ISO8601);
|
||||
// Autoload
|
||||
$sPHPFile = $sTempTargetDir.'/autoload.php';
|
||||
$sPHPFileContent =
|
||||
$sPHPFileContent =
|
||||
<<<EOF
|
||||
<?php
|
||||
//
|
||||
@@ -709,7 +713,7 @@ EOF;
|
||||
//
|
||||
EOF
|
||||
;
|
||||
|
||||
|
||||
$sPHPFileContent .= "\nMetaModel::IncludeModule(MODULESROOT.'/core/main.php');\n";
|
||||
$sPHPFileContent .= implode("\n", $aDataModelFiles);
|
||||
$sPHPFileContent .= implode("\n", $aWebservicesFiles);
|
||||
@@ -717,14 +721,491 @@ EOF
|
||||
$sModulesInfo = str_replace("'".$sRelFinalTargetDir."/", "\$sCurrEnv.'/", $sModulesInfo);
|
||||
$sPHPFileContent .= "\nfunction GetModulesInfo()\n{\n\$sCurrEnv = 'env-'.utils::GetCurrentEnvironment();\nreturn ".$sModulesInfo.";\n}\n";
|
||||
file_put_contents($sPHPFile, $sPHPFileContent);
|
||||
|
||||
|
||||
} // DoCompile()
|
||||
|
||||
/**
|
||||
* @since 3.0.x N°4762
|
||||
* Perform the enhanced "Compilation" of all modules
|
||||
* @param string $sTempTargetDir
|
||||
* @param string $sFinalTargetDir
|
||||
* @param Page $oP
|
||||
* @param bool $bUseSymbolicLinks
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function DoNewCompile($sTempTargetDir, $sFinalTargetDir, $oP = null, $bUseSymbolicLinks = false)
|
||||
{
|
||||
$aAllClasses = []; // flat list of classes
|
||||
$aModulesInfo = []; // Hash array of module_name => array('version' => string, 'root_dir' => string)
|
||||
|
||||
// Determine the target modules for the MENUS
|
||||
//
|
||||
$aMenuNodes = [];
|
||||
$aMenusByModule = [];
|
||||
foreach ($this->oFactory->GetNodes('menus/menu') as $oMenuNode)
|
||||
{
|
||||
$sMenuId = $oMenuNode->getAttribute('id');
|
||||
$aMenuNodes[$sMenuId] = $oMenuNode;
|
||||
|
||||
$sModuleMenu = $oMenuNode->getAttribute('_created_in');
|
||||
$aMenusByModule[$sModuleMenu][] = $sMenuId;
|
||||
}
|
||||
|
||||
// Determine the target module (exactly one!) for USER RIGHTS
|
||||
// This used to be based solely on the module which created the user_rights node first
|
||||
// Unfortunately, our sample extension was delivered with the xml structure, resulting in the new module to be the recipient of the compilation
|
||||
// Then model.itop-profiles-itil would not exist... resulting in an error after the compilation (and the actual product of the compiler would never be included
|
||||
// The bullet proof implementation would be to compile in a separate directory as it has been done with the dictionaries... that's another story
|
||||
$aModules = $this->oFactory->GetLoadedModules();
|
||||
$sUserRightsModule = '';
|
||||
foreach ($aModules as $foo => $oModule) {
|
||||
if ($oModule->GetName() == 'itop-profiles-itil') {
|
||||
$sUserRightsModule = 'itop-profiles-itil';
|
||||
break;
|
||||
}
|
||||
}
|
||||
$oUserRightsNode = $this->oFactory->GetNodes('user_rights')->item(0);
|
||||
if ($oUserRightsNode && ($sUserRightsModule == '')) {
|
||||
// Legacy algorithm (itop <= 2.0.3)
|
||||
$sUserRightsModule = $oUserRightsNode->getAttribute('_created_in');
|
||||
}
|
||||
$this->Log("User Rights module found: '$sUserRightsModule'");
|
||||
|
||||
// List root classes
|
||||
//
|
||||
$this->aRootClasses = [];
|
||||
foreach ($this->oFactory->ListRootClasses() as $oClass) {
|
||||
$this->Log("Root class (with child classes): ".$oClass->getAttribute('id'));
|
||||
$this->aRootClasses[$oClass->getAttribute('id')] = $oClass;
|
||||
}
|
||||
|
||||
$this->LoadSnippets();
|
||||
|
||||
// Compile, module by module
|
||||
//
|
||||
$aModules = $this->oFactory->GetLoadedModules();
|
||||
$aDataModelFiles = [];
|
||||
$aWebservicesFiles = [];
|
||||
$iStart = strlen(realpath(APPROOT));
|
||||
$sRelFinalTargetDir = substr($sFinalTargetDir, strlen(APPROOT));
|
||||
|
||||
$this->WriteStaticOnlyHtaccess($sTempTargetDir);
|
||||
$this->WriteStaticOnlyWebConfig($sTempTargetDir);
|
||||
|
||||
static::SetUseSymbolicLinksFlag($bUseSymbolicLinks);
|
||||
|
||||
$aParentModuleRootDirs = [];
|
||||
$aParentMenuNodes = [];
|
||||
foreach ($aModules as $foo => $oModule) {
|
||||
$sModuleName = $oModule->GetName();
|
||||
$sModuleVersion = $oModule->GetVersion();
|
||||
|
||||
$sModuleRootDir = $oModule->GetRootDir();
|
||||
if ($sModuleRootDir != '') {
|
||||
$sModuleRootDir = realpath($sModuleRootDir);
|
||||
$sRelativeDir = basename($sModuleRootDir);
|
||||
if ($bUseSymbolicLinks) {
|
||||
$sRealRelativeDir = substr($sModuleRootDir, $iStart);
|
||||
} else {
|
||||
$sRealRelativeDir = $sRelFinalTargetDir.'/'.$sRelativeDir;
|
||||
}
|
||||
|
||||
// Push the other module files
|
||||
SetupUtils::copydir($sModuleRootDir, $sTempTargetDir.'/'.$sRelativeDir, $bUseSymbolicLinks);
|
||||
} else {
|
||||
$sRelativeDir = $sModuleName;
|
||||
$sRealRelativeDir = $sModuleName;
|
||||
}
|
||||
$aModulesInfo[$sModuleName] = array('root_dir' => $sRealRelativeDir, 'version' => $sModuleVersion);
|
||||
|
||||
$sCompiledCode = '';
|
||||
|
||||
$oConstants = $this->oFactory->ListConstants($sModuleName);
|
||||
if ($oConstants->length > 0)
|
||||
{
|
||||
foreach($oConstants as $oConstant)
|
||||
{
|
||||
$sCompiledCode .= $this->CompileConstant($oConstant)."\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists($sModuleName, $this->aSnippets))
|
||||
{
|
||||
foreach( $this->aSnippets[$sModuleName]['before'] as $aSnippet)
|
||||
{
|
||||
$sCompiledCode .= "\n";
|
||||
$sCompiledCode .= "/**\n";
|
||||
$sCompiledCode .= " * Snippet: {$aSnippet['snippet_id']}\n";
|
||||
$sCompiledCode .= " */\n";
|
||||
$sCompiledCode .= $aSnippet['content']."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$oClasses = $this->oFactory->ListClasses($sModuleName);
|
||||
$iClassCount = $oClasses->length;
|
||||
if ($iClassCount == 0)
|
||||
{
|
||||
$this->Log("Found module without classes declared: $sModuleName");
|
||||
}
|
||||
else
|
||||
{
|
||||
/** @var \MFElement $oClass */
|
||||
foreach($oClasses as $oClass)
|
||||
{
|
||||
$sClass = $oClass->getAttribute("id");
|
||||
$aAllClasses[] = $sClass;
|
||||
try
|
||||
{
|
||||
$sCompiledCode .= $this->CompileClass($oClass, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir);
|
||||
}
|
||||
catch (DOMFormatException $e)
|
||||
{
|
||||
$sMessage = "Failed to process class '$sClass', ";
|
||||
if (!empty($sModuleRootDir)) {
|
||||
$sMessage .= "from '$sModuleRootDir': ";
|
||||
}
|
||||
$sMessage .= $e->getMessage();
|
||||
throw new Exception($sMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!array_key_exists($sModuleName, $aMenusByModule))
|
||||
{
|
||||
$this->Log("Found module without menus declared: $sModuleName");
|
||||
}
|
||||
else
|
||||
{
|
||||
$sMenuCreationClass = 'MenuCreation_'.preg_replace('/[^A-Za-z0-9_]/', '_', $sModuleName);
|
||||
$sCompiledCode .=
|
||||
<<<EOF
|
||||
|
||||
//
|
||||
// Menus
|
||||
//
|
||||
class $sMenuCreationClass extends ModuleHandlerAPI
|
||||
{
|
||||
public static function OnMenuCreation()
|
||||
{
|
||||
global \$__comp_menus__; // ensure that the global variable is indeed global !
|
||||
|
||||
EOF;
|
||||
// Preliminary: determine parent menus not defined within the current module
|
||||
$aMenusToLoad = array();
|
||||
foreach($aMenusByModule[$sModuleName] as $sMenuId)
|
||||
{
|
||||
$oMenuNode = $aMenuNodes[$sMenuId];
|
||||
if ($sParent = $oMenuNode->GetChildText('parent', null))
|
||||
{
|
||||
$aMenusToLoad[] = $sParent;
|
||||
if (!array_key_exists($sParent, $aParentModuleRootDirs)){
|
||||
$aParentModuleRootDirs[$sParent] = $sModuleRootDir;
|
||||
}
|
||||
}
|
||||
// Note: the order matters: the parents must be defined BEFORE
|
||||
$aMenusToLoad[] = $sMenuId;
|
||||
}
|
||||
$aMenusToLoad = array_unique($aMenusToLoad);
|
||||
$aMenuLinesForAll = [];
|
||||
$aMenuLinesForAdmins = [];
|
||||
$aAdminMenus = [];
|
||||
foreach($aMenusToLoad as $sMenuId)
|
||||
{
|
||||
$oMenuNode = $aMenuNodes[$sMenuId];
|
||||
if (is_null($oMenuNode))
|
||||
{
|
||||
throw new Exception("Module '{$oModule->GetId()}' (location : '$sModuleRootDir') contains an unknown menuId : '$sMenuId'");
|
||||
}
|
||||
if ($oMenuNode->getAttribute("xsi:type") == 'MenuGroup' || array_key_exists($sMenuId, $aParentModuleRootDirs))
|
||||
{
|
||||
$aParentMenuNodes[$sMenuId] = $oMenuNode;
|
||||
$sParent = $oMenuNode->GetChildText('parent', null);
|
||||
if (($oMenuNode->GetChildText('enable_admin_only') == '1') || isset($aAdminMenus[$sParent]))
|
||||
{
|
||||
$aAdminMenus[$sMenuId] = true;
|
||||
}
|
||||
continue;
|
||||
|
||||
// Note: this algorithm is wrong
|
||||
// 1 - the module may appear empty in the current module, while children are defined in other modules
|
||||
// 2 - check recursively that child nodes are not empty themselves
|
||||
// Future algorithm:
|
||||
// a- browse the modules and build the menu tree
|
||||
// b- browse the tree and blacklist empty menus
|
||||
// c- before compiling, discard if blacklisted
|
||||
}
|
||||
try
|
||||
{
|
||||
$aMenuLines = $this->CompileMenu($oMenuNode, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
}
|
||||
catch (DOMFormatException $e)
|
||||
{
|
||||
throw new Exception("Failed to process menu '$sMenuId', from '$sModuleRootDir': ".$e->getMessage());
|
||||
}
|
||||
$sParent = $oMenuNode->GetChildText('parent', null);
|
||||
if (($oMenuNode->GetChildText('enable_admin_only') == '1') || isset($aAdminMenus[$sParent]))
|
||||
{
|
||||
$aMenuLinesForAdmins = array_merge($aMenuLinesForAdmins, $aMenuLines);
|
||||
$aAdminMenus[$oMenuNode->getAttribute("id")] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
$aMenuLinesForAll = array_merge($aMenuLinesForAll, $aMenuLines);
|
||||
}
|
||||
}
|
||||
$sIndent = "\t\t";
|
||||
foreach ($aMenuLinesForAll as $sPHPLine)
|
||||
{
|
||||
$sCompiledCode .= $sIndent.$sPHPLine."\n";
|
||||
}
|
||||
if (count($aMenuLinesForAdmins) > 0)
|
||||
{
|
||||
$sCompiledCode .= $sIndent."if (UserRights::IsAdministrator())\n";
|
||||
$sCompiledCode .= $sIndent."{\n";
|
||||
foreach ($aMenuLinesForAdmins as $sPHPLine)
|
||||
{
|
||||
$sCompiledCode .= $sIndent."\t".$sPHPLine."\n";
|
||||
}
|
||||
$sCompiledCode .= $sIndent."}\n";
|
||||
}
|
||||
$sCompiledCode .=
|
||||
<<<EOF
|
||||
}
|
||||
} // class $sMenuCreationClass
|
||||
|
||||
EOF;
|
||||
}
|
||||
|
||||
// User rights
|
||||
//
|
||||
if ($sModuleName == $sUserRightsModule)
|
||||
{
|
||||
$sCompiledCode .= $this->CompileUserRights($oUserRightsNode);
|
||||
}
|
||||
|
||||
if (array_key_exists($sModuleName, $this->aSnippets))
|
||||
{
|
||||
foreach( $this->aSnippets[$sModuleName]['after'] as $aSnippet)
|
||||
{
|
||||
$sCompiledCode .= "\n";
|
||||
$sCompiledCode .= "/**\n";
|
||||
$sCompiledCode .= " * Snippet: {$aSnippet['snippet_id']}\n";
|
||||
$sCompiledCode .= " */\n";
|
||||
$sCompiledCode .= $aSnippet['content']."\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Create (overwrite if existing) the compiled file
|
||||
//
|
||||
if (strlen($sCompiledCode) > 0)
|
||||
{
|
||||
// We have compiled something: write the code somewhere
|
||||
//
|
||||
if (strlen($sModuleRootDir) > 0)
|
||||
{
|
||||
// Write the code into the given module as model.<module>.php
|
||||
//
|
||||
$sResultFile = $sTempTargetDir.'/'.$sRelativeDir.'/model.'.$sModuleName.'.php';
|
||||
$this->WritePHPFile($sResultFile, $sModuleName, $sModuleVersion, $sCompiledCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write the code into core/main.php
|
||||
//
|
||||
$this->sMainPHPCode .=
|
||||
<<<EOF
|
||||
/**
|
||||
* Data model from the delta file
|
||||
*/
|
||||
|
||||
EOF;
|
||||
$this->sMainPHPCode .= $sCompiledCode;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->Log("Compilation of module $sModuleName in version $sModuleVersion produced not code at all. No file written.");
|
||||
}
|
||||
|
||||
// files to include (PHP datamodels)
|
||||
foreach($oModule->GetFilesToInclude('business') as $sRelFileName)
|
||||
{
|
||||
$aDataModelFiles[] = "MetaModel::IncludeModule(MODULESROOT.'/$sRelativeDir/$sRelFileName');";
|
||||
}
|
||||
// files to include (PHP webservices providers)
|
||||
foreach($oModule->GetFilesToInclude('webservices') as $sRelFileName)
|
||||
{
|
||||
$aWebservicesFiles[] = "MetaModel::IncludeModule(MODULESROOT.'/$sRelativeDir/$sRelFileName');";
|
||||
}
|
||||
} // foreach module
|
||||
|
||||
// Compile the dictionaries -out of the modules
|
||||
//
|
||||
$sDictDir = $sTempTargetDir.'/dictionaries';
|
||||
if (!is_dir($sDictDir))
|
||||
{
|
||||
$this->Log("Creating directory $sDictDir");
|
||||
mkdir($sDictDir, 0777, true);
|
||||
}
|
||||
|
||||
$oDictionaries = $this->oFactory->GetNodes('dictionaries/dictionary');
|
||||
$this->CompileDictionaries($oDictionaries, $sTempTargetDir, $sFinalTargetDir);
|
||||
|
||||
// Compile the branding
|
||||
//
|
||||
$oBrandingNode = $this->oFactory->GetNodes('branding')->item(0);
|
||||
$this->CompileBranding($oBrandingNode, $sTempTargetDir, $sFinalTargetDir);
|
||||
|
||||
if (array_key_exists('_core_', $this->aSnippets))
|
||||
{
|
||||
foreach( $this->aSnippets['_core_']['before'] as $aSnippet)
|
||||
{
|
||||
$this->sMainPHPCode .= "\n";
|
||||
$this->sMainPHPCode .= "/**\n";
|
||||
$this->sMainPHPCode .= " * Snippet: {$aSnippet['snippet_id']}\n";
|
||||
$this->sMainPHPCode .= " */\n";
|
||||
$this->sMainPHPCode .= $aSnippet['content']."\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Compile the portals
|
||||
$oPortalsNode = $this->oFactory->GetNodes('/itop_design/portals')->item(0);
|
||||
$this->CompilePortals($oPortalsNode, $sTempTargetDir, $sFinalTargetDir);
|
||||
|
||||
// Create module design XML files
|
||||
$oModuleDesignsNode = $this->oFactory->GetNodes('/itop_design/module_designs')->item(0);
|
||||
$this->CompileModuleDesigns($oModuleDesignsNode, $sTempTargetDir, $sFinalTargetDir);
|
||||
|
||||
// Compile the XML parameters
|
||||
$oParametersNode = $this->oFactory->GetNodes('/itop_design/module_parameters')->item(0);
|
||||
$this->CompileParameters($oParametersNode, $sTempTargetDir, $sFinalTargetDir);
|
||||
|
||||
if (array_key_exists('_core_', $this->aSnippets))
|
||||
{
|
||||
foreach( $this->aSnippets['_core_']['after'] as $aSnippet)
|
||||
{
|
||||
$this->sMainPHPCode .= "\n";
|
||||
$this->sMainPHPCode .= "/**\n";
|
||||
$this->sMainPHPCode .= " * Snippet: {$aSnippet['snippet_id']}\n";
|
||||
$this->sMainPHPCode .= " */\n";
|
||||
$this->sMainPHPCode .= $aSnippet['content']."\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (count($this->aRelations) > 0)
|
||||
{
|
||||
$this->sMainPHPCode .= "\n";
|
||||
$this->sMainPHPCode .= "/**\n";
|
||||
$this->sMainPHPCode .= " * Relations\n";
|
||||
$this->sMainPHPCode .= " */\n";
|
||||
foreach($this->aRelations as $sRelationCode => $aData)
|
||||
{
|
||||
$sRelCodeSafe = addslashes($sRelationCode);
|
||||
$this->sMainPHPCode .= "MetaModel::RegisterRelation('$sRelCodeSafe');\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Write core/main.php
|
||||
SetupUtils::builddir($sTempTargetDir.'/core');
|
||||
$sPHPFile = $sTempTargetDir.'/core/main.php';
|
||||
file_put_contents($sPHPFile, $this->sMainPHPCode);
|
||||
|
||||
$this->GenerateMenuNodePhpCode($aParentModuleRootDirs, $aParentMenuNodes, $aAdminMenus, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
|
||||
$sCurrDate = date(DATE_ISO8601);
|
||||
// Autoload
|
||||
$sPHPFile = $sTempTargetDir.'/autoload.php';
|
||||
$sPHPFileContent =
|
||||
<<<EOF
|
||||
<?php
|
||||
//
|
||||
// File generated on $sCurrDate
|
||||
// Please do not edit manually
|
||||
//
|
||||
EOF
|
||||
;
|
||||
|
||||
$sPHPFileContent .= "\nMetaModel::IncludeModule(MODULESROOT.'/core/parent_menus.php');\n";
|
||||
$sPHPFileContent .= "MetaModel::IncludeModule(MODULESROOT.'/core/main.php');\n";
|
||||
|
||||
$sPHPFileContent .= implode("\n", $aDataModelFiles);
|
||||
$sPHPFileContent .= implode("\n", $aWebservicesFiles);
|
||||
$sModulesInfo = var_export($aModulesInfo, true);
|
||||
$sModulesInfo = str_replace("'".$sRelFinalTargetDir."/", "\$sCurrEnv.'/", $sModulesInfo);
|
||||
$sPHPFileContent .= "\nfunction GetModulesInfo()\n{\n\$sCurrEnv = 'env-'.utils::GetCurrentEnvironment();\nreturn ".$sModulesInfo.";\n}\n";
|
||||
file_put_contents($sPHPFile, $sPHPFileContent);
|
||||
|
||||
} // DoNewCompile()
|
||||
|
||||
/**
|
||||
* @since 3.0.x N°4762
|
||||
*/
|
||||
private function GenerateMenuNodePhpCode(array $aParentModuleRootDirs, array $aParentMenuNodes, array $aAdminMenus,
|
||||
string $sTempTargetDir, string $sFinalTargetDir, string $sRelativeDir, ?Page $oP = null) : void
|
||||
{
|
||||
$oCompileParentMenuNode = new ParentMenuNodeCompiler($this, $aParentModuleRootDirs, $aParentMenuNodes, $aAdminMenus,
|
||||
$sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
|
||||
//Handle menu nodes
|
||||
foreach ($aParentModuleRootDirs as $sMenuId => $sModuleRootDir) {
|
||||
$oCompileParentMenuNode->CompileParentMenuNode($sMenuId);
|
||||
}
|
||||
|
||||
$aMenuLinesForAdmins = $oCompileParentMenuNode->GetMenuLinesForAdmins();
|
||||
$aMenuLinesForAll = $oCompileParentMenuNode->GetMenuLinesForAll();
|
||||
|
||||
$sCurrDate = date(DATE_ISO8601);
|
||||
$sCompiledCode =
|
||||
<<<PHP
|
||||
<?php
|
||||
//
|
||||
// File generated on $sCurrDate
|
||||
// Please do not edit manually
|
||||
//
|
||||
//
|
||||
// Menus
|
||||
//
|
||||
class ParentMenusCreation extends ModuleHandlerAPI
|
||||
{
|
||||
public static function OnMenuCreation()
|
||||
{
|
||||
global \$__comp_menus__; // ensure that the global variable is indeed global !
|
||||
|
||||
PHP;
|
||||
|
||||
$sIndent = "\t\t";
|
||||
foreach ($aMenuLinesForAll as $sPHPLine)
|
||||
{
|
||||
$sCompiledCode .= $sIndent.$sPHPLine."\n";
|
||||
}
|
||||
if (count($aMenuLinesForAdmins) > 0)
|
||||
{
|
||||
$sCompiledCode .= $sIndent."if (UserRights::IsAdministrator())\n";
|
||||
$sCompiledCode .= $sIndent."{\n";
|
||||
foreach ($aMenuLinesForAdmins as $sPHPLine)
|
||||
{
|
||||
$sCompiledCode .= $sIndent."\t".$sPHPLine."\n";
|
||||
}
|
||||
$sCompiledCode .= $sIndent."}\n";
|
||||
}
|
||||
|
||||
$sCompiledCode .= <<<PHP
|
||||
|
||||
}
|
||||
} // class ParentMenusCreation
|
||||
PHP;
|
||||
|
||||
|
||||
$sPHPFile = $sTempTargetDir.'/core/parent_menus.php';
|
||||
file_put_contents($sPHPFile, $sCompiledCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to form a valid ZList from the array built by GetNodeAsArrayOfItems()
|
||||
*
|
||||
* @param array $aItems
|
||||
*/
|
||||
*/
|
||||
protected function ArrayOfItemsToZList(&$aItems)
|
||||
{
|
||||
// Note: $aItems can be null in some cases so we have to protect it otherwise a PHP warning will be thrown during the foreach
|
||||
@@ -756,7 +1237,7 @@ EOF
|
||||
* Helper to format the flags for an attribute, in a given state
|
||||
* @param object $oAttNode DOM node containing the information to build the flags
|
||||
* Returns string PHP flags, based on the OPT_ATT_ constants, or empty (meaning 0, can be omitted)
|
||||
*/
|
||||
*/
|
||||
protected function FlagsToPHP($oAttNode)
|
||||
{
|
||||
static $aNodeAttributeToFlag = array(
|
||||
@@ -766,7 +1247,7 @@ EOF
|
||||
'must_change' => 'OPT_ATT_MUSTCHANGE',
|
||||
'hidden' => 'OPT_ATT_HIDDEN',
|
||||
);
|
||||
|
||||
|
||||
$aFlags = array();
|
||||
foreach ($aNodeAttributeToFlag as $sNodeAttribute => $sFlag)
|
||||
{
|
||||
@@ -778,7 +1259,7 @@ EOF
|
||||
}
|
||||
if (empty($aFlags))
|
||||
{
|
||||
$aFlags[] = 'OPT_ATT_NORMAL'; // When no flag is defined, reset the state to "normal"
|
||||
$aFlags[] = 'OPT_ATT_NORMAL'; // When no flag is defined, reset the state to "normal"
|
||||
}
|
||||
$sRes = implode(' | ', $aFlags);
|
||||
return $sRes;
|
||||
@@ -800,7 +1281,7 @@ EOF
|
||||
'details' => 'LINKSET_TRACKING_DETAILS',
|
||||
'all' => 'LINKSET_TRACKING_ALL',
|
||||
);
|
||||
|
||||
|
||||
static $aXmlToPHP_Others = array(
|
||||
'none' => 'ATTRIBUTE_TRACKING_NONE',
|
||||
'all' => 'ATTRIBUTE_TRACKING_ALL',
|
||||
@@ -841,7 +1322,7 @@ EOF
|
||||
'in_place' => 'LINKSET_EDITMODE_INPLACE',
|
||||
'add_remove' => 'LINKSET_EDITMODE_ADDREMOVE',
|
||||
);
|
||||
|
||||
|
||||
if (!array_key_exists($sEditMode, $aXmlToPHP))
|
||||
{
|
||||
throw new DOMFormatException("Edit mode: unknown value '$sEditMode'");
|
||||
@@ -849,10 +1330,10 @@ EOF
|
||||
return $aXmlToPHP[$sEditMode];
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Format a path (file or url) as an absolute path or relative to the module or the app
|
||||
*/
|
||||
*/
|
||||
protected function PathToPHP($sPath, $sModuleRelativeDir, $bIsUrl = false)
|
||||
{
|
||||
if ($sPath == '')
|
||||
@@ -945,7 +1426,7 @@ EOF
|
||||
else
|
||||
{
|
||||
throw new DOMFormatException("missing (or empty) mandatory tag '$sTag' under the tag '".$oNode->nodeName."'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1049,7 +1530,7 @@ EOF
|
||||
|
||||
/**
|
||||
* Adds quotes and escape characters
|
||||
*/
|
||||
*/
|
||||
protected function QuoteForPHP($sStr, $bSimpleQuotes = false)
|
||||
{
|
||||
if ($bSimpleQuotes)
|
||||
@@ -1084,7 +1565,7 @@ EOF
|
||||
$sScalar = (string)(int)$sText;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'float':
|
||||
if (is_null($sText))
|
||||
{
|
||||
@@ -1096,7 +1577,7 @@ EOF
|
||||
$sScalar = (string)(float)$sText;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'bool':
|
||||
if (is_null($sText))
|
||||
{
|
||||
@@ -1328,7 +1809,7 @@ EOF
|
||||
// $oField
|
||||
$sAttCode = $oField->getAttribute('id');
|
||||
$sAttType = $oField->getAttribute('xsi:type');
|
||||
|
||||
|
||||
$aDependencies = array();
|
||||
$oDependencies = $oField->GetOptionalElement('dependencies');
|
||||
if (!is_null($oDependencies))
|
||||
@@ -1340,9 +1821,9 @@ EOF
|
||||
}
|
||||
}
|
||||
$sDependencies = 'array('.implode(', ', $aDependencies).')';
|
||||
|
||||
|
||||
$aParameters = array();
|
||||
|
||||
|
||||
if ($sAttType == 'AttributeLinkedSetIndirect')
|
||||
{
|
||||
$aParameters['linked_class'] = $this->GetMandatoryPropString($oField, 'linked_class');
|
||||
@@ -2416,7 +2897,7 @@ CSS;
|
||||
* @return array
|
||||
* @throws \DOMFormatException
|
||||
*/
|
||||
protected function CompileMenu($oMenu, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir, $oP)
|
||||
public function CompileMenu($oMenu, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir, $oP)
|
||||
{
|
||||
$this->CompileFiles($oMenu, $sTempTargetDir.'/'.$sModuleRelativeDir, $sFinalTargetDir.'/'.$sModuleRelativeDir, $sModuleRelativeDir);
|
||||
|
||||
@@ -2512,11 +2993,11 @@ CSS;
|
||||
case '1':
|
||||
$sSearchFormOpen = 'true';
|
||||
break;
|
||||
|
||||
|
||||
case '0':
|
||||
$sSearchFormOpen = 'false';
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
$sSearchFormOpen = 'true';
|
||||
}
|
||||
@@ -2605,7 +3086,7 @@ CSS;
|
||||
foreach($this->oFactory->ListFields($oClass) as $oField)
|
||||
{
|
||||
$sAttType = $oField->getAttribute('xsi:type');
|
||||
|
||||
|
||||
if (($sAttType == 'AttributeExternalKey') || ($sAttType == 'AttributeHierarchicalKey'))
|
||||
{
|
||||
$sOnTargetDel = $oField->GetChildText('on_target_delete');
|
||||
@@ -2631,7 +3112,7 @@ CSS;
|
||||
$oClasses = $oGroup->GetUniqueElement('classes');
|
||||
foreach($oClasses->getElementsByTagName('class') as $oClass)
|
||||
{
|
||||
|
||||
|
||||
$sClass = $oClass->getAttribute("id");
|
||||
$aClasses[] = $sClass;
|
||||
|
||||
@@ -2649,7 +3130,7 @@ CSS;
|
||||
$aProfiles[1] = array(
|
||||
'name' => 'Administrator',
|
||||
'description' => 'Has the rights on everything (bypassing any control)',
|
||||
);
|
||||
);
|
||||
|
||||
$aGrants = array();
|
||||
$oProfiles = $oUserRightsNode->GetUniqueElement('profiles');
|
||||
@@ -2681,7 +3162,7 @@ CSS;
|
||||
}
|
||||
$sGrant = $oAction->GetText();
|
||||
$bGrant = ($sGrant == 'allow');
|
||||
|
||||
|
||||
if ($sGroupId == '*')
|
||||
{
|
||||
$aGrantClasses = array('*');
|
||||
@@ -2920,7 +3401,7 @@ Dict::SetLanguagesList(
|
||||
$sLanguagesDump
|
||||
);
|
||||
EOF;
|
||||
|
||||
|
||||
file_put_contents($sLanguagesFile, $sLanguagesFileContent);
|
||||
}
|
||||
|
||||
@@ -2957,7 +3438,7 @@ EOF;
|
||||
{
|
||||
throw new DOMFormatException('Could not find the file with ref '.$sFileId);
|
||||
}
|
||||
|
||||
|
||||
$sName = $oNodes->item(0)->GetChildText('name');
|
||||
$sData = base64_decode($oNodes->item(0)->GetChildText('data'));
|
||||
$aPathInfo = pathinfo($sName);
|
||||
@@ -2971,7 +3452,7 @@ EOF;
|
||||
}
|
||||
$oParentNode = $oFileRef->parentNode;
|
||||
$oParentNode->removeChild($oFileRef);
|
||||
|
||||
|
||||
$oTextNode = $oParentNode->ownerDocument->createTextNode($sRelativePath.'/images/'.$sFile);
|
||||
$oParentNode->appendChild($oTextNode);
|
||||
}
|
||||
@@ -3284,8 +3765,8 @@ EOF;
|
||||
{
|
||||
SetupUtils::rrmdir($sTempTargetDir.'/branding/images');
|
||||
}
|
||||
|
||||
// Compile themes
|
||||
|
||||
// Compile themes
|
||||
$this->CompileThemes($oBrandingNode, $sTempTargetDir);
|
||||
}
|
||||
}
|
||||
@@ -3326,11 +3807,11 @@ EOF;
|
||||
{
|
||||
$aPortalsConfig[$sPortalId]['deny'][] = $oProfile->getAttribute('id');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
uasort($aPortalsConfig, array(get_class($this), 'SortOnRank'));
|
||||
|
||||
|
||||
$this->sMainPHPCode .= "\n";
|
||||
$this->sMainPHPCode .= "/**\n";
|
||||
$this->sMainPHPCode .= " * Portal(s) definition(s) extracted from the XML definition at compile time\n";
|
||||
@@ -3373,7 +3854,7 @@ EOF;
|
||||
$oParamsReader = new MFParameters($oParams);
|
||||
$aParametersConfig[$sModuleId] = $oParamsReader->GetAll();
|
||||
}
|
||||
|
||||
|
||||
$this->sMainPHPCode .= "\n";
|
||||
$this->sMainPHPCode .= "/**\n";
|
||||
$this->sMainPHPCode .= " * Modules parameters extracted from the XML definition at compile time\n";
|
||||
|
||||
150
setup/parentmenunodecompiler.class.inc.php
Normal file
150
setup/parentmenunodecompiler.class.inc.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2013-2021 Combodo SARL
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @since 3.0.x N°4762
|
||||
*/
|
||||
class ParentMenuNodeCompiler
|
||||
{
|
||||
/**
|
||||
* @var MFCompiler
|
||||
*/
|
||||
|
||||
private $oMFCompiler;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $aParentModuleRootDirs;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $aParentMenuNodes;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $aAdminMenus;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sTempTargetDir;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sFinalTargetDir;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sRelativeDir;
|
||||
|
||||
/**
|
||||
* @var Page|null
|
||||
*/
|
||||
private $oP;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $aMenuLinesForAdmins = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $aMenuLinesForAll = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $aMenuProcessStatus = [];
|
||||
|
||||
const COMPILED = 1;
|
||||
const COMPILING = 2;
|
||||
|
||||
public function __construct(MFCompiler $oMFCompiler, array $aParentModuleRootDirs, array $aParentMenuNodes, array $aAdminMenus,
|
||||
string $sTempTargetDir, string $sFinalTargetDir, string $sRelativeDir, ?Page $oP = null) {
|
||||
$this->oMFCompiler = $oMFCompiler;
|
||||
$this->aParentModuleRootDirs = $aParentModuleRootDirs;
|
||||
$this->aParentMenuNodes = $aParentMenuNodes;
|
||||
$this->aAdminMenus = $aAdminMenus;
|
||||
$this->sTempTargetDir = $sTempTargetDir;
|
||||
$this->sFinalTargetDir = $sFinalTargetDir;
|
||||
$this->sRelativeDir = $sRelativeDir;
|
||||
$this->oP = $oP;
|
||||
}
|
||||
|
||||
public function CompileParentMenuNode(string $sMenuId) : void
|
||||
{
|
||||
$sStatus = array_key_exists($sMenuId, $this->aMenuProcessStatus) ? $this->aMenuProcessStatus[$sMenuId] : null;
|
||||
if ($sStatus === self::COMPILED){
|
||||
//node already processed before
|
||||
return;
|
||||
} else if ($sStatus === self::COMPILING){
|
||||
throw new \Exception("Cyclic dependency between parent menus ($sMenuId)");
|
||||
}
|
||||
|
||||
$this->aMenuProcessStatus[$sMenuId] = self::COMPILING;
|
||||
|
||||
try {
|
||||
if (! array_key_exists($sMenuId, $this->aParentMenuNodes)){
|
||||
throw new Exception("Failed to process parent menu '$sMenuId' that is referenced by a child but not defined");
|
||||
}
|
||||
$oMenuNode = $this->aParentMenuNodes[$sMenuId];
|
||||
|
||||
$sParent = $oMenuNode->GetChildText('parent', null);
|
||||
if (! empty($sParent)){
|
||||
//compile parents before (even parent of parents ... recursively)
|
||||
$this->CompileParentMenuNode($sParent);
|
||||
}
|
||||
|
||||
if (! array_key_exists($sMenuId, $this->aParentModuleRootDirs)){
|
||||
throw new Exception("Failed to process parent menu '$sMenuId' that is referenced by a child but not defined");
|
||||
}
|
||||
$sModuleRootDir = $this->aParentModuleRootDirs[$sMenuId];
|
||||
|
||||
$aMenuLines = $this->oMFCompiler->CompileMenu($oMenuNode, $this->sTempTargetDir, $this->sFinalTargetDir, $this->sRelativeDir, $this->oP);
|
||||
} catch (DOMFormatException $e) {
|
||||
throw new Exception("Failed to process menu '$sMenuId', from '$sModuleRootDir': ".$e->getMessage());
|
||||
}
|
||||
$sParent = $oMenuNode->GetChildText('parent', null);
|
||||
if (($oMenuNode->GetChildText('enable_admin_only') == '1') || isset($this->aAdminMenus[$sParent])) {
|
||||
$this->aMenuLinesForAdmins = array_merge($this->aMenuLinesForAdmins, $aMenuLines);
|
||||
$this->aAdminMenus[$oMenuNode->getAttribute("id")] = true;
|
||||
} else {
|
||||
$this->aMenuLinesForAll = array_merge($this->aMenuLinesForAll, $aMenuLines);
|
||||
}
|
||||
|
||||
$this->aMenuProcessStatus[$sMenuId] = self::COMPILED;
|
||||
}
|
||||
|
||||
public function GetMenuLinesForAdmins(): array {
|
||||
return $this->aMenuLinesForAdmins;
|
||||
}
|
||||
|
||||
public function GetMenuLinesForAll(): array {
|
||||
return $this->aMenuLinesForAll;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
78
test/setup/MFCompilerMenuTest.php
Normal file
78
test/setup/MFCompilerMenuTest.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Setup;
|
||||
|
||||
use ApplicationMenu;
|
||||
use Combodo\iTop\Test\UnitTest\ItopTestCase;
|
||||
use Config;
|
||||
use MetaModel;
|
||||
use MFCompiler;
|
||||
use RunTimeEnvironment;
|
||||
|
||||
/**
|
||||
* @group menu_compilation
|
||||
* @runTestsInSeparateProcesses
|
||||
* @preserveGlobalState disabled
|
||||
* @backupGlobals disabled
|
||||
* @since 3.0.x N°4762
|
||||
* @covers \MFCompiler::UseLatestPrecompiledFile
|
||||
*/
|
||||
class MFCompilerMenuTest extends ItopTestCase {
|
||||
private static $aPreviousEnvMenus;
|
||||
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
require_once(APPROOT.'setup/compiler.class.inc.php');
|
||||
require_once(APPROOT.'setup/modelfactory.class.inc.php');
|
||||
}
|
||||
|
||||
public function tearDown(): void {
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function CompileMenusProvider(){
|
||||
return [
|
||||
'production' => ['production'],
|
||||
'phpunit' => ['phpunit'],
|
||||
];
|
||||
}
|
||||
/**
|
||||
* @dataProvider CompileMenusProvider
|
||||
*/
|
||||
public function testCompileMenus($sEnv){
|
||||
if(\utils::GetCurrentEnvironment() != $sEnv) {
|
||||
$sConfigFilePath = \utils::GetConfigFilePath($sEnv);
|
||||
|
||||
//copy conf from production to phpunit context
|
||||
$sDirPath = dirname($sConfigFilePath);
|
||||
if (! is_dir($sDirPath)){
|
||||
mkdir($sDirPath);
|
||||
}
|
||||
$oConfig = new Config(\utils::GetConfigFilePath());
|
||||
$oConfig->WriteToFile($sConfigFilePath);
|
||||
|
||||
$oConfig = new Config($sConfigFilePath);
|
||||
$oConfig->Set('set_menu_compilation_algorithm', 'v2', 'test', true);
|
||||
$oConfig->WriteToFile();
|
||||
$oRunTimeEnvironment = new RunTimeEnvironment($sEnv);
|
||||
$oRunTimeEnvironment->CompileFrom(\utils::GetCurrentEnvironment());
|
||||
$oConfig->Set('set_menu_compilation_algorithm', 'v1', 'test', true);
|
||||
$oConfig->WriteToFile();
|
||||
}
|
||||
|
||||
$sConfigFile = APPCONF.\utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE;
|
||||
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
|
||||
|
||||
$aMenuGroups = ApplicationMenu::GetMenuGroups();
|
||||
$this->assertNotEquals([], $aMenuGroups);
|
||||
|
||||
if (! is_null(static::$aPreviousEnvMenus)){
|
||||
$this->assertEquals(static::$aPreviousEnvMenus, $aMenuGroups);
|
||||
} else {
|
||||
$this->assertNotEquals([], $aMenuGroups);
|
||||
}
|
||||
static::$aPreviousEnvMenus = $aMenuGroups;
|
||||
|
||||
//$this->InvokeNonPublicMethod(MFCompiler::class, 'CompileThemes', $this->oMFCompiler, [$oBrandingNode, $this->sTmpDir]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user