mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
2764 lines
75 KiB
PHP
2764 lines
75 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Copyright (C) 2013-2024 Combodo SAS
|
|
*
|
|
* 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
|
|
*/
|
|
|
|
/**
|
|
* ModelFactory: in-memory manipulation of the XML MetaModel
|
|
*/
|
|
|
|
use Combodo\iTop\DesignDocument;
|
|
use Combodo\iTop\DesignElement;
|
|
|
|
require_once(APPROOT.'setup/moduleinstaller.class.inc.php');
|
|
require_once(APPROOT.'setup/itopdesignformat.class.inc.php');
|
|
require_once(APPROOT.'setup/compat/domcompat.php');
|
|
require_once(APPROOT.'core/designdocument.class.inc.php');
|
|
|
|
/**
|
|
* Special exception type thrown when the XML stacking fails
|
|
*
|
|
*/
|
|
class MFException extends Exception
|
|
{
|
|
/**
|
|
* @var integer
|
|
*/
|
|
protected $iSourceLineNumber;
|
|
|
|
/**
|
|
* Used when editing partial xml delta
|
|
* @var integer
|
|
*/
|
|
protected $iSourceLineOffset;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $sXPath;
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $sExtraInfo;
|
|
|
|
public const COULD_NOT_BE_ADDED = 1;
|
|
public const COULD_NOT_BE_DELETED = 2;
|
|
public const COULD_NOT_BE_MODIFIED_NOT_FOUND = 3;
|
|
public const COULD_NOT_BE_MODIFIED_ALREADY_DELETED = 4;
|
|
public const INVALID_DELTA = 5;
|
|
public const ALREADY_DELETED = 6;
|
|
public const NOT_FOUND = 7;
|
|
public const PARENT_NOT_FOUND = 8;
|
|
public const AMBIGUOUS_LEAF = 9;
|
|
|
|
/**
|
|
* MFException constructor.
|
|
*
|
|
* @inheritDoc
|
|
*
|
|
* @param $message
|
|
* @param $code: error code
|
|
* @param $oNode: dom node
|
|
* @param $sXPath: XML xpath: if provided used in exception message. otherwise computed via $oNode
|
|
* @param $sExtraInfo: additional information stored in exception
|
|
* @param $oParentFallbackNode: fallback dom node (usually parent). in case $oNode XML line is wrong (set to 0), line number computed/displayed in error message comes from $oParentFallbackNode
|
|
*/
|
|
public function __construct($message = null, $code = 0, $oNode, $sXPath = null, $sExtraInfo = '', $oParentFallbackNode = null)
|
|
{
|
|
$iSourceLineNumber = ModelFactory::GetXMLLineNumber($oNode);
|
|
if ($iSourceLineNumber==0 && ! is_null($oParentFallbackNode)){
|
|
$iSourceLineNumber = ModelFactory::GetXMLLineNumber($oParentFallbackNode);
|
|
}
|
|
|
|
if (is_null($sXPath)){
|
|
$sXPath = DesignDocument::GetItopNodePath($oNode);
|
|
}
|
|
|
|
$this->iSourceLineNumber = $iSourceLineNumber;
|
|
$this->iSourceLineOffset = 0;
|
|
$this->sXPath = $sXPath;
|
|
$this->sExtraInfo = $sExtraInfo;
|
|
parent::__construct("$sXPath at line $iSourceLineNumber: $message", $code);
|
|
|
|
$aContext = [
|
|
'error' => $code,
|
|
'stack' => $this->getTraceAsString(),
|
|
'extra_info' => $sExtraInfo,
|
|
];
|
|
\IssueLog::Error($this->getMessage(), null, $aContext);
|
|
}
|
|
|
|
/**
|
|
* Get the source line number where the problem happened
|
|
*
|
|
* @return number
|
|
*/
|
|
public function GetSourceLineNumber()
|
|
{
|
|
return intval($this->iSourceLineNumber) - $this->iSourceLineOffset;
|
|
}
|
|
|
|
/**
|
|
* Get the XPath in the whole document where the problem happened
|
|
*
|
|
* @return string
|
|
*/
|
|
public function GetXPath()
|
|
{
|
|
return $this->sXPath;
|
|
}
|
|
|
|
/**
|
|
* Get some extra info (depending on the exception's code), like the invalid value for the _delta attribute
|
|
*
|
|
* @return string
|
|
*/
|
|
public function GetExtraInfo()
|
|
{
|
|
return $this->sExtraInfo;
|
|
}
|
|
|
|
public function SetSourceLineOffset(int $iSourceLineOffset): void
|
|
{
|
|
$this->iSourceLineOffset = $iSourceLineOffset;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ModelFactoryModule: the representation of a Module (i.e. element that can be selected during the setup)
|
|
*
|
|
* @package ModelFactory
|
|
*/
|
|
class MFModule
|
|
{
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $sId;
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $sName;
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $sVersion;
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $sRootDir;
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $sLabel;
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $aDataModels;
|
|
/**
|
|
* @var bool
|
|
*/
|
|
protected $bAutoSelect;
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $sAutoSelect;
|
|
/**
|
|
* @see ModelFactory::FindModules init of this structure from the module.*.php files
|
|
* @var array{
|
|
* business: string[],
|
|
* webservices: string[],
|
|
* addons: string[],
|
|
* }
|
|
* Warning, there are naming mismatches between this structure and the module.*.php :
|
|
* - `business` here correspond to `datamodel` in module.*.php
|
|
* - `webservices` here correspond to `webservice` in module.*.php
|
|
*/
|
|
protected $aFilesToInclude;
|
|
|
|
/**
|
|
* MFModule constructor.
|
|
*
|
|
* @param string $sId
|
|
* @param string $sRootDir
|
|
* @param string $sLabel
|
|
* @param bool $bAutoSelect
|
|
*/
|
|
public function __construct($sId, $sRootDir, $sLabel, $bAutoSelect = false)
|
|
{
|
|
$this->sId = $sId;
|
|
|
|
[$this->sName, $this->sVersion] = ModuleDiscovery::GetModuleName($sId);
|
|
if (strlen($this->sVersion) == 0) {
|
|
$this->sVersion = '1.0.0';
|
|
}
|
|
|
|
$this->sRootDir = $sRootDir;
|
|
$this->sLabel = $sLabel;
|
|
$this->aDataModels = [];
|
|
$this->bAutoSelect = $bAutoSelect;
|
|
$this->sAutoSelect = 'false';
|
|
$this->aFilesToInclude = ['addons' => [], 'business' => [], 'webservices' => [],];
|
|
|
|
if (is_null($sRootDir)) {
|
|
return;
|
|
}
|
|
|
|
if (!is_dir($sRootDir)) {
|
|
$sRootDir = APPROOT.$sRootDir;
|
|
}
|
|
|
|
// Scan the module's root directory to find the datamodel(*).xml files
|
|
if ($hDir = opendir($sRootDir)) {
|
|
// This is the correct way to loop over the directory. (according to the documentation)
|
|
while (($sFile = readdir($hDir)) !== false) {
|
|
if (preg_match('/^datamodel(.*)\.xml$/i', $sFile, $aMatches)) {
|
|
$this->aDataModels[] = $this->sRootDir.'/'.$aMatches[0];
|
|
}
|
|
}
|
|
closedir($hDir);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function GetId()
|
|
{
|
|
return $this->sId;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function GetName()
|
|
{
|
|
return $this->sName;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function GetVersion()
|
|
{
|
|
return $this->sVersion;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function GetLabel()
|
|
{
|
|
return $this->sLabel;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function GetRootDir()
|
|
{
|
|
return $this->sRootDir;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function GetModuleDir()
|
|
{
|
|
return basename($this->sRootDir);
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function GetDataModelFiles()
|
|
{
|
|
return $this->aDataModels;
|
|
}
|
|
|
|
/**
|
|
* List all classes in this module
|
|
*
|
|
* @return array
|
|
*/
|
|
public function ListClasses()
|
|
{
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function GetDictionaryFiles()
|
|
{
|
|
$aDictionaries = [];
|
|
foreach ([$this->sRootDir, $this->sRootDir.'/dictionaries'] as $sRootDir) {
|
|
if ($hDir = @opendir($sRootDir)) {
|
|
while (($sFile = readdir($hDir)) !== false) {
|
|
$aMatches = [];
|
|
if (preg_match(
|
|
"/^[^\\.]+.dict.".$this->sName.'.php$/i',
|
|
$sFile,
|
|
$aMatches
|
|
)) { // Dictionary files are named like <Lang>.dict.<ModuleName>.php
|
|
$aDictionaries[] = $sRootDir.'/'.$sFile;
|
|
}
|
|
}
|
|
closedir($hDir);
|
|
}
|
|
}
|
|
|
|
return $aDictionaries;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
public function IsAutoSelect()
|
|
{
|
|
return $this->bAutoSelect;
|
|
}
|
|
|
|
/**
|
|
* @param string $sAutoSelect
|
|
*/
|
|
public function SetAutoSelect($sAutoSelect)
|
|
{
|
|
$this->sAutoSelect = $sAutoSelect;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function GetAutoSelect()
|
|
{
|
|
return $this->sAutoSelect;
|
|
}
|
|
|
|
/**
|
|
* @param array $aFiles
|
|
* @param string $sCategory
|
|
*/
|
|
public function SetFilesToInclude($aFiles, $sCategory)
|
|
{
|
|
// Now ModuleDiscovery provides us directly with relative paths... nothing to do
|
|
$this->aFilesToInclude[$sCategory] = $aFiles;
|
|
}
|
|
|
|
/**
|
|
* @param string $sCategory
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function GetFilesToInclude($sCategory)
|
|
{
|
|
return $this->aFilesToInclude[$sCategory];
|
|
}
|
|
|
|
public function AddFileToInclude($sCategory, $sFile)
|
|
{
|
|
if (in_array($sFile, $this->aFilesToInclude[$sCategory], true)) {
|
|
return;
|
|
}
|
|
$this->aFilesToInclude[$sCategory][] = $sFile;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* MFDeltaModule: an optional module, made of a single file
|
|
*
|
|
* @package ModelFactory
|
|
*/
|
|
class MFDeltaModule extends MFModule
|
|
{
|
|
/**
|
|
* MFDeltaModule constructor.
|
|
*
|
|
* @param $sDeltaFile
|
|
*/
|
|
public function __construct($sDeltaFile)
|
|
{
|
|
parent::__construct('datamodel-delta', '', 'Additional Delta');
|
|
$this->sName = 'delta';
|
|
$this->sVersion = '1.0';
|
|
$this->aDataModels = [$sDeltaFile];
|
|
$this->aFilesToInclude = ['addons' => [], 'business' => [], 'webservices' => [],];
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function GetName()
|
|
{
|
|
return ''; // Objects created inside this pseudo module retain their original module's name
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function GetRootDir()
|
|
{
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function GetModuleDir()
|
|
{
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function GetDictionaryFiles()
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* MFDeltaModule: an optional module, made of a single file
|
|
*
|
|
* @package ModelFactory
|
|
*/
|
|
class MFCoreModule extends MFModule
|
|
{
|
|
/**
|
|
* MFCoreModule constructor.
|
|
*
|
|
* @param $sName
|
|
* @param $sLabel
|
|
* @param $sDeltaFile
|
|
*/
|
|
public function __construct($sName, $sLabel, $sDeltaFile)
|
|
{
|
|
parent::__construct($sName, '', $sLabel);
|
|
$this->sName = $sName;
|
|
$this->sVersion = '1.0';
|
|
$this->aDataModels = [$sDeltaFile];
|
|
$this->aFilesToInclude = ['addons' => [], 'business' => [], 'webservices' => [],];
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function GetRootDir()
|
|
{
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function GetModuleDir()
|
|
{
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function GetDictionaryFiles()
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* MFDictModule: an optional module, consisting only of dictionaries
|
|
*
|
|
* @package ModelFactory
|
|
*/
|
|
class MFDictModule extends MFModule
|
|
{
|
|
/**
|
|
* MFDictModule constructor.
|
|
*
|
|
* @param $sName
|
|
* @param $sLabel
|
|
* @param $sRootDir
|
|
*/
|
|
public function __construct($sName, $sLabel, $sRootDir)
|
|
{
|
|
parent::__construct($sName, $sRootDir, $sLabel);
|
|
$this->sName = $sName;
|
|
$this->sVersion = '1.0';
|
|
$this->aDataModels = [];
|
|
$this->aFilesToInclude = ['addons' => [], 'business' => [], 'webservices' => [],];
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function GetRootDir()
|
|
{
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function GetModuleDir()
|
|
{
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Scan for dictionary files recursively in $sDir
|
|
*
|
|
* @inheritDoc
|
|
*/
|
|
public function GetDictionaryFiles($sDir = null)
|
|
{
|
|
$aDictionaries = [];
|
|
$sDictionaryFilePattern = '*dictionary.itop.*.php';
|
|
|
|
if ($sDir === null) {
|
|
$sDir = $this->sRootDir;
|
|
}
|
|
|
|
if ($hDir = opendir($sDir)) {
|
|
// Matching files
|
|
$aDictionaries = glob($sDir.'/'.$sDictionaryFilePattern);
|
|
|
|
// Directories to scan
|
|
foreach (glob($sDir.'/*', GLOB_ONLYDIR | GLOB_NOSORT) as $sSubDir) {
|
|
/** @noinspection SlowArrayOperationsInLoopInspection */
|
|
$aDictionaries = array_merge($aDictionaries, $this->GetDictionaryFiles($sSubDir));
|
|
}
|
|
}
|
|
|
|
return $aDictionaries;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ModelFactory: the class that manages the in-memory representation of the XML MetaModel
|
|
*
|
|
* @package ModelFactory
|
|
*/
|
|
class ModelFactory
|
|
{
|
|
/**
|
|
* @var array Values of the _delta flag meaning that a node is "in definition" = currently being added to the delta
|
|
* @since 3.0.0
|
|
*/
|
|
public const DELTA_FLAG_IN_DEFINITION_VALUES = ['define', 'define_if_not_exists', 'redefine', 'force'];
|
|
/**
|
|
* @var array Values of the _delta flag meaning that a node is "in deletion" = currently being removed from the delta
|
|
* @since 3.0.0
|
|
*/
|
|
public const DELTA_FLAG_IN_DELETION_VALUES = ['delete', 'delete_if_exists'];
|
|
|
|
public const LOAD_DELTA_MODE_LAX = 'lax';
|
|
public const LOAD_DELTA_MODE_STRICT = 'strict';
|
|
|
|
protected $aRootDirs;
|
|
protected $oDOMDocument;
|
|
protected $oRoot;
|
|
protected static $aWellKnownParents = ['DBObject', 'CMDBObject', 'cmdbAbstractObject'];
|
|
protected static $aLoadedModules;
|
|
protected static $aLoadErrors;
|
|
protected $aDict;
|
|
protected $aDictKeys;
|
|
|
|
/**
|
|
* ModelFactory constructor.
|
|
*
|
|
* @param $aRootDirs
|
|
* @param array $aRootNodeExtensions
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function __construct($aRootDirs, $aRootNodeExtensions = [])
|
|
{
|
|
$this->aDict = [];
|
|
$this->aDictKeys = [];
|
|
$this->aRootDirs = $aRootDirs;
|
|
$this->oDOMDocument = new MFDocument();
|
|
$this->oRoot = $this->oDOMDocument->CreateElement('itop_design');
|
|
$this->oRoot->setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance");
|
|
$this->oRoot->setAttribute('version', ITOP_DESIGN_LATEST_VERSION);
|
|
$this->oDOMDocument->appendChild($this->oRoot);
|
|
$oModules = $this->oDOMDocument->CreateElement('loaded_modules');
|
|
$this->oRoot->appendChild($oModules);
|
|
$oClasses = $this->oDOMDocument->CreateElement('classes');
|
|
$this->oRoot->appendChild($oClasses);
|
|
$oDictionaries = $this->oDOMDocument->CreateElement('dictionaries');
|
|
$this->oRoot->appendChild($oDictionaries);
|
|
|
|
foreach (self::$aWellKnownParents as $sWellKnownParent) {
|
|
$this->AddWellKnownParent($oClasses, $sWellKnownParent);
|
|
}
|
|
$oMenus = $this->oDOMDocument->CreateElement('menus');
|
|
$this->oRoot->appendChild($oMenus);
|
|
|
|
$oMeta = $this->oDOMDocument->CreateElement('meta');
|
|
$this->oRoot->appendChild($oMeta);
|
|
$oEvents = $this->oDOMDocument->CreateElement('events');
|
|
$this->oRoot->appendChild($oEvents);
|
|
|
|
foreach ($aRootNodeExtensions as $sElementName) {
|
|
$oElement = $this->oDOMDocument->CreateElement($sElementName);
|
|
$this->oRoot->appendChild($oElement);
|
|
}
|
|
self::$aLoadedModules = [];
|
|
self::$aLoadErrors = [];
|
|
|
|
libxml_use_internal_errors(true);
|
|
}
|
|
|
|
/**
|
|
* @param null $oNode
|
|
* @param bool $bReturnRes
|
|
*
|
|
* @return mixed
|
|
* @throws \Exception
|
|
*/
|
|
public function Dump($oNode = null, $bReturnRes = false)
|
|
{
|
|
if (is_null($oNode)) {
|
|
$oNode = $this->oRoot;
|
|
}
|
|
|
|
return $oNode->Dump($bReturnRes);
|
|
}
|
|
|
|
/**
|
|
* @param $sCacheFile
|
|
*/
|
|
public function LoadFromFile($sCacheFile)
|
|
{
|
|
$this->oDOMDocument->load($sCacheFile);
|
|
$this->oRoot = $this->oDOMDocument->firstChild;
|
|
|
|
$oModules = $this->oRoot->getElementsByTagName('loaded_modules')->item(0);
|
|
self::$aLoadedModules = [];
|
|
foreach ($oModules->getElementsByTagName('module') as $oModuleNode) {
|
|
$sId = $oModuleNode->getAttribute('id');
|
|
$sRootDir = $oModuleNode->GetChildText('root_dir');
|
|
$sLabel = $oModuleNode->GetChildText('label');
|
|
self::$aLoadedModules[] = new MFModule($sId, $sRootDir, $sLabel);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param $sCacheFile
|
|
*/
|
|
public function SaveToFile($sCacheFile)
|
|
{
|
|
$this->oDOMDocument->save($sCacheFile);
|
|
}
|
|
|
|
/**
|
|
* To progressively replace LoadModule
|
|
*
|
|
* @param DesignElement $oSourceNode
|
|
* @param \MFDocument|\MFElement $oTargetParentNode
|
|
*
|
|
* @throws MFException
|
|
* @throws \DOMFormatException
|
|
* @throws \Exception
|
|
*/
|
|
public function LoadDelta($oSourceNode, $oTargetParentNode, string $sMode = self::LOAD_DELTA_MODE_LAX)
|
|
{
|
|
if (!$oSourceNode instanceof DOMElement) {
|
|
return;
|
|
}
|
|
if ($oTargetParentNode instanceof MFDocument) {
|
|
$oTargetDocument = $oTargetParentNode;
|
|
} else {
|
|
$oTargetDocument = $oTargetParentNode->ownerDocument;
|
|
}
|
|
|
|
if ($oSourceNode->tagName === 'itop_design') {
|
|
// Get mode if present in the tag
|
|
if ($oSourceNode->hasAttribute('load')) {
|
|
switch ($oSourceNode->getAttribute('load')) {
|
|
case self::LOAD_DELTA_MODE_STRICT:
|
|
$sMode = self::LOAD_DELTA_MODE_STRICT;
|
|
break;
|
|
case self::LOAD_DELTA_MODE_LAX:
|
|
$sMode = self::LOAD_DELTA_MODE_LAX;
|
|
break;
|
|
}
|
|
}
|
|
$oSourceNode = $this->FlattenClassesInDelta($oSourceNode);
|
|
}
|
|
|
|
$this->LoadFlattenDelta($oSourceNode, $oTargetDocument, $oTargetParentNode, $sMode);
|
|
}
|
|
|
|
private function FlattenClassesInDelta(DesignElement $oRootNode): DesignElement
|
|
{
|
|
$oDOMDocument = $oRootNode->ownerDocument;
|
|
$oXPath = new DOMXPath($oDOMDocument);
|
|
foreach ($oRootNode->childNodes as $oFirstLevelChild) {
|
|
if ($oFirstLevelChild instanceof MFElement) {
|
|
if ($oFirstLevelChild->tagName === 'classes') {
|
|
$oClassCollectionNode = $oFirstLevelChild;
|
|
// Find all <class> nodes and copy them under the target <classes> node
|
|
$oSubClassNodes = $oXPath->query('.//class[parent::class or parent::classes]', $oClassCollectionNode);
|
|
foreach ($oSubClassNodes as $oSubClassNode) {
|
|
/** @var DesignElement $oSubClassNode */
|
|
$this->SpecifyDeltaSpecsOnSubClass($oSubClassNode);
|
|
// Move comment along with class node
|
|
$oComment = ModelFactory::GetPreviousComment($oSubClassNode);
|
|
// Move (Sub)Classes from parent tree to the end of <classes>
|
|
$sParentId = $oSubClassNode->parentNode->getAttribute('id');
|
|
$oClassCollectionNode->appendChild($oSubClassNode);
|
|
if (!is_null($oComment)) {
|
|
$oClassCollectionNode->insertBefore($oComment, $oSubClassNode);
|
|
}
|
|
if ($sParentId !== '') {
|
|
$sComment = " Automatically moved from class/$sParentId to classes ";
|
|
$oCommentNode = $oDOMDocument->importNode(new DOMComment($sComment));
|
|
$oClassCollectionNode->insertBefore($oCommentNode, $oSubClassNode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $oRootNode;
|
|
}
|
|
|
|
/**
|
|
* @param DesignElement $oSubClassNode
|
|
*
|
|
* @return void
|
|
* @throws MFException
|
|
*/
|
|
public function SpecifyDeltaSpecsOnSubClass(DesignElement $oSubClassNode): void
|
|
{
|
|
$sParentDeltaSpec = $oSubClassNode->parentNode->getAttribute('_delta');
|
|
switch ($sParentDeltaSpec) {
|
|
case '':
|
|
break;
|
|
case 'define':
|
|
case 'force':
|
|
case 'redefine':
|
|
$oSubClassNode->setAttribute('_delta', 'force');
|
|
break;
|
|
case 'if_exists':
|
|
case 'define_if_not_exists':
|
|
/** @var \MFElement $oParentNode */
|
|
$oParentNode = $oSubClassNode->parentNode;
|
|
throw new MFException("_delta=\"$sParentDeltaSpec\" not supported for classes in hierarchy", MFException::NOT_FOUND, $oParentNode);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param DesignElement $oSourceNode Delta node
|
|
* @param \MFDocument $oTargetDocument Datamodel
|
|
* @param \MFDocument|\MFElement $oTargetParentNode location in the datamodel
|
|
*
|
|
* @return void
|
|
* @throws \DOMFormatException
|
|
* @throws MFException
|
|
* @throws \Exception
|
|
*/
|
|
private function LoadFlattenDelta(DesignElement $oSourceNode, MFDocument $oTargetDocument, $oTargetParentNode, string $sMode)
|
|
{
|
|
$sDeltaSpec = $oSourceNode->getAttribute('_delta');
|
|
|
|
// IMPORTANT: In case of a new flag value, mind to update the iTopDesignFormat methods
|
|
$sSearchId = $oSourceNode->hasAttribute('_rename_from') ? $oSourceNode->getAttribute('_rename_from') : $oSourceNode->getAttribute('id');
|
|
|
|
if ($oSourceNode->IsClassNode()) {
|
|
switch ($sDeltaSpec) {
|
|
case 'delete_if_exists':
|
|
case 'delete':
|
|
// Delete the nodes of all the subclasses
|
|
$this->DeleteSubClasses($oTargetParentNode->_FindChildNode($oSourceNode));
|
|
break;
|
|
|
|
case 'define_if_not_exists':
|
|
case 'define':
|
|
break;
|
|
|
|
default:
|
|
// In case the parent class has changed, be sure to move the class after its parent
|
|
/** @var MFElement $oTargetNode */
|
|
$oTargetNode = $oTargetParentNode->_FindChildNode($oSourceNode, $sSearchId);
|
|
if (!$oTargetNode) {
|
|
// No need to move non-existing class
|
|
break;
|
|
}
|
|
|
|
// Check that the parent is defined before the class
|
|
$oSourceParentClassNode = $oSourceNode->GetOptionalElement("parent");
|
|
if ($oSourceParentClassNode) {
|
|
$sParentClassName = $oSourceParentClassNode->GetText();
|
|
$sClassName = $oSourceNode->getAttribute('id');
|
|
$oNodes = $oTargetDocument->GetNodes("/itop_design/classes/class[@id=\"$sParentClassName\"]/following-sibling::class[@id=\"$sClassName\"]");
|
|
if ($oNodes->length > 0) {
|
|
// The class is already after its parent, do not move
|
|
break;
|
|
}
|
|
|
|
// Move class after new parent class (before its next sibling)
|
|
$oNodeForTargetParent = $oTargetDocument->GetNodes("/itop_design/classes/class[@id=\"$sParentClassName\"]")->item(0);
|
|
if (is_null($oNodeForTargetParent)) {
|
|
throw new MFException("invalid parent class '$sParentClassName'", MFException::NOT_FOUND, $oSourceParentClassNode);
|
|
}
|
|
$oNextParentSibling = $oNodeForTargetParent->nextSibling;
|
|
if ($oNextParentSibling) {
|
|
$oTargetParentNode->insertBefore($oTargetNode, $oNextParentSibling);
|
|
} else {
|
|
// last node, append class at the end
|
|
$oTargetParentNode->appendChild($oTargetNode);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch ($sDeltaSpec) {
|
|
case 'if_exists':
|
|
case 'must_exist':
|
|
case 'merge':
|
|
case '':
|
|
$bMustExist = ($sDeltaSpec === 'must_exist');
|
|
$bIfExists = ($sDeltaSpec === 'if_exists');
|
|
$bSpecifiedMerge = $oSourceNode->IsInSpecifiedMerge();
|
|
|
|
/** @var MFElement $oTargetNode */
|
|
$oTargetNode = $oTargetParentNode->_FindChildNode($oSourceNode, $sSearchId);
|
|
if (!$oTargetNode || $oTargetNode->IsRemoved()) {
|
|
// The node does not exist or is marked as removed
|
|
if ($bMustExist) {
|
|
throw new MFException("could not be found or marked as removed", MFException::NOT_FOUND, $oSourceNode);
|
|
}
|
|
if ($bIfExists) {
|
|
// Do not continue deeper
|
|
$oTargetNode = null;
|
|
} else {
|
|
if (!$bSpecifiedMerge && $sMode === self::LOAD_DELTA_MODE_STRICT && ($sSearchId !== '' || is_null($oSourceNode->GetFirstElementChild()))) {
|
|
throw new MFException("could not be found or marked as removed (strict mode)", MFException::NOT_FOUND, $oSourceNode, null, 'strict mode');
|
|
}
|
|
|
|
// Ignore renaming non-existant node
|
|
if ($oSourceNode->hasAttribute('_rename_from')) {
|
|
$oSourceNode->removeAttribute('_rename_from');
|
|
}
|
|
|
|
/** @var \MFElement $oTargetNode */
|
|
if (trim($oSourceNode->GetText('')) !== '') {
|
|
// node with text, let's presume that the user wants to add the complete node
|
|
$oTargetNode = $oTargetDocument->importNode($oSourceNode, true);
|
|
$oTargetParentNode->AddChildNode($oTargetNode);
|
|
// Do not continue deeper everything is already copied
|
|
$oTargetNode = null;
|
|
} else {
|
|
// copy the node with attributes (except _delta) and continue deeper
|
|
$oTargetNode = $oTargetDocument->importNode($oSourceNode, false);
|
|
foreach ($oSourceNode->attributes as $oAttributeNode) {
|
|
$oTargetNode->setAttribute($oAttributeNode->name, $oAttributeNode->value);
|
|
}
|
|
if ($oTargetNode->hasAttribute('_delta')) {
|
|
$oTargetNode->removeAttribute('_delta');
|
|
}
|
|
if ($sSearchId !== '' || $bSpecifiedMerge) {
|
|
// Add the node by default
|
|
$oTargetParentNode->AddChildNode($oTargetNode);
|
|
} else {
|
|
// Merge the node
|
|
$oTargetParentNode->appendChild($oTargetNode);
|
|
}
|
|
$oComment = $this->GetPreviousComment($oSourceNode);
|
|
if (!is_null($oComment)) {
|
|
$oCommentNode = $oTargetDocument->importNode(new DOMComment($oComment->textContent));
|
|
$oTargetParentNode->insertBefore($oCommentNode, $oTargetNode);
|
|
}
|
|
// Continue deeper
|
|
for ($oSourceChild = $oSourceNode->GetFirstElementChild(); !is_null($oSourceChild); $oSourceChild = $oSourceChild->GetNextElementSibling()) {
|
|
$this->LoadFlattenDelta($oSourceChild, $oTargetDocument, $oTargetNode, $sMode);
|
|
}
|
|
$oTargetNode = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($oTargetNode) {
|
|
if (is_null($oSourceNode->GetFirstElementChild()) && $oTargetParentNode instanceof MFElement) {
|
|
// Leaf node
|
|
if ($sMode === self::LOAD_DELTA_MODE_STRICT && !$oSourceNode->hasAttribute('_rename_from') && trim($oSourceNode->GetText('')) !== '') {
|
|
throw new MFException("cannot be modified without _delta flag (strict mode)", MFException::AMBIGUOUS_LEAF, $oSourceNode, null, 'strict mode');
|
|
} else {
|
|
// Lax mode: same as redefine
|
|
// Replace the existing node by the given node - copy child nodes as well
|
|
/** @var \MFElement $oTargetNode */
|
|
if (trim($oSourceNode->GetText('')) !== '') {
|
|
$oTargetNode = $oTargetDocument->importNode($oSourceNode, true);
|
|
$sSearchId = $oSourceNode->hasAttribute('_rename_from') ? $oSourceNode->getAttribute('_rename_from') : $oSourceNode->getAttribute('id');
|
|
$oTargetParentNode->RedefineChildNode($oTargetNode, $sSearchId, $oSourceNode);
|
|
}
|
|
}
|
|
} else {
|
|
for ($oSourceChild = $oSourceNode->GetFirstElementChild(); !is_null($oSourceChild); $oSourceChild = $oSourceChild->GetNextElementSibling()) {
|
|
// Continue deeper
|
|
$this->LoadFlattenDelta($oSourceChild, $oTargetDocument, $oTargetNode, $sMode);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'define_if_not_exists':
|
|
$oExistingNode = $oTargetParentNode->_FindChildNode($oSourceNode, $sSearchId);
|
|
if (($oExistingNode == null) || ($oExistingNode->IsRemoved())) {
|
|
// Same as 'define' below
|
|
/** @var \MFElement $oTargetNode */
|
|
$oTargetNode = $oTargetDocument->importNode($oSourceNode, true);
|
|
$oTargetParentNode->AddChildNode($oTargetNode);
|
|
$oTargetNode->SetAlteration('needed');
|
|
} else {
|
|
$oTargetNode = $oExistingNode;
|
|
}
|
|
break;
|
|
|
|
case 'define':
|
|
// New node - copy child nodes as well
|
|
/** @var \MFElement $oTargetNode */
|
|
$oTargetNode = $oTargetDocument->importNode($oSourceNode, true);
|
|
$oTargetParentNode->AddChildNode($oTargetNode);
|
|
break;
|
|
|
|
case 'force':
|
|
// Force node - copy child nodes as well
|
|
/** @var \MFElement $oTargetNode */
|
|
$oTargetNode = $oTargetDocument->importNode($oSourceNode, true);
|
|
$oTargetParentNode->SetChildNode($oTargetNode, $sSearchId, true);
|
|
break;
|
|
|
|
case 'redefine':
|
|
// Warning: this code has been duplicated above
|
|
// Replace the existing node by the given node - copy child nodes as well
|
|
/** @var \MFElement $oTargetNode */
|
|
$oTargetNode = $oTargetDocument->importNode($oSourceNode, true);
|
|
$oTargetParentNode->RedefineChildNode($oTargetNode, $sSearchId, $oSourceNode);
|
|
break;
|
|
|
|
case 'delete_if_exists':
|
|
/** @var \MFElement $oTargetNode */
|
|
$oTargetNode = $oTargetParentNode->_FindChildNode($oSourceNode, $sSearchId);
|
|
if (is_null($oTargetNode)) {
|
|
$oTargetNode = $oTargetDocument->importNode($oSourceNode, false);
|
|
$oTargetParentNode->appendChild($oTargetNode);
|
|
}
|
|
if (!$oTargetNode->IsRemoved()) {
|
|
// Delete the node if it actually exists and is not already marked as deleted
|
|
$oTargetNode->Delete(true);
|
|
}
|
|
// otherwise fail silently
|
|
break;
|
|
|
|
case 'delete':
|
|
/** @var \MFElement $oTargetNode */
|
|
$oTargetNode = $oTargetParentNode->_FindChildNode($oSourceNode, $sSearchId);
|
|
|
|
if ($oTargetNode == null) {
|
|
throw new MFException("could not be deleted (not found)", MFException::COULD_NOT_BE_DELETED, $oSourceNode);
|
|
}
|
|
if ($oTargetNode->IsRemoved()) {
|
|
throw new MFException("could not be deleted (already marked as deleted)", MFException::ALREADY_DELETED, $oSourceNode);
|
|
}
|
|
$oTargetNode->Delete();
|
|
break;
|
|
|
|
default:
|
|
throw new MFException("unexpected value for attribute _delta: '".$sDeltaSpec."'", MFException::INVALID_DELTA, $oSourceNode, null, $sDeltaSpec);
|
|
}
|
|
|
|
if ($oTargetNode && $oTargetNode->parentNode) {
|
|
if ($oSourceNode->hasAttribute('_rename_from')) {
|
|
$oTargetNode->Rename($oSourceNode->getAttribute('id'));
|
|
}
|
|
if ($oTargetNode->hasAttribute('_delta')) {
|
|
$oTargetNode->removeAttribute('_delta');
|
|
}
|
|
if ($oSourceNode->IsClassNode()) {
|
|
$oComment = $this->GetPreviousComment($oSourceNode);
|
|
if (!is_null($oComment)) {
|
|
$oCommentNode = $oTargetDocument->importNode(new DOMComment($oComment->textContent));
|
|
try {
|
|
$oTargetParentNode->insertBefore($oCommentNode, $oTargetNode);
|
|
} catch (Exception $e) {
|
|
$sComment = $oCommentNode->textContent;
|
|
throw new Exception("Error Not Found: delta: $sDeltaSpec - Comment: $sComment - ".MFDocument::GetItopNodePath($oSourceNode));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove completely the subclasses node in the datamodel to comply with the previous behavior (hierarchical classes)
|
|
* Only the root class is marked with _alteration="removed"
|
|
*
|
|
* @param $oClassNode
|
|
* @param $bIsRoot
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
public function DeleteSubClasses($oClassNode, $bIsRoot = true)
|
|
{
|
|
if (!$oClassNode instanceof MFElement) {
|
|
return;
|
|
}
|
|
|
|
$oSubClassNodes = $this->GetChildClasses($oClassNode);
|
|
foreach ($oSubClassNodes as $oSubClassNode) {
|
|
// Put the subclass before the parent classes to delete in reverse order
|
|
$this->DeleteSubClasses($oSubClassNode, false);
|
|
}
|
|
if (!$bIsRoot) {
|
|
// Hard deletion is necessary
|
|
$oClassNode->parentNode->removeChild($oClassNode);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the comment node preceding the given node
|
|
*
|
|
* @param DesignElement $oNode
|
|
*
|
|
* @return \DOMComment|null null when no comment found for that node
|
|
*/
|
|
public static function GetPreviousComment(DesignElement $oNode)
|
|
{
|
|
$oPreviousNode = $oNode->previousSibling;
|
|
|
|
while (!is_null($oPreviousNode)) {
|
|
if ($oPreviousNode instanceof DOMComment) {
|
|
return $oPreviousNode;
|
|
}
|
|
if ($oPreviousNode instanceof DesignElement) {
|
|
return null;
|
|
}
|
|
$oPreviousNode = $oPreviousNode->previousSibling;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Loads the definitions corresponding to the given Module
|
|
*
|
|
* @param MFModule $oModule
|
|
* @param array $aLanguages The list of languages to process (for the dictionaries). If empty all languages are kept
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function LoadModule(MFModule $oModule, $aLanguages = [])
|
|
{
|
|
try {
|
|
$aDataModels = $oModule->GetDataModelFiles();
|
|
$sModuleName = $oModule->GetName();
|
|
self::$aLoadedModules[] = $oModule;
|
|
|
|
// For persistence in the cache
|
|
$oModuleNode = $this->oDOMDocument->CreateElement('module');
|
|
$oModuleNode->setAttribute('id', $oModule->GetId());
|
|
$oModuleNode->appendChild($this->oDOMDocument->CreateElement('root_dir', $oModule->GetRootDir()));
|
|
$oModuleNode->appendChild($this->oDOMDocument->CreateElement('label', $oModule->GetLabel()));
|
|
|
|
$oModules = $this->oRoot->getElementsByTagName('loaded_modules')->item(0);
|
|
$oModules->appendChild($oModuleNode);
|
|
|
|
foreach ($aDataModels as $sXmlFile) {
|
|
$oDocument = new MFDocument();
|
|
libxml_clear_errors();
|
|
$oDocument->load($sXmlFile);
|
|
$aErrors = libxml_get_errors();
|
|
if (count($aErrors) > 0) {
|
|
throw new Exception($this->GetXMLErrorMessage($aErrors));
|
|
}
|
|
|
|
$oXPath = new DOMXPath($oDocument);
|
|
$oNodeList = $oXPath->query('/itop_design/classes//class');
|
|
foreach ($oNodeList as $oNode) {
|
|
if ($oNode->getAttribute('_created_in') == '') {
|
|
$oNode->SetAttribute('_created_in', $sModuleName);
|
|
}
|
|
}
|
|
$oNodeList = $oXPath->query('/itop_design/constants/constant');
|
|
foreach ($oNodeList as $oNode) {
|
|
if ($oNode->getAttribute('_created_in') == '') {
|
|
$oNode->SetAttribute('_created_in', $sModuleName);
|
|
}
|
|
}
|
|
$oNodeList = $oXPath->query('/itop_design/events/event');
|
|
foreach ($oNodeList as $oNode) {
|
|
if ($oNode->getAttribute('_created_in') == '') {
|
|
$oNode->SetAttribute('_created_in', $sModuleName);
|
|
}
|
|
}
|
|
$oNodeList = $oXPath->query('/itop_design/menus/menu');
|
|
foreach ($oNodeList as $oNode) {
|
|
if ($oNode->getAttribute('_created_in') == '') {
|
|
$oNode->SetAttribute('_created_in', $sModuleName);
|
|
}
|
|
}
|
|
$oUserRightsNode = $oXPath->query('/itop_design/user_rights')->item(0);
|
|
if ($oUserRightsNode) {
|
|
if ($oUserRightsNode->getAttribute('_created_in') == '') {
|
|
$oUserRightsNode->SetAttribute('_created_in', $sModuleName);
|
|
}
|
|
}
|
|
|
|
$oAlteredNodes = $oXPath->query('/itop_design//*[@_delta]');
|
|
if ($oAlteredNodes->length > 0) {
|
|
foreach ($oAlteredNodes as $oAlteredNode) {
|
|
$oAlteredNode->SetAttribute('_altered_in', $sModuleName);
|
|
}
|
|
}
|
|
|
|
$oFormat = new iTopDesignFormat($oDocument);
|
|
if (!$oFormat->Convert()) {
|
|
$sError = implode(', ', $oFormat->GetErrors());
|
|
throw new Exception("Cannot load module $sModuleName, failed to upgrade to datamodel format of: $sXmlFile. Reason(s): $sError");
|
|
}
|
|
|
|
$oDeltaRoot = $oDocument->childNodes->item(0);
|
|
$this->LoadDelta($oDeltaRoot, $this->oDOMDocument);
|
|
}
|
|
|
|
$aDictionaries = $oModule->GetDictionaryFiles();
|
|
|
|
$sPHPFile = 'undefined';
|
|
try {
|
|
$this->ResetTempDictionary();
|
|
foreach ($aDictionaries as $sPHPFile) {
|
|
$sDictFileContents = file_get_contents($sPHPFile);
|
|
$sDictFileContents = str_replace(['<'.'?'.'php', '?'.'>'], '', $sDictFileContents);
|
|
$sDictFileContents = str_replace('Dict::Add', '$this->AddToTempDictionary', $sDictFileContents);
|
|
eval($sDictFileContents);
|
|
}
|
|
foreach ($this->aDict as $sLanguageCode => $aDictDefinition) {
|
|
if ((count($aLanguages) > 0) && !in_array($sLanguageCode, $aLanguages)) {
|
|
// skip some languages if the parameter says so
|
|
continue;
|
|
}
|
|
$this->IntegrateDictEntriesIntoXML($sLanguageCode, $aDictDefinition);
|
|
}
|
|
} catch (Exception|Error $e) { // Error can occurs on eval() calls
|
|
throw new DictException('Failed to load dictionary file "'.$sPHPFile.'"', [
|
|
'exception_class' => get_class($e),
|
|
'exception_msg' => $e->getMessage(),
|
|
]);
|
|
}
|
|
} catch (Exception $e) {
|
|
$aLoadedModuleNames = [];
|
|
foreach (self::$aLoadedModules as $oLoadedModule) {
|
|
$aLoadedModuleNames[] = $oLoadedModule->GetName().':'.$oLoadedModule->GetVersion();
|
|
}
|
|
throw new Exception('Error loading module "'.$oModule->GetName().'": '.$e->getMessage().' - Loaded modules: '.implode(
|
|
', ',
|
|
$aLoadedModuleNames
|
|
));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $sLanguageCode
|
|
* @param array $aDictDefinition
|
|
*
|
|
* @return void
|
|
* @throws MFException
|
|
*/
|
|
public function IntegrateDictEntriesIntoXML(string $sLanguageCode, array $aDictDefinition): void
|
|
{
|
|
$oDictionaries = $this->oRoot->getElementsByTagName('dictionaries')->item(0);
|
|
$oNodes = $this->GetNodeById('dictionary', $sLanguageCode, $oDictionaries);
|
|
if ($oNodes->length == 0) {
|
|
$oXmlDict = $this->oDOMDocument->CreateElement('dictionary');
|
|
$oXmlDict->setAttribute('id', $sLanguageCode);
|
|
$oDictionaries->AddChildNode($oXmlDict);
|
|
$oXmlEntries = $this->oDOMDocument->CreateElement('english_description', $aDictDefinition['english_description']);
|
|
$oXmlDict->appendChild($oXmlEntries);
|
|
$oXmlEntries = $this->oDOMDocument->CreateElement(
|
|
'localized_description',
|
|
$aDictDefinition['localized_description']
|
|
);
|
|
$oXmlDict->appendChild($oXmlEntries);
|
|
$oXmlEntries = $this->oDOMDocument->CreateElement('entries');
|
|
$oXmlDict->appendChild($oXmlEntries);
|
|
} else {
|
|
$oXmlDict = $oNodes->item(0);
|
|
$oXmlEntries = $oXmlDict->GetUniqueElement('entries');
|
|
}
|
|
|
|
foreach ($aDictDefinition['entries'] as $sCode => $sLabel) {
|
|
|
|
$oXmlEntry = $this->oDOMDocument->CreateElement('entry');
|
|
$oXmlEntry->setAttribute('id', $sCode);
|
|
$oXmlValue = $this->oDOMDocument->CreateCDATASection($sLabel);
|
|
$oXmlEntry->appendChild($oXmlValue);
|
|
if (array_key_exists($sLanguageCode, $this->aDictKeys) && array_key_exists($sCode, $this->aDictKeys[$sLanguageCode])) {
|
|
$oXmlEntries->RedefineChildNode($oXmlEntry);
|
|
$oXmlEntry->RemoveAlteration();
|
|
} else {
|
|
// To avoid memory peak during execution of ApplyChanges, just set the node without alteration flag
|
|
$oXmlEntries->appendChild($oXmlEntry);
|
|
}
|
|
$this->aDictKeys[$sLanguageCode][$sCode] = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collects the PHP Dict entries into the ModelFactory for transforming the dictionary into an XML structure
|
|
*
|
|
* @param string $sLanguageCode The language code
|
|
* @param string $sEnglishLanguageDesc English description of the language (unused but kept for API compatibility)
|
|
* @param string $sLocalizedLanguageDesc Localized description of the language (unused but kept for API compatibility)
|
|
* @param array $aEntries The entries to load: string_code => translation
|
|
*/
|
|
protected function AddToTempDictionary($sLanguageCode, $sEnglishLanguageDesc, $sLocalizedLanguageDesc, $aEntries)
|
|
{
|
|
$this->aDict[$sLanguageCode]['english_description'] = $sEnglishLanguageDesc;
|
|
$this->aDict[$sLanguageCode]['localized_description'] = $sLocalizedLanguageDesc;
|
|
if (!array_key_exists('entries', $this->aDict[$sLanguageCode])) {
|
|
$this->aDict[$sLanguageCode]['entries'] = [];
|
|
}
|
|
|
|
foreach ($aEntries as $sKey => $sValue) {
|
|
$this->aDict[$sLanguageCode]['entries'][$sKey] = $sValue;
|
|
}
|
|
}
|
|
|
|
protected function ResetTempDictionary()
|
|
{
|
|
$this->aDict = [];
|
|
}
|
|
|
|
/**
|
|
* XML load errors (XML format and validation)
|
|
*
|
|
* @Deprecated Errors are now sent by Exception
|
|
*/
|
|
public function HasLoadErrors()
|
|
{
|
|
DeprecatedCallsLog::NotifyDeprecatedPhpMethod('Errors are now sent by Exception');
|
|
|
|
return (count(self::$aLoadErrors) > 0);
|
|
}
|
|
|
|
/**
|
|
* @Deprecated Errors are now sent by Exception
|
|
* @return array
|
|
*/
|
|
public function GetLoadErrors()
|
|
{
|
|
DeprecatedCallsLog::NotifyDeprecatedPhpMethod('Errors are now sent by Exception');
|
|
|
|
return self::$aLoadErrors;
|
|
}
|
|
|
|
/**
|
|
* @param array $aErrors
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function GetXMLErrorMessage($aErrors)
|
|
{
|
|
$sMessage = "Data model source file ({$aErrors[0]->file}) could not be loaded : \n";
|
|
foreach ($aErrors as $oXmlError) {
|
|
// XML messages already ends with \n
|
|
$sMessage .= $oXmlError->message;
|
|
}
|
|
|
|
return $sMessage;
|
|
}
|
|
|
|
/**
|
|
* @param bool $bExcludeWorkspace
|
|
*
|
|
* @return MFModule[]
|
|
*/
|
|
public function GetLoadedModules($bExcludeWorkspace = true)
|
|
{
|
|
if ($bExcludeWorkspace) {
|
|
$aModules = [];
|
|
foreach (self::$aLoadedModules as $oModule) {
|
|
if (!$oModule instanceof MFWorkspace) {
|
|
$aModules[] = $oModule;
|
|
}
|
|
}
|
|
} else {
|
|
$aModules = self::$aLoadedModules;
|
|
}
|
|
|
|
return $aModules;
|
|
}
|
|
|
|
/**
|
|
* @param $sModuleName
|
|
*
|
|
* @return mixed|null
|
|
*/
|
|
public function GetModule($sModuleName)
|
|
{
|
|
foreach (self::$aLoadedModules as $oModule) {
|
|
if ($oModule->GetName() == $sModuleName) {
|
|
return $oModule;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param $sTagName
|
|
* @param string $sValue
|
|
*
|
|
* @return mixed
|
|
* @throws \Exception
|
|
*/
|
|
public function CreateElement($sTagName, $sValue = '')
|
|
{
|
|
return $this->oDOMDocument->createElement($sTagName, $sValue);
|
|
}
|
|
|
|
/**
|
|
* @param $sXPath
|
|
* @param $sId
|
|
* @param null $oContextNode
|
|
*
|
|
* @return \DOMNodeList
|
|
*/
|
|
public function GetNodeById($sXPath, $sId, $oContextNode = null)
|
|
{
|
|
return $this->oDOMDocument->GetNodeById($sXPath, $sId, $oContextNode);
|
|
}
|
|
|
|
/**
|
|
* Check if the class specified by the given name already exists in the loaded DOM
|
|
*
|
|
* @param string $sClassName The node corresponding to the class to load
|
|
* @param bool $bIncludeMetas Look for $sClassName also in meta declaration (PHP classes) if not found in XML classes
|
|
*
|
|
* @return bool True if the class exists, false otherwise
|
|
* @throws \Exception
|
|
*
|
|
*/
|
|
protected function ClassNameExists($sClassName, $bIncludeMetas = false)
|
|
{
|
|
return !is_null($this->GetClass($sClassName, $bIncludeMetas));
|
|
}
|
|
|
|
/**
|
|
* Add the given class to the DOM
|
|
*
|
|
* @param DOMNode $oClassNode
|
|
* @param string $sModuleName The name of the module in which this class is declared
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
public function AddClass(DOMNode $oClassNode, $sModuleName)
|
|
{
|
|
if ($oClassNode->hasAttribute('id')) {
|
|
$sClassName = $oClassNode->GetAttribute('id');
|
|
} else {
|
|
throw new Exception('ModelFactory::AddClass: Cannot add a class with no name');
|
|
}
|
|
if ($this->ClassNameExists($oClassNode->getAttribute('id'))) {
|
|
throw new Exception("ModelFactory::AddClass: Cannot add the already existing class $sClassName");
|
|
}
|
|
$sParentClass = $oClassNode->GetChildText('parent', '');
|
|
if (false === $this->ClassNameExists($sParentClass)) {
|
|
throw new Exception("ModelFactory::AddClass: Cannot find the parent class of '$sClassName': '$sParentClass'");
|
|
}
|
|
|
|
if ($sModuleName != '') {
|
|
$oClassNode->SetAttribute('_created_in', $sModuleName);
|
|
}
|
|
|
|
/** @var \MFElement $oImportedNode */
|
|
$oClasses = $this->GetNodes("/itop_design/classes")->item(0);
|
|
$oImportedNode = $this->oDOMDocument->importNode($oClassNode, true);
|
|
$oClasses->AddChildNode($oImportedNode);
|
|
}
|
|
|
|
/**
|
|
* @param $sName
|
|
* @param $sIcon
|
|
*
|
|
* @return string
|
|
*/
|
|
public function GetClassXMLTemplate($sName, $sIcon)
|
|
{
|
|
$sHeader = '<?'.'xml version="1.0" encoding="utf-8"?'.'>';
|
|
|
|
return
|
|
<<<EOF
|
|
$sHeader
|
|
<class id="$sName">
|
|
<comment/>
|
|
<properties>
|
|
</properties>
|
|
<naming format=""><attributes/></naming>
|
|
<reconciliation><attributes/></reconciliation>
|
|
<icon>$sIcon</icon>
|
|
</properties>
|
|
<fields/>
|
|
<lifecycle/>
|
|
<methods/>
|
|
<presentation>
|
|
<details><items/></details>
|
|
<search><items/></search>
|
|
<list><items/></list>
|
|
</presentation>
|
|
</class>
|
|
EOF
|
|
;
|
|
}
|
|
|
|
/**
|
|
* List all constants from the DOM, for a given module
|
|
*
|
|
* @param string $sModuleName
|
|
*
|
|
* @return \DOMNodeList
|
|
* @throws Exception
|
|
*/
|
|
public function ListConstants($sModuleName)
|
|
{
|
|
return $this->GetNodes("/itop_design/constants/constant[@_created_in='$sModuleName']");
|
|
}
|
|
|
|
/**
|
|
* List all events from the DOM, for a given module
|
|
*
|
|
* @param string $sModuleName
|
|
*
|
|
* @return \DOMNodeList
|
|
* @throws Exception
|
|
*/
|
|
public function ListEvents($sModuleName)
|
|
{
|
|
return $this->GetNodes("/itop_design/events/event[@_created_in='$sModuleName']");
|
|
}
|
|
|
|
/**
|
|
* List all classes from the DOM, for a given module
|
|
*
|
|
* @param string $sModuleName
|
|
*
|
|
* @return \DOMNodeList
|
|
* @throws Exception
|
|
*/
|
|
public function ListClasses($sModuleName)
|
|
{
|
|
return $this->GetNodes("/itop_design/classes/class[@id][@_created_in='$sModuleName']");
|
|
}
|
|
|
|
/**
|
|
* List all classes from the DOM
|
|
*
|
|
* @param bool $bIncludeMetas Also look for meta declaration (PHP classes) in addition to XML classes
|
|
*
|
|
* @return \DOMNodeList
|
|
*/
|
|
public function ListAllClasses($bIncludeMetas = false)
|
|
{
|
|
$sXPath = "/itop_design/classes/class[@id]";
|
|
if ($bIncludeMetas === true) {
|
|
$sXPath .= "|/itop_design/meta/classes/class[@id]";
|
|
}
|
|
|
|
return $this->GetNodes($sXPath);
|
|
}
|
|
|
|
/**
|
|
* List top level (non abstract) classes having child classes
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
public function ListRootClasses()
|
|
{
|
|
$aClasses = $this->ListAllClasses();
|
|
$aRootClasses = [];
|
|
/** @var \MFElement $oClass */
|
|
foreach ($aClasses as $oClass) {
|
|
if (false === in_array($oClass->GetChildText('parent', ''), self::$aWellKnownParents)) {
|
|
continue;
|
|
}
|
|
$sClassName = $oClass->getAttribute('id');
|
|
$sClassName = DesignDocument::XPathQuote($sClassName);
|
|
if (count($this->GetNodes("/itop_design/classes/class/parent[text()=$sClassName]")) > 0) {
|
|
$aRootClasses[] = "@id=$sClassName";
|
|
}
|
|
}
|
|
if (count($aRootClasses) === 0) {
|
|
return $this->GetNodes('/itop_design/classes/class[not(@id)]');
|
|
}
|
|
$sIds = implode(' and ', $aRootClasses);
|
|
return $this->GetNodes("/itop_design/classes/class[$sIds]");
|
|
}
|
|
|
|
/**
|
|
* @param string $sClassName
|
|
* @param bool $bIncludeMetas Look for $sClassName also in meta declaration (PHP classes) if not found in XML classes
|
|
*
|
|
* @return \MFElement|null
|
|
*/
|
|
public function GetClass($sClassName, $bIncludeMetas = false)
|
|
{
|
|
// Check if class among XML classes
|
|
/** @var \MFElement|null $oClassNode */
|
|
$oClassNode = $this->GetNodes("/itop_design/classes/class[@id='$sClassName']")->item(0);
|
|
|
|
// If not, check if class among exposed meta classes (PHP classes)
|
|
if (is_null($oClassNode) && ($bIncludeMetas === true)) {
|
|
/** @var \MFElement|null $oClassNode */
|
|
$oClassNode = $this->GetNodes("/itop_design/meta/classes/class[@id='$sClassName']")->item(0);
|
|
}
|
|
|
|
return $oClassNode;
|
|
}
|
|
|
|
/**
|
|
* @param string $sWellKnownParent
|
|
*
|
|
* @return mixed
|
|
* @throws \Exception
|
|
*/
|
|
public function AddWellKnownParent(MFElement $oClasses, $sWellKnownParent)
|
|
{
|
|
$oWKClass = $this->oDOMDocument->CreateElement('class');
|
|
$oWKClass->setAttribute('id', $sWellKnownParent);
|
|
$oClasses->appendChild($oWKClass);
|
|
|
|
return $oWKClass;
|
|
}
|
|
|
|
/**
|
|
* Get the direct child classes
|
|
* @param \MFElement $oClassNode
|
|
*
|
|
* @return \DOMNodeList
|
|
*/
|
|
public function GetChildClasses($oClassNode)
|
|
{
|
|
$sClassId = $oClassNode->getAttribute('id');
|
|
return $this->oDOMDocument->GetNodes("/itop_design/classes/class[parent/text()[. = '$sClassId']]");
|
|
}
|
|
|
|
/**
|
|
* @param string $sClassName
|
|
* @param string $sAttCode
|
|
*
|
|
* @return \MFElement|null
|
|
* @throws \Exception
|
|
*/
|
|
public function GetField($sClassName, $sAttCode)
|
|
{
|
|
if (!$this->ClassNameExists($sClassName)) {
|
|
return null;
|
|
}
|
|
$oClassNode = $this->GetClass($sClassName);
|
|
/** @var \MFElement|null $oFieldNode */
|
|
$oFieldNode = $this->GetNodes("fields/field[@id='$sAttCode']", $oClassNode)->item(0);
|
|
if (($oFieldNode == null) && ($sParentClass = $oClassNode->GetChildText('parent'))) {
|
|
return $this->GetField($sParentClass, $sAttCode);
|
|
}
|
|
|
|
return $oFieldNode;
|
|
}
|
|
|
|
/**
|
|
* List all fields of a class from the DOM
|
|
*
|
|
* @param \DOMNode $oClassNode
|
|
*
|
|
* @return \DOMNodeList
|
|
*/
|
|
public function ListFields(DOMNode $oClassNode)
|
|
{
|
|
return $this->GetNodes("fields/field", $oClassNode);
|
|
}
|
|
|
|
/**
|
|
* List all transitions from a given state
|
|
*
|
|
* @param DOMNode $oStateNode The state
|
|
*
|
|
* @return \DOMNodeList
|
|
* @throws Exception
|
|
*/
|
|
public function ListTransitions(DOMNode $oStateNode)
|
|
{
|
|
return $this->GetNodes("transitions/transition", $oStateNode);
|
|
}
|
|
|
|
/**
|
|
* List all states of a given class
|
|
*
|
|
* @param DOMNode $oClassNode The class
|
|
*
|
|
* @return \DOMNodeList
|
|
* @throws Exception
|
|
*/
|
|
public function ListStates(DOMNode $oClassNode)
|
|
{
|
|
return $this->GetNodes("lifecycle/states/state", $oClassNode);
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function ApplyChanges()
|
|
{
|
|
$this->oRoot->ApplyChanges();
|
|
}
|
|
|
|
/**
|
|
* @return mixed
|
|
*/
|
|
public function ListChanges()
|
|
{
|
|
return $this->oRoot->ListChanges();
|
|
}
|
|
|
|
/**
|
|
* Import the node into the delta
|
|
*
|
|
* @param DesignElement $oNodeClone
|
|
*
|
|
* @return mixed
|
|
*/
|
|
protected function SetDeltaFlags($oNodeClone)
|
|
{
|
|
$sAlteration = $oNodeClone->GetAlteration();
|
|
$oNodeClone->RemoveAlteration();
|
|
if ($oNodeClone->hasAttribute('_old_id')) {
|
|
$oNodeClone->setAttribute('_rename_from', $oNodeClone->getAttribute('_old_id'));
|
|
$oNodeClone->removeAttribute('_old_id');
|
|
}
|
|
// IMPORTANT: In case of a new flag value, mind to update the iTopDesignFormat methods
|
|
switch ($sAlteration) {
|
|
case '':
|
|
if ($oNodeClone->hasAttribute('id')) {
|
|
//$oNodeClone->setAttribute('_delta', 'merge');
|
|
}
|
|
break;
|
|
case 'added':
|
|
$oNodeClone->setAttribute('_delta', 'define');
|
|
break;
|
|
case 'replaced':
|
|
$oNodeClone->setAttribute('_delta', 'redefine');
|
|
break;
|
|
case 'removed':
|
|
$oNodeClone->setAttribute('_delta', 'delete');
|
|
break;
|
|
case 'remove_needed':
|
|
$oNodeClone->setAttribute('_delta', 'delete_if_exists');
|
|
break;
|
|
case 'needed':
|
|
$oNodeClone->setAttribute('_delta', 'define_if_not_exists');
|
|
break;
|
|
case 'forced':
|
|
$oNodeClone->setAttribute('_delta', 'force');
|
|
break;
|
|
}
|
|
|
|
return $oNodeClone;
|
|
}
|
|
|
|
/**
|
|
* Create path for the delta
|
|
*
|
|
* @param DOMDocument $oTargetDoc Where to attach the top of the hierarchy
|
|
* @param MFElement $oNode The node to import with its path
|
|
*
|
|
* @return \DOMElement|null
|
|
*/
|
|
protected function ImportNodeAndPathDelta($oTargetDoc, $oNode)
|
|
{
|
|
$oParent = $oNode->parentNode;
|
|
|
|
// Recursively create the path for the parent
|
|
if ($oParent instanceof DOMElement) {
|
|
$oParentClone = $this->ImportNodeAndPathDelta($oTargetDoc, $oParent);
|
|
} else {
|
|
// We've reached the top let's add the node into the root recipient
|
|
$oParentClone = $oTargetDoc;
|
|
}
|
|
|
|
$sAlteration = $oNode->GetAlteration();
|
|
if ($oNode->IsClassNode() && ($sAlteration != '')) {
|
|
// Handle the moved classes
|
|
//
|
|
// Import the whole root node
|
|
$oNodeClone = $oTargetDoc->importNode($oNode->cloneNode(true), true);
|
|
$oParentClone->appendChild($oNodeClone);
|
|
$this->SetDeltaFlags($oNodeClone);
|
|
} else {
|
|
// Look for the node into the parent node
|
|
// Note: this is an identified weakness of the algorithm,
|
|
// because for each node modified, and each node of its path
|
|
// we will have to lookup for the existing entry
|
|
// Anyhow, this loop is quite quick to execute because in the delta
|
|
// the number of nodes is limited
|
|
$oNodeClone = null;
|
|
foreach ($oParentClone->childNodes as $oChild) {
|
|
if (($oChild instanceof DOMElement) && ($oChild->tagName == $oNode->tagName)) {
|
|
if (!$oNode->hasAttribute('id') || ($oNode->getAttribute('id') == $oChild->getAttribute('id'))) {
|
|
$oNodeClone = $oChild;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!$oNodeClone) {
|
|
$bCopyContents = ($sAlteration == 'replaced') || ($sAlteration == 'added') || ($sAlteration == 'needed') || ($sAlteration == 'forced');
|
|
$oNodeClone = $oTargetDoc->importNode($oNode->cloneNode($bCopyContents), $bCopyContents);
|
|
$this->SetDeltaFlags($oNodeClone);
|
|
$oParentClone->appendChild($oNodeClone);
|
|
}
|
|
}
|
|
|
|
return $oNodeClone;
|
|
}
|
|
|
|
/**
|
|
* Set the value for a given trace attribute
|
|
* See MFElement::SetTrace to enable/disable change traces
|
|
*
|
|
* @param $sAttribute
|
|
* @param $sPreviousValue
|
|
* @param $sNewValue
|
|
*/
|
|
public function SetTraceValue($sAttribute, $sPreviousValue, $sNewValue)
|
|
{
|
|
// Search into the deleted node as well!
|
|
$oNodeSet = $this->oDOMDocument->GetNodes("//*[@$sAttribute='$sPreviousValue']", null, false);
|
|
foreach ($oNodeSet as $oTouchedNode) {
|
|
$oTouchedNode->setAttribute($sAttribute, $sNewValue);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the document version of the delta
|
|
*
|
|
* @param array $aNodesToIgnore
|
|
* @param null $aAttributes
|
|
*
|
|
* @return \MFDocument
|
|
* @throws \Exception
|
|
*/
|
|
public function GetDeltaDocument($aNodesToIgnore = [], $aAttributes = null)
|
|
{
|
|
$oDelta = new MFDocument();
|
|
|
|
foreach ($this->ListChanges() as $oAlteredNode) {
|
|
$this->ImportNodeAndPathDelta($oDelta, $oAlteredNode);
|
|
}
|
|
foreach ($aNodesToIgnore as $sXPath) {
|
|
$oNodesToRemove = $oDelta->GetNodes($sXPath);
|
|
foreach ($oNodesToRemove as $oNode) {
|
|
if ($oNode instanceof DOMAttr) {
|
|
$oNode->ownerElement->removeAttributeNode($oNode);
|
|
} else {
|
|
$oNode->parentNode->removeChild($oNode);
|
|
}
|
|
}
|
|
}
|
|
$oNodesToClean = $oDelta->GetNodes('/itop_design//*[@_altered_in]');
|
|
foreach ($oNodesToClean as $oNode) {
|
|
$oNode->removeAttribute('_altered_in');
|
|
}
|
|
|
|
if ($aAttributes != null) {
|
|
foreach ($aAttributes as $sAttribute => $value) {
|
|
if ($oDelta->documentElement) { // yes, this may happen when still no change has been performed (and a module has been selected for installation)
|
|
$oDelta->documentElement->setAttribute($sAttribute, $value);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $oDelta;
|
|
}
|
|
|
|
/**
|
|
* Get the text/XML version of the delta
|
|
*
|
|
* @param array $aNodesToIgnore
|
|
* @param null $aAttributes
|
|
*
|
|
* @return string
|
|
* @throws \Exception
|
|
*/
|
|
public function GetDelta($aNodesToIgnore = [], $aAttributes = null)
|
|
{
|
|
$oDelta = $this->GetDeltaDocument($aNodesToIgnore, $aAttributes);
|
|
|
|
return $oDelta->saveXML();
|
|
}
|
|
|
|
/**
|
|
* Searches on disk in the root directories for module description files
|
|
* and returns an array of MFModules
|
|
*
|
|
* @return array Array of MFModules
|
|
* @throws \Exception
|
|
*/
|
|
public function FindModules()
|
|
{
|
|
$aAvailableModules = ModuleDiscovery::GetAvailableModules($this->aRootDirs);
|
|
$aResult = [];
|
|
foreach ($aAvailableModules as $sId => $aModule) {
|
|
$oModule = new MFModule($sId, $aModule['root_dir'], $aModule['label'], isset($aModule['auto_select']));
|
|
if (isset($aModule['auto_select'])) {
|
|
$oModule->SetAutoSelect($aModule['auto_select']);
|
|
}
|
|
if (isset($aModule['datamodel']) && is_array($aModule['datamodel'])) {
|
|
$oModule->SetFilesToInclude($aModule['datamodel'], 'business');
|
|
}
|
|
if (isset($aModule['webservice']) && is_array($aModule['webservice'])) {
|
|
$oModule->SetFilesToInclude($aModule['webservice'], 'webservices');
|
|
}
|
|
if (isset($aModule['addons']) && is_array($aModule['addons'])) {
|
|
$oModule->SetFilesToInclude($aModule['addons'], 'addons');
|
|
}
|
|
$aResult[] = $oModule;
|
|
}
|
|
|
|
return $aResult;
|
|
}
|
|
|
|
/**
|
|
* Extracts some nodes from the DOM
|
|
*
|
|
* @param string $sXPath A XPath expression
|
|
* @param null $oContextNode
|
|
* @param bool $bSafe
|
|
*
|
|
* @return DOMNodeList
|
|
*/
|
|
public function GetNodes($sXPath, $oContextNode = null, $bSafe = true)
|
|
{
|
|
return $this->oDOMDocument->GetNodes($sXPath, $oContextNode, $bSafe);
|
|
}
|
|
|
|
/**
|
|
* @return mixed
|
|
*/
|
|
public function GetRootDirs()
|
|
{
|
|
return $this->aRootDirs;
|
|
}
|
|
|
|
/**
|
|
* @param \DOMElement $oNode
|
|
*
|
|
* @return mixed
|
|
* @Since 3.1.1
|
|
*/
|
|
public static function GetXMLLineNumber($oNode)
|
|
{
|
|
return $oNode->getLineNo();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* MFElement: helper to read/change the DOM
|
|
*
|
|
* @package ModelFactory
|
|
* @property \MFDocument $ownerDocument This is only here for type hinting as iTop replaces \DOMDocument with \MFDocument
|
|
* @property \MFElement $parentNode This is only here for type hinting as iTop replaces \DOMElement with \MFElement
|
|
*/
|
|
class MFElement extends Combodo\iTop\DesignElement
|
|
{
|
|
/**
|
|
* Extracts some nodes from the DOM
|
|
*
|
|
* @param string $sXPath A XPath expression
|
|
* @param bool $bSafe
|
|
*
|
|
* @return DOMNodeList
|
|
*/
|
|
public function GetNodes($sXPath, $bSafe = true)
|
|
{
|
|
return $this->ownerDocument->GetNodes($sXPath, $this, $bSafe);
|
|
}
|
|
|
|
/**
|
|
* Extracts some nodes from the DOM (active nodes only !!!)
|
|
*
|
|
* @param string $sXPath A XPath expression
|
|
* @param string $sId
|
|
*
|
|
* @return DOMNodeList
|
|
*/
|
|
public function GetNodeById($sXPath, $sId)
|
|
{
|
|
return $this->ownerDocument->GetNodeById($sXPath, $sId, $this);
|
|
}
|
|
|
|
/**
|
|
* Returns the node directly under the given node
|
|
*
|
|
* @param string $sTagName
|
|
* @param bool $bMustExist
|
|
*
|
|
* @return MFElement
|
|
* @throws \DOMFormatException
|
|
*/
|
|
public function GetUniqueElement($sTagName, $bMustExist = true)
|
|
{
|
|
$oNode = null;
|
|
foreach ($this->childNodes as $oChildNode) {
|
|
/** @var MFElement $oChildNode */
|
|
if (($oChildNode->nodeName == $sTagName) && !$oChildNode->IsRemoved()) {
|
|
$oNode = $oChildNode;
|
|
break;
|
|
}
|
|
}
|
|
if ($bMustExist && is_null($oNode)) {
|
|
$sXPath = DesignDocument::GetItopNodePath($this);
|
|
throw new DOMFormatException("Missing unique tag: $sTagName in: $sXPath");
|
|
}
|
|
|
|
return $oNode;
|
|
}
|
|
|
|
/**
|
|
* Assumes the current node to be either a text or
|
|
* <items>
|
|
* <item [key]="..."]>value<item>
|
|
* <item [key]="..."]>value<item>
|
|
* </items>
|
|
* where value can be the either a text or an array of items... recursively
|
|
* Returns a PHP array
|
|
*
|
|
* @param string $sElementName
|
|
*
|
|
* @return array|string if no subnode is found, return current node text, else return results as array
|
|
* @throws \DOMFormatException
|
|
*/
|
|
public function GetNodeAsArrayOfItems($sElementName = 'items')
|
|
{
|
|
$oItems = $this->GetOptionalElement($sElementName);
|
|
if ($oItems) {
|
|
$res = [];
|
|
$aRanks = [];
|
|
foreach ($oItems->childNodes as $oItem) {
|
|
if ($oItem instanceof DOMElement) {
|
|
// When an attribute is missing
|
|
if ($oItem->hasAttribute('id')) {
|
|
$key = $oItem->getAttribute('id');
|
|
if (array_key_exists($key, $res)) {
|
|
// Houston!
|
|
$sXPath = DesignDocument::GetItopNodePath($this);
|
|
throw new DOMFormatException("id '$key' already used in $sXPath", 0, null, $oItem);
|
|
}
|
|
$res[$key] = $oItem->GetNodeAsArrayOfItems();
|
|
} else {
|
|
$res[] = $oItem->GetNodeAsArrayOfItems();
|
|
}
|
|
$sRank = $oItem->GetChildText('rank');
|
|
if ($sRank != '') {
|
|
$aRanks[] = (float)$sRank;
|
|
} else {
|
|
$aRanks[] = count($aRanks) > 0 ? max($aRanks) + 1 : 0;
|
|
}
|
|
array_multisort($aRanks, $res);
|
|
}
|
|
}
|
|
} else {
|
|
$res = $this->GetText();
|
|
}
|
|
|
|
return $res;
|
|
}
|
|
|
|
/**
|
|
* @param $oXmlDoc
|
|
* @param $oXMLNode
|
|
* @param $itemValue
|
|
*/
|
|
protected static function AddItemToNode($oXmlDoc, $oXMLNode, $itemValue)
|
|
{
|
|
if (is_array($itemValue)) {
|
|
$oXmlItems = $oXmlDoc->CreateElement('items');
|
|
$oXMLNode->appendChild($oXmlItems);
|
|
|
|
foreach ($itemValue as $key => $item) {
|
|
$oXmlItem = $oXmlDoc->CreateElement('item');
|
|
$oXmlItems->appendChild($oXmlItem);
|
|
|
|
if (is_string($key)) {
|
|
$oXmlItem->SetAttribute('key', $key);
|
|
}
|
|
self::AddItemToNode($oXmlDoc, $oXmlItem, $item);
|
|
}
|
|
} else {
|
|
$oXmlText = $oXmlDoc->CreateTextNode((string)$itemValue);
|
|
$oXMLNode->appendChild($oXmlText);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to remove child nodes
|
|
*/
|
|
protected function DeleteChildren()
|
|
{
|
|
while (isset($this->firstChild)) {
|
|
if ($this->firstChild instanceof MFElement) {
|
|
$this->firstChild->DeleteChildren();
|
|
}
|
|
$this->removeChild($this->firstChild);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is under a node 'added' or 'altered'
|
|
* Usage: In such a case, the change must not be tracked
|
|
*
|
|
* @return boolean true if `_alteration` flag is set on any parent of the current node
|
|
*/
|
|
public function IsInDefinition()
|
|
{
|
|
// Iterate through the parents: reset the flag if any of them has a flag set
|
|
for ($oParent = $this; $oParent instanceof MFElement; $oParent = $oParent->parentNode) {
|
|
if ($oParent->GetAlteration() != '') {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if the given node is (a child of a node) altered by one of the supplied modules
|
|
*
|
|
* @param array $aModules The list of module codes to consider
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function IsAlteredByModule($aModules)
|
|
{
|
|
// Iterate through the parents: reset the flag if any of them has a flag set
|
|
for ($oParent = $this; $oParent instanceof MFElement; $oParent = $oParent->parentNode) {
|
|
if (in_array($oParent->getAttribute('_altered_in'), $aModules)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected static $aTraceAttributes = null;
|
|
|
|
/**
|
|
* Enable/disable the trace on changed nodes
|
|
*
|
|
* @param array aAttributes Array of attributes (key => value) to be added onto any changed node
|
|
*/
|
|
public static function SetTrace($aAttributes = null)
|
|
{
|
|
self::$aTraceAttributes = $aAttributes;
|
|
}
|
|
|
|
/**
|
|
* Mark the node as touched (if tracing is active)
|
|
*/
|
|
public function AddTrace()
|
|
{
|
|
if (!is_null(self::$aTraceAttributes)) {
|
|
foreach (self::$aTraceAttributes as $sAttribute => $value) {
|
|
$this->setAttribute($sAttribute, $value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a node and set the flags that will be used to compute the delta
|
|
*
|
|
* @param MFElement $oNode The node (including all subnodes) to add
|
|
*
|
|
* @throws MFException
|
|
* @throws \Exception
|
|
*/
|
|
public function AddChildNode(MFElement $oNode)
|
|
{
|
|
// First: cleanup any flag behind the new node, and eventually add trace data
|
|
$oNode->ApplyChanges();
|
|
$oNode->AddTrace();
|
|
|
|
$oExisting = $this->_FindChildNode($oNode);
|
|
if ($oExisting) {
|
|
if (!$oExisting->IsRemoved()) {
|
|
$sExistingPath = MFDocument::GetItopNodePath($oExisting).' created_in: ['.$oExisting->getAttribute('_created_in').']';
|
|
$iExistingLine = ModelFactory::GetXMLLineNumber($oExisting);
|
|
$sExceptionMessage = <<<EOF
|
|
could not be added : already exists in `{$sExistingPath}` at line {$iExistingLine}
|
|
EOF;
|
|
throw new MFException($sExceptionMessage, MFException::COULD_NOT_BE_ADDED, $oNode);
|
|
}
|
|
$oExisting->ReplaceWithSingleNode($oNode);
|
|
$sFlag = 'replaced';
|
|
} else {
|
|
$this->appendChild($oNode);
|
|
$sFlag = 'added';
|
|
}
|
|
if (!$this->IsInDefinition()) {
|
|
$oNode->SetAlteration($sFlag);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Modify a node and set the flags that will be used to compute the delta
|
|
*
|
|
* @param MFElement $oNode The node (including all subnodes) to set
|
|
* @param string|null $sSearchId
|
|
* @param mixed $oParentFallbackNode: provided to print accurate line number in case $oNode line is 0
|
|
*
|
|
* @return void
|
|
*
|
|
* @throws MFException
|
|
* @throws \Exception
|
|
*/
|
|
public function RedefineChildNode(MFElement $oNode, $sSearchId = null, $oParentFallbackNode=null)
|
|
{
|
|
// First: cleanup any flag behind the new node, and eventually add trace data
|
|
$oNode->ApplyChanges();
|
|
$oNode->AddTrace();
|
|
|
|
$oExisting = $this->_FindChildNode($oNode, $sSearchId);
|
|
if (!$oExisting) {
|
|
$sPath = MFDocument::GetItopNodePath($this)."/".$oNode->tagName.(empty($sSearchId) ? '' : "[$sSearchId]");
|
|
throw new MFException('could not be modified (not found)', MFException::COULD_NOT_BE_MODIFIED_NOT_FOUND, $oNode, $sPath, $oParentFallbackNode);
|
|
}
|
|
$sPrevFlag = $oExisting->GetAlteration();
|
|
$sOldId = $oExisting->getAttribute('_old_id');
|
|
if ($oExisting->IsRemoved()) {
|
|
$sPath = MFDocument::GetItopNodePath($this)."/".$oNode->tagName.(empty($sSearchId) ? '' : "[$sSearchId]");
|
|
throw new MFException('could not be modified (marked as deleted)', MFException::COULD_NOT_BE_MODIFIED_ALREADY_DELETED, $oNode, $sPath, $oParentFallbackNode);
|
|
}
|
|
$oExisting->ReplaceWithSingleNode($oNode);
|
|
if (!$this->IsInDefinition()) {
|
|
if ($sPrevFlag == '') {
|
|
$sPrevFlag = 'replaced';
|
|
}
|
|
$oNode->SetAlteration($sPrevFlag);
|
|
if ($sOldId !== '') {
|
|
$oNode->setAttribute('_old_id', $sOldId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Combination of AddChildNode or RedefineChildNode... it depends
|
|
* This should become the preferred way of doing things (instead of implementing a test + the call to one of the APIs!
|
|
*
|
|
* @param MFElement $oNode The node (including all subnodes) to set
|
|
* @param string $sSearchId Optional Id of the node to SearchMenuNode
|
|
* @param bool $bForce Force mode to dynamically add or replace nodes
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function SetChildNode(MFElement $oNode, $sSearchId = null, $bForce = false)
|
|
{
|
|
// First: cleanup any flag behind the new node, and eventually add trace data
|
|
$oNode->ApplyChanges();
|
|
$oNode->AddTrace();
|
|
|
|
$oExisting = $this->_FindChildNode($oNode, $sSearchId);
|
|
if ($oExisting) {
|
|
$sOldId = $oExisting->getAttribute('_old_id');
|
|
if (!empty($sOldId)) {
|
|
$oNode->setAttribute('_old_id', $sOldId);
|
|
}
|
|
|
|
$sPrevFlag = $oExisting->GetAlteration();
|
|
if ($oExisting->IsRemoved()) {
|
|
$sFlag = $bForce ? 'forced' : 'replaced';
|
|
} else {
|
|
$sFlag = $sPrevFlag; // added, replaced or ''
|
|
}
|
|
$oExisting->ReplaceWithSingleNode($oNode);
|
|
} else {
|
|
$this->appendChild($oNode);
|
|
$sFlag = $bForce ? 'forced' : 'added';
|
|
}
|
|
if (!$this->IsInDefinition()) {
|
|
if ($sFlag == '') {
|
|
$sFlag = $bForce ? 'forced' : 'replaced';
|
|
}
|
|
$oNode->SetAlteration($sFlag);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Replaces a node by another one, making sure that recursive nodes are preserved
|
|
*
|
|
* @param MFElement $oNewNode The replacement
|
|
*
|
|
* @since 2.7.7 3.0.1 3.1.0 N°3129 rename method (from `ReplaceWith` to `ReplaceWithSingleNode`) to avoid collision with parent `\DOMElement::replaceWith` method (different method modifier and parameters :
|
|
* throws fatal error in PHP 8.0)
|
|
*/
|
|
protected function ReplaceWithSingleNode($oNewNode)
|
|
{
|
|
// Move the classes from the old node into the new one
|
|
if ($this->IsClassNode()) {
|
|
foreach ($this->GetNodes('class') as $oChild) {
|
|
$oNewNode->appendChild($oChild);
|
|
}
|
|
}
|
|
|
|
$oParentNode = $this->parentNode;
|
|
$oParentNode->replaceChild($oNewNode, $this);
|
|
}
|
|
|
|
/**
|
|
* Remove a node and set the flags that will be used to compute the delta
|
|
*
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function Delete(bool $bIsConditional = false)
|
|
{
|
|
switch ($this->GetAlteration()) {
|
|
case 'replaced':
|
|
$sFlag = $bIsConditional ? 'remove_needed' : 'removed';
|
|
break;
|
|
case 'added':
|
|
case 'needed':
|
|
$sFlag = null;
|
|
break;
|
|
case 'removed':
|
|
throw new Exception("Attempting to remove a deleted node: $this->tagName (id: ".$this->getAttribute('id')."");
|
|
|
|
default:
|
|
$sFlag = $bIsConditional ? 'remove_needed' : 'removed';
|
|
if ($this->IsInDefinition()) {
|
|
$sFlag = null;
|
|
break;
|
|
}
|
|
}
|
|
if ($sFlag) {
|
|
// If class move the node AFTER all the removed classes to keep the delete order
|
|
// and remain compatible with GetDelta/LoadDelta class flattening
|
|
if ($this->IsClassNode()) {
|
|
$this->parentNode->appendChild($this);
|
|
}
|
|
|
|
$this->SetAlteration($sFlag);
|
|
$this->DeleteChildren();
|
|
|
|
// Add trace data
|
|
$this->AddTrace();
|
|
} else {
|
|
// Remove the node entirely
|
|
$this->parentNode->removeChild($this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renames a node and set the flags that will be used to compute the delta
|
|
*
|
|
* @param string $sId The new id
|
|
*/
|
|
public function Rename($sId)
|
|
{
|
|
$sAlteration = $this->GetAlteration();
|
|
if (($sAlteration == 'replaced') || ($sAlteration == 'forced') || !$this->IsInDefinition()) {
|
|
$sOriginalId = $this->getAttribute('_old_id');
|
|
if ($sOriginalId == '') {
|
|
$sRenameOrig = $this->getAttribute('_rename_from');
|
|
if (empty($sRenameOrig)) {
|
|
$this->setAttribute('_old_id', $this->getAttribute('id'));
|
|
} else {
|
|
$this->setAttribute('_old_id', $sRenameOrig);
|
|
$this->removeAttribute('_rename_from');
|
|
}
|
|
} else {
|
|
if ($sOriginalId == $sId) {
|
|
$this->removeAttribute('_old_id');
|
|
}
|
|
}
|
|
}
|
|
$this->setAttribute('id', $sId);
|
|
|
|
// Leave a trace of this change
|
|
$this->AddTrace();
|
|
}
|
|
|
|
/**
|
|
* List changes below a given node (see also MFDocument::ListChanges)
|
|
*/
|
|
public function ListChanges()
|
|
{
|
|
// Note: omitting the dot will make the query be global to the whole document!!!
|
|
return $this->ownerDocument->GetNodes('.//*[@_alteration or @_old_id]', $this, false);
|
|
}
|
|
|
|
/**
|
|
* List changes below a given node (see also MFDocument::ApplyChanges)
|
|
*/
|
|
public function ApplyChanges()
|
|
{
|
|
// Note: omitting the dot will make the query be global to the whole document!!!
|
|
$oNodes = $this->ownerDocument->GetNodes('.//*[@_alteration or @_old_id or @_delta]', $this, false);
|
|
/** @var DesignElement $oNode */
|
|
foreach ($oNodes as $oNode) {
|
|
// _delta must not exist after applying changes
|
|
if ($oNode->hasAttribute('_delta')) {
|
|
$oNode->removeAttribute('_delta');
|
|
}
|
|
if ($oNode->hasAttribute('_old_id')) {
|
|
$oNode->removeAttribute('_old_id');
|
|
}
|
|
if ($oNode->HasAlteration()) {
|
|
if ($oNode->IsRemoved()) {
|
|
$oComment = ModelFactory::GetPreviousComment($oNode);
|
|
if (!is_null($oComment)) {
|
|
$oNode->parentNode->removeChild($oComment);
|
|
}
|
|
$oNode->parentNode->removeChild($oNode);
|
|
} else {
|
|
// marked as added or modified, just reset the flag
|
|
$oNode->RemoveAlteration();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function IsRemoved(): bool
|
|
{
|
|
$sAlteration = $this->GetAlteration();
|
|
return $sAlteration === 'removed' || $sAlteration === 'remove_needed';
|
|
}
|
|
|
|
public function GetAlteration(): string
|
|
{
|
|
return $this->getAttribute('_alteration');
|
|
}
|
|
|
|
public function SetAlteration(string $sAlteration)
|
|
{
|
|
return $this->setAttribute('_alteration', $sAlteration);
|
|
}
|
|
|
|
public function RemoveAlteration()
|
|
{
|
|
$this->removeAttribute('_alteration');
|
|
}
|
|
|
|
public function HasAlteration(): bool
|
|
{
|
|
return $this->hasAttribute('_alteration');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* MFDocument - formatting rules for XML input/output
|
|
*
|
|
* @package ModelFactory
|
|
*/
|
|
class MFDocument extends \Combodo\iTop\DesignDocument
|
|
{
|
|
/**
|
|
* Over loadable. Called prior to data loading.
|
|
*/
|
|
protected function Init()
|
|
{
|
|
parent::Init();
|
|
$this->registerNodeClass('DOMElement', 'MFElement');
|
|
}
|
|
|
|
/**
|
|
* Overload the standard API
|
|
*
|
|
* @param \DOMNode|null $node
|
|
* @param int $options
|
|
*
|
|
* @return string
|
|
* @throws \Exception
|
|
*/
|
|
// Return type union is not supported by PHP 7.4, we can remove the following PHP attribute and add the return type once iTop min PHP version is PHP 8.0+
|
|
#[\ReturnTypeWillChange]
|
|
public function saveXML(DOMNode $node = null, $options = 0)
|
|
{
|
|
$oRootNode = $this->firstChild;
|
|
if (!$oRootNode) {
|
|
$oRootNode = $this->createElement('itop_design'); // make sure that the document is not empty
|
|
$oRootNode->setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance");
|
|
$oRootNode->setAttribute('version', ITOP_DESIGN_LATEST_VERSION);
|
|
$this->appendChild($oRootNode);
|
|
}
|
|
|
|
return parent::saveXML($node, $options);
|
|
}
|
|
|
|
/**
|
|
* Overload createElement to make sure (via new DOMText) that the XML entities are
|
|
* always properly escaped
|
|
* (non-PHPdoc)
|
|
*
|
|
* @see DOMDocument::createElement()
|
|
*
|
|
* @param string $sName
|
|
* @param null $value
|
|
* @param string $namespaceURI
|
|
*
|
|
* @return \MFElement
|
|
* @throws \Exception
|
|
*
|
|
* @since 3.1.0 N°4517 $namespaceURI parameter must be empty string by default so
|
|
*/
|
|
public function createElement($sName, $value = null, $namespaceURI = '')
|
|
{
|
|
/** @var \MFElement $oElement */
|
|
$oElement = $this->importNode(new MFElement($sName, null, $namespaceURI));
|
|
if (($value !== '') && ($value !== null)) {
|
|
$oElement->appendChild(new DOMText($value));
|
|
}
|
|
|
|
return $oElement;
|
|
}
|
|
|
|
/**
|
|
* Find the child node matching the given node
|
|
* A method with the same signature MUST exist in MFElement for the recursion to work fine
|
|
*
|
|
* @param DesignElement $oRefNode The node to search for
|
|
* @param string $sSearchId substitutes to the value of the 'id' attribute
|
|
*
|
|
* @return DesignElement|null
|
|
* @throws \Exception
|
|
*/
|
|
public function _FindChildNode(DesignElement $oRefNode, $sSearchId = null)
|
|
{
|
|
return DesignElement::_FindNode($this, $oRefNode, $sSearchId);
|
|
}
|
|
|
|
/**
|
|
* Extracts some nodes from the DOM
|
|
*
|
|
* @param string $sXPath A XPath expression
|
|
* @param null $oContextNode
|
|
* @param bool $bSafe
|
|
*
|
|
* @return DOMNodeList
|
|
*/
|
|
public function GetNodes($sXPath, $oContextNode = null, $bSafe = true)
|
|
{
|
|
$oXPath = new DOMXPath($this);
|
|
// For Designer audit
|
|
$oXPath->registerNamespace("php", "http://php.net/xpath");
|
|
$oXPath->registerNamespace('xsi', 'http://www.w3.org/2001/XMLSchema-instance');
|
|
$oXPath->registerPhpFunctions();
|
|
|
|
if ($bSafe) {
|
|
$sXPath = "($sXPath)[not(@_alteration) or (@_alteration!='removed' and @_alteration!='remove_needed')]";
|
|
}
|
|
|
|
if (is_null($oContextNode)) {
|
|
$oResult = $oXPath->query($sXPath);
|
|
} else {
|
|
$oResult = $oXPath->query($sXPath, $oContextNode);
|
|
}
|
|
|
|
return $oResult;
|
|
}
|
|
|
|
/**
|
|
* @param string $sXPath
|
|
* @param string $sId
|
|
* @param \DOMNode $oContextNode
|
|
*
|
|
* @return \DOMNodeList
|
|
*/
|
|
public function GetNodeById($sXPath, $sId, $oContextNode = null)
|
|
{
|
|
$oXPath = new DOMXPath($this);
|
|
$sQuotedId = self::XPathQuote($sId);
|
|
$sXPath = "($sXPath)[@id=$sQuotedId and (not(@_alteration) or @_alteration!='removed' or @_alteration!='remove_needed')]";
|
|
|
|
if (is_null($oContextNode)) {
|
|
return $oXPath->query($sXPath);
|
|
} else {
|
|
return $oXPath->query($sXPath, $oContextNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper class manage parameters stored as XML nodes
|
|
* to be converted to a PHP structure during compilation
|
|
* Values can be either a hash, an array, a string, a boolean, an int or a float
|
|
*/
|
|
class MFParameters
|
|
{
|
|
protected $aData = null;
|
|
|
|
/**
|
|
* MFParameters constructor.
|
|
*
|
|
* @param \DOMNode $oNode
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function __construct(DOMNode $oNode)
|
|
{
|
|
$this->aData = [];
|
|
$this->LoadFromDOM($oNode);
|
|
}
|
|
|
|
/**
|
|
* @param $sCode
|
|
* @param string $default
|
|
*
|
|
* @return mixed|string
|
|
*/
|
|
public function Get($sCode, $default = '')
|
|
{
|
|
if (array_key_exists($sCode, $this->aData)) {
|
|
return $this->aData[$sCode];
|
|
}
|
|
|
|
return $default;
|
|
}
|
|
|
|
/**
|
|
* @return array|null
|
|
*/
|
|
public function GetAll()
|
|
{
|
|
return $this->aData;
|
|
}
|
|
|
|
/**
|
|
* @param \DOMNode $oNode
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function LoadFromDOM(DOMNode $oNode)
|
|
{
|
|
$this->aData = [];
|
|
foreach ($oNode->childNodes as $oChildNode) {
|
|
if ($oChildNode instanceof DOMElement) {
|
|
$this->aData[$oChildNode->nodeName] = $this->ReadElement($oChildNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param \DOMNode $oNode
|
|
*
|
|
* @return array|bool|int
|
|
* @throws \Exception
|
|
*/
|
|
protected function ReadElement(DOMNode $oNode)
|
|
{
|
|
$value = null;
|
|
if ($oNode instanceof DOMElement) {
|
|
$sDefaultNodeType = ($this->HasChildNodes($oNode)) ? 'hash' : 'string';
|
|
$sNodeType = $oNode->getAttribute('type');
|
|
if ($sNodeType == '') {
|
|
$sNodeType = $sDefaultNodeType;
|
|
}
|
|
|
|
switch ($sNodeType) {
|
|
case 'array':
|
|
$value = [];
|
|
// Treat the current element as zero based array, child tag names are NOT meaningful
|
|
$sFirstTagName = null;
|
|
foreach ($oNode->childNodes as $oChildElement) {
|
|
if ($oChildElement instanceof DOMElement) {
|
|
if ($sFirstTagName == null) {
|
|
$sFirstTagName = $oChildElement->nodeName;
|
|
} else {
|
|
if ($sFirstTagName != $oChildElement->nodeName) {
|
|
throw new Exception("Invalid Parameters: mixed tags ('$sFirstTagName' and '".$oChildElement->nodeName."') inside array '".$oNode->nodeName."'");
|
|
}
|
|
}
|
|
$val = $this->ReadElement($oChildElement);
|
|
// No specific Id, just push the value at the end of the array
|
|
$value[] = $val;
|
|
}
|
|
}
|
|
ksort($value, SORT_NUMERIC);
|
|
break;
|
|
|
|
case 'hash':
|
|
$value = [];
|
|
// Treat the current element as a hash, child tag names are keys
|
|
foreach ($oNode->childNodes as $oChildElement) {
|
|
if ($oChildElement instanceof DOMElement) {
|
|
if (array_key_exists($oChildElement->nodeName, $value)) {
|
|
throw new Exception("Invalid Parameters file: duplicate tags '".$oChildElement->nodeName."' inside hash '".$oNode->nodeName."'");
|
|
}
|
|
$val = $this->ReadElement($oChildElement);
|
|
$value[$oChildElement->nodeName] = $val;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'int':
|
|
case 'integer':
|
|
$value = (int)$this->GetText($oNode);
|
|
break;
|
|
|
|
case 'bool':
|
|
case 'boolean':
|
|
if (($this->GetText($oNode) == 'true') || ($this->GetText($oNode) == 1)) {
|
|
$value = true;
|
|
} else {
|
|
$value = false;
|
|
}
|
|
break;
|
|
|
|
case 'string':
|
|
default:
|
|
$value = str_replace('\n', "\n", (string)$this->GetText($oNode));
|
|
}
|
|
} else {
|
|
if ($oNode instanceof DOMText) {
|
|
$value = $oNode->wholeText;
|
|
}
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* @param $sAttName
|
|
* @param $oNode
|
|
* @param $sDefaultValue
|
|
*
|
|
* @return mixed
|
|
*/
|
|
protected function GetAttribute($sAttName, $oNode, $sDefaultValue)
|
|
{
|
|
$sRet = $sDefaultValue;
|
|
|
|
foreach ($oNode->attributes as $oAttribute) {
|
|
if ($oAttribute->nodeName == $sAttName) {
|
|
$sRet = $oAttribute->nodeValue;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $sRet;
|
|
}
|
|
|
|
/**
|
|
* Returns the TEXT of the current node (possibly from several sub nodes)
|
|
*
|
|
* @param $oNode
|
|
* @param null $sDefault
|
|
*
|
|
* @return null|string
|
|
*/
|
|
public function GetText($oNode, $sDefault = null)
|
|
{
|
|
$sText = null;
|
|
foreach ($oNode->childNodes as $oChildNode) {
|
|
if ($oChildNode instanceof DOMText) {
|
|
if (is_null($sText)) {
|
|
$sText = '';
|
|
}
|
|
$sText .= $oChildNode->wholeText;
|
|
}
|
|
}
|
|
if (is_null($sText)) {
|
|
return $sDefault;
|
|
} else {
|
|
return $sText;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a node has child nodes (apart from text nodes)
|
|
*
|
|
* @param $oNode
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function HasChildNodes($oNode)
|
|
{
|
|
if ($oNode instanceof DOMElement) {
|
|
foreach ($oNode->childNodes as $oChildNode) {
|
|
if ($oChildNode instanceof DOMElement) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param \XMLParameters $oTask
|
|
*/
|
|
public function Merge(XMLParameters $oTask)
|
|
{
|
|
//todo: clarify the usage of this function that CANNOT work
|
|
$this->aData = $this->array_merge_recursive_distinct($this->aData, $oTask->aData);
|
|
}
|
|
|
|
/**
|
|
* array_merge_recursive does indeed merge arrays, but it converts values with duplicate
|
|
* keys to arrays rather than overwriting the value in the first array with the duplicate
|
|
* value in the second array, as array_merge does. I.e., with array_merge_recursive,
|
|
* this happens (documented behavior):
|
|
*
|
|
* array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
|
|
* => array('key' => array('org value', 'new value'));
|
|
*
|
|
* array_merge_recursive_distinct does not change the data types of the values in the arrays.
|
|
* Matching keys' values in the second array overwrite those in the first array, as is the
|
|
* case with array_merge, i.e.:
|
|
*
|
|
* array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
|
|
* => array('key' => array('new value'));
|
|
*
|
|
* Parameters are passed by reference, though only for performance reasons. They're not
|
|
* altered by this function.
|
|
*
|
|
* @param array $array1
|
|
* @param array $array2
|
|
*
|
|
* @return array
|
|
* @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
|
|
* @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
|
|
*/
|
|
protected function array_merge_recursive_distinct(array &$array1, array &$array2)
|
|
{
|
|
$merged = $array1;
|
|
|
|
foreach ($array2 as $key => &$value) {
|
|
if (is_array($value) && isset($merged [$key]) && is_array($merged [$key])) {
|
|
$merged [$key] = $this->array_merge_recursive_distinct($merged [$key], $value);
|
|
} else {
|
|
$merged [$key] = $value;
|
|
}
|
|
}
|
|
|
|
return $merged;
|
|
}
|
|
}
|