From 2f30a0146eeb927d51ccbb5d6cad44115cb76f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Espi=C3=A9?= Date: Tue, 19 Mar 2024 13:53:14 +0100 Subject: [PATCH] =?UTF-8?q?N=C2=B06974=20-=20Flatten=20classes=20in=20data?= =?UTF-8?q?model=20(#603)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * N°6974 - Flatten classes in datamodel * N°6882 - LoadDelta() in lax/strict mode * N°6974 - Flatten classes in datamodel * N°6882 - LoadDelta() in lax/strict mode * N°7186 - Code hardening * N°6974 - Flatten classes in datamodel * N°6660 and N°7318 - Support delete_if_exists and define_if_not_exists in XML injection * N°6974 - Flatten classes in datamodel * Apply suggestions from code review * Update core/designdocument.class.inc.php --------- Co-authored-by: Molkobain --- core/designdocument.class.inc.php | 145 +- js/icon_select.js | 12 +- lib/autoload.php | 18 - lib/composer/ClassLoader.php | 139 +- lib/composer/autoload_classmap.php | 2 +- lib/composer/autoload_files.php | 12 +- lib/composer/autoload_namespaces.php | 2 +- lib/composer/autoload_psr4.php | 2 +- lib/composer/autoload_real.php | 41 +- lib/composer/autoload_static.php | 10 +- lib/composer/include_paths.php | 2 +- setup/itopdesignformat.class.inc.php | 4 +- setup/modelfactory.class.inc.php | 950 ++--- .../unitary-tests/setup/ModelFactoryTest.php | 3059 +++++++++++++---- .../Convert-samples/1.7_to_3.0.expected.xml | 3 + .../Convert-samples/1.7_to_3.0.input.xml | 1 + 16 files changed, 3223 insertions(+), 1179 deletions(-) diff --git a/core/designdocument.class.inc.php b/core/designdocument.class.inc.php index df3dea7bd..e04c0fb54 100644 --- a/core/designdocument.class.inc.php +++ b/core/designdocument.class.inc.php @@ -26,10 +26,18 @@ namespace Combodo\iTop; +use DOMComment; use DOMDocument; use DOMFormatException; +use DOMNode; +use DOMNodeList; +use DOMXPath; +use Exception; use IssueLog; use LogAPI; +use MFDocument; +use MFElement; +use ModelFactory; use utils; /** @@ -41,6 +49,11 @@ use utils; */ class DesignDocument extends DOMDocument { + + /** To fix DOMNode::getLineNo() ref https://www.php.net/manual/en/domnode.getlineno.php */ + public const XML_PARSE_BIG_LINES = 4194304; + + /** * @throws \Exception */ @@ -69,10 +82,12 @@ class DesignDocument extends DOMDocument */ public function load($filename, $options = null) { - libxml_clear_errors(); - if (parent::load($filename, LIBXML_NOBLANKS) === false) { - $aErrors = libxml_get_errors(); - IssueLog::Error("Error loading $filename", LogAPI::CHANNEL_DEFAULT, $aErrors); + if (is_file($filename)) { + libxml_clear_errors(); + if (parent::load($filename, LIBXML_NOBLANKS | LIBXML_BIGLINES | LIBXML_PARSEHUGE | self::XML_PARSE_BIG_LINES) === false) { + $aErrors = libxml_get_errors(); + IssueLog::Error("Error loading $filename", LogAPI::CHANNEL_DEFAULT, $aErrors); + } } } @@ -307,4 +322,126 @@ class DesignElement extends \DOMElement } return $sRet; } + + /** + * Check that the current node is actually a class node, under classes + * @since 3.1.2 3.2.0 N°6974 + */ + public function IsClassNode(): bool + { + if ($this->tagName == 'class') { + // Beware: classes/class also exists in the group definition + if (($this->parentNode->tagName == 'classes') && ($this->parentNode->parentNode->tagName == 'itop_design')) { + return true; + } + } + + return false; + } + + /** + * Find the child node matching the given node. + * UNSAFE: may return nodes marked as _alteration="removed" + * A method with the same signature MUST exist in MFDocument for the recursion to work fine + * + * @param DesignElement $oRefNode The node to search for + * @param null|string $sSearchId substitutes to the value of the 'id' attribute + * + * @return DesignElement|null + * @throws \Exception + * @since 3.1.2 3.2.0 N°6974 + */ + public function _FindChildNode(DesignElement $oRefNode, $sSearchId = null): ?DesignElement + { + return self::_FindNode($this, $oRefNode, $sSearchId); + } + + + /** + * Find the child node matching the given node. + * UNSAFE: may return nodes marked as _alteration="removed" + * A method with the same signature MUST exist in MFDocument for the recursion to work fine + * + * @param DesignElement $oRefNode The node to search for + * + * @return DesignElement|null + * @throws \Exception + * @since 3.1.2 3.2.0 N°6974 + */ + public function _FindChildNodes(DesignElement $oRefNode): ?DesignElement + { + return self::_FindNodes($this, $oRefNode); + } + + /** + * Find the child node matching the given node under the specified parent. + * UNSAFE: may return nodes marked as _alteration="removed" + * + * @param \DOMNode $oParent + * @param DesignElement $oRefNode + * @param string|null $sSearchId + * + * @return DesignElement|null + * @throws Exception + * @since 3.1.2 3.2.0 N°6974 + */ + public static function _FindNode(DOMNode $oParent, DesignElement $oRefNode, string $sSearchId = null): ?DesignElement + { + $oNodes = self::_FindNodes($oParent, $oRefNode, $sSearchId); + if ($oNodes instanceof DOMNodeList) { + /** @var DesignElement $oNode */ + $oNode = $oNodes->item(0); + + return $oNode; + } + + return null; + } + + /** + * Find the child node matching the given node under the specified parent. + * UNSAFE: may return nodes marked as _alteration="removed" + * + * @param \DOMNode $oParent + * @param DesignElement $oRefNode + * @param string|null $sSearchId + * + * @return \DOMNodeList|false|mixed + * @since 3.1.2 3.2.0 N°6974 + */ + public static function _FindNodes(DOMNode $oParent, DesignElement $oRefNode, string $sSearchId = null) + { + if ($oParent instanceof DOMDocument) + { + $oDoc = $oParent->firstChild->ownerDocument; + $oRoot = $oParent; + } + else + { + $oDoc = $oParent->ownerDocument; + $oRoot = $oParent; + } + + $oXPath = new DOMXPath($oDoc); + if ($oRefNode->hasAttribute('id')) + { + // Find the elements having the same tag name and id + if (!$sSearchId) + { + $sSearchId = $oRefNode->getAttribute('id'); + } + $sXPath = './'.$oRefNode->tagName."[@id='$sSearchId']"; + + $oRes = $oXPath->query($sXPath, $oRoot); + } + else + { + // Get the elements having the same tag name + $sXPath = './'.$oRefNode->tagName; + + $oRes = $oXPath->query($sXPath, $oRoot); + } + + return $oRes; + } } diff --git a/js/icon_select.js b/js/icon_select.js index b8fa945b5..0afd9bf48 100644 --- a/js/icon_select.js +++ b/js/icon_select.js @@ -214,7 +214,7 @@ $(function() _upload_dlg: function() { var me = this; - this.oUploadDlg = $('

'+this.options.labels['pick_icon_file']+'

'); + this.oUploadDlg = $('

'+this.options.labels['pick_icon_file']+'

'); this.element.after(this.oUploadDlg); $('input[type=file]').bind('change', function() { me._do_upload(); }); this.oUploadDlg.dialog({ @@ -281,13 +281,13 @@ $(function() }, _on_upload_error: function(data, status, e) { - if(data.responseText.indexOf('login-body') !== false) - { + if (data.responseText.indexOf('login-body') !== -1) { alert('Sorry, your session has expired. In order to continue, the whole page has to be loaded again.'); this.oUploadDlg.dialog('close'); - } - else - { + } else if (data.responseText.length > 0) { + alert(data.responseText); + this.oUploadDlg.dialog('close'); + } else { alert(e); this.oUploadDlg.closest('.ui-dialog').find('.ui-button').button('enable'); } diff --git a/lib/autoload.php b/lib/autoload.php index db10dc867..460e67535 100644 --- a/lib/autoload.php +++ b/lib/autoload.php @@ -2,24 +2,6 @@ // autoload.php @generated by Composer -if (PHP_VERSION_ID < 50600) { - if (!headers_sent()) { - header('HTTP/1.1 500 Internal Server Error'); - } - $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; - if (!ini_get('display_errors')) { - if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { - fwrite(STDERR, $err); - } elseif (!headers_sent()) { - echo $err; - } - } - trigger_error( - $err, - E_USER_ERROR - ); -} - require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInit7f81b4a2a468a061c306af5e447a9a9f::getLoader(); diff --git a/lib/composer/ClassLoader.php b/lib/composer/ClassLoader.php index 7824d8f7e..0cd6055d1 100644 --- a/lib/composer/ClassLoader.php +++ b/lib/composer/ClassLoader.php @@ -42,37 +42,35 @@ namespace Composer\Autoload; */ class ClassLoader { - /** @var \Closure(string):void */ - private static $includeFile; - - /** @var string|null */ + /** @var ?string */ private $vendorDir; // PSR-4 /** - * @var array> + * @var array[] + * @psalm-var array> */ private $prefixLengthsPsr4 = array(); /** - * @var array> + * @var array[] + * @psalm-var array> */ private $prefixDirsPsr4 = array(); /** - * @var list + * @var array[] + * @psalm-var array */ private $fallbackDirsPsr4 = array(); // PSR-0 /** - * List of PSR-0 prefixes - * - * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) - * - * @var array>> + * @var array[] + * @psalm-var array> */ private $prefixesPsr0 = array(); /** - * @var list + * @var array[] + * @psalm-var array */ private $fallbackDirsPsr0 = array(); @@ -80,7 +78,8 @@ class ClassLoader private $useIncludePath = false; /** - * @var array + * @var string[] + * @psalm-var array */ private $classMap = array(); @@ -88,29 +87,29 @@ class ClassLoader private $classMapAuthoritative = false; /** - * @var array + * @var bool[] + * @psalm-var array */ private $missingClasses = array(); - /** @var string|null */ + /** @var ?string */ private $apcuPrefix; /** - * @var array + * @var self[] */ private static $registeredLoaders = array(); /** - * @param string|null $vendorDir + * @param ?string $vendorDir */ public function __construct($vendorDir = null) { $this->vendorDir = $vendorDir; - self::initializeIncludeClosure(); } /** - * @return array> + * @return string[] */ public function getPrefixes() { @@ -122,7 +121,8 @@ class ClassLoader } /** - * @return array> + * @return array[] + * @psalm-return array> */ public function getPrefixesPsr4() { @@ -130,7 +130,8 @@ class ClassLoader } /** - * @return list + * @return array[] + * @psalm-return array */ public function getFallbackDirs() { @@ -138,7 +139,8 @@ class ClassLoader } /** - * @return list + * @return array[] + * @psalm-return array */ public function getFallbackDirsPsr4() { @@ -146,7 +148,8 @@ class ClassLoader } /** - * @return array Array of classname => path + * @return string[] Array of classname => path + * @psalm-var array */ public function getClassMap() { @@ -154,7 +157,8 @@ class ClassLoader } /** - * @param array $classMap Class to filename map + * @param string[] $classMap Class to filename map + * @psalm-param array $classMap * * @return void */ @@ -171,25 +175,24 @@ class ClassLoader * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * - * @param string $prefix The prefix - * @param list|string $paths The PSR-0 root directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { - $paths = (array) $paths; if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( - $paths, + (array) $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, - $paths + (array) $paths ); } @@ -198,19 +201,19 @@ class ClassLoader $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { - $this->prefixesPsr0[$first][$prefix] = $paths; + $this->prefixesPsr0[$first][$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( - $paths, + (array) $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], - $paths + (array) $paths ); } } @@ -219,9 +222,9 @@ class ClassLoader * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param list|string $paths The PSR-4 base directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * @@ -229,18 +232,17 @@ class ClassLoader */ public function addPsr4($prefix, $paths, $prepend = false) { - $paths = (array) $paths; if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( - $paths, + (array) $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, - $paths + (array) $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { @@ -250,18 +252,18 @@ class ClassLoader throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = $paths; + $this->prefixDirsPsr4[$prefix] = (array) $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( - $paths, + (array) $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], - $paths + (array) $paths ); } } @@ -270,8 +272,8 @@ class ClassLoader * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * - * @param string $prefix The prefix - * @param list|string $paths The PSR-0 base directories + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 base directories * * @return void */ @@ -288,8 +290,8 @@ class ClassLoader * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param list|string $paths The PSR-4 base directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * @@ -423,8 +425,7 @@ class ClassLoader public function loadClass($class) { if ($file = $this->findFile($class)) { - $includeFile = self::$includeFile; - $includeFile($file); + includeFile($file); return true; } @@ -475,9 +476,9 @@ class ClassLoader } /** - * Returns the currently registered loaders keyed by their corresponding vendor directories. + * Returns the currently registered loaders indexed by their corresponding vendor directories. * - * @return array + * @return self[] */ public static function getRegisteredLoaders() { @@ -554,26 +555,18 @@ class ClassLoader return false; } - - /** - * @return void - */ - private static function initializeIncludeClosure() - { - if (self::$includeFile !== null) { - return; - } - - /** - * Scope isolated include. - * - * Prevents access to $this/self from included files. - * - * @param string $file - * @return void - */ - self::$includeFile = \Closure::bind(static function($file) { - include $file; - }, null, null); - } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + * @private + */ +function includeFile($file) +{ + include $file; } diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 93f7e5a20..da1772061 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -2,7 +2,7 @@ // autoload_classmap.php @generated by Composer -$vendorDir = dirname(__DIR__); +$vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( diff --git a/lib/composer/autoload_files.php b/lib/composer/autoload_files.php index 50c5aa314..2dfdef1d3 100644 --- a/lib/composer/autoload_files.php +++ b/lib/composer/autoload_files.php @@ -2,23 +2,23 @@ // autoload_files.php @generated by Composer -$vendorDir = dirname(__DIR__); +$vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( - 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php', + 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', - '23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php', '0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php', + '23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php', '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php', - '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', 'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php', - '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', 'c9d07b32a2e02bc0fc582d4f0c1b56cc' => $vendorDir . '/laminas/laminas-servicemanager/src/autoload.php', + '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', '8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php', + 'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php', + '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', '25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php', 'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php', - 'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php', ); diff --git a/lib/composer/autoload_namespaces.php b/lib/composer/autoload_namespaces.php index 6629b7e09..1db5bf646 100644 --- a/lib/composer/autoload_namespaces.php +++ b/lib/composer/autoload_namespaces.php @@ -2,7 +2,7 @@ // autoload_namespaces.php @generated by Composer -$vendorDir = dirname(__DIR__); +$vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( diff --git a/lib/composer/autoload_psr4.php b/lib/composer/autoload_psr4.php index 3b30be01d..eb2c95ace 100644 --- a/lib/composer/autoload_psr4.php +++ b/lib/composer/autoload_psr4.php @@ -2,7 +2,7 @@ // autoload_psr4.php @generated by Composer -$vendorDir = dirname(__DIR__); +$vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( diff --git a/lib/composer/autoload_real.php b/lib/composer/autoload_real.php index 671821bc0..cc554d8d1 100644 --- a/lib/composer/autoload_real.php +++ b/lib/composer/autoload_real.php @@ -25,31 +25,46 @@ class ComposerAutoloaderInit7f81b4a2a468a061c306af5e447a9a9f require __DIR__ . '/platform_check.php'; spl_autoload_register(array('ComposerAutoloaderInit7f81b4a2a468a061c306af5e447a9a9f', 'loadClassLoader'), true, true); - self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__))); spl_autoload_unregister(array('ComposerAutoloaderInit7f81b4a2a468a061c306af5e447a9a9f', 'loadClassLoader')); $includePaths = require __DIR__ . '/include_paths.php'; $includePaths[] = get_include_path(); set_include_path(implode(PATH_SEPARATOR, $includePaths)); - require __DIR__ . '/autoload_static.php'; - call_user_func(\Composer\Autoload\ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f::getInitializer($loader)); + $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f::getInitializer($loader)); + } else { + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } $loader->setClassMapAuthoritative(true); $loader->register(true); - $filesToLoad = \Composer\Autoload\ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f::$files; - $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { - if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { - $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; - - require $file; - } - }, null, null); - foreach ($filesToLoad as $fileIdentifier => $file) { - $requireFile($fileIdentifier, $file); + if ($useStaticLoader) { + $includeFiles = Composer\Autoload\ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f::$files; + } else { + $includeFiles = require __DIR__ . '/autoload_files.php'; + } + foreach ($includeFiles as $fileIdentifier => $file) { + composerRequire7f81b4a2a468a061c306af5e447a9a9f($fileIdentifier, $file); } return $loader; } } + +function composerRequire7f81b4a2a468a061c306af5e447a9a9f($fileIdentifier, $file) +{ + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + require $file; + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + } +} diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index f42c3bfef..5beb2c75f 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -7,21 +7,21 @@ namespace Composer\Autoload; class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f { public static $files = array ( - 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', + 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', - '23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php', '0d59ee240a4cd96ddbb4ff164fccea4d' => __DIR__ . '/..' . '/symfony/polyfill-php73/bootstrap.php', + '23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php', '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php', - '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', 'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php', - '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', 'c9d07b32a2e02bc0fc582d4f0c1b56cc' => __DIR__ . '/..' . '/laminas/laminas-servicemanager/src/autoload.php', + '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', '8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php', + 'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php', + '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', '25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php', 'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php', - 'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php', ); public static $prefixLengthsPsr4 = array ( diff --git a/lib/composer/include_paths.php b/lib/composer/include_paths.php index af33c1491..d4fb96718 100644 --- a/lib/composer/include_paths.php +++ b/lib/composer/include_paths.php @@ -2,7 +2,7 @@ // include_paths.php @generated by Composer -$vendorDir = dirname(__DIR__); +$vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( diff --git a/setup/itopdesignformat.class.inc.php b/setup/itopdesignformat.class.inc.php index 29ad2f3ee..d940ec84f 100644 --- a/setup/itopdesignformat.class.inc.php +++ b/setup/itopdesignformat.class.inc.php @@ -895,7 +895,9 @@ class iTopDesignFormat // N°6562 textContent is readonly, see https://www.php.net/manual/en/class.domnode.php#95545 // $oNode->textContent = ''; // N°6562 to update text node content we must use the node methods ! - $oNode->removeChild($oNode->firstChild); + if ($oNode->firstChild) { + $oNode->removeChild($oNode->firstChild); + } $oCodeNode = $oNode->ownerDocument->createElement("code", $sCode); $oNode->appendChild($oCodeNode); } diff --git a/setup/modelfactory.class.inc.php b/setup/modelfactory.class.inc.php index afc7aa5c0..573f19b26 100644 --- a/setup/modelfactory.class.inc.php +++ b/setup/modelfactory.class.inc.php @@ -21,6 +21,9 @@ * 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'); @@ -36,6 +39,13 @@ class MFException extends Exception * @var integer */ protected $iSourceLineNumber; + + /** + * Used when editing partial xml delta + * @var integer + */ + protected $iSourceLineOffset; + /** * @var string */ @@ -53,7 +63,7 @@ class MFException extends Exception const ALREADY_DELETED = 6; const NOT_FOUND = 7; const PARENT_NOT_FOUND = 8; - + const AMBIGUOUS_LEAF = 9; /** * MFException constructor. @@ -64,6 +74,7 @@ class MFException extends Exception { parent::__construct($message, $code, $previous); $this->iSourceLineNumber = $iSourceLineNumber; + $this->iSourceLineOffset = 0; $this->sXPath = $sXPath; $this->sExtraInfo = $sExtraInfo; } @@ -75,7 +86,7 @@ class MFException extends Exception */ public function GetSourceLineNumber() { - return $this->iSourceLineNumber; + return $this->iSourceLineNumber - $this->iSourceLineOffset; } /** @@ -97,6 +108,11 @@ class MFException extends Exception { return $this->sExtraInfo; } + + public function SetSourceLineOffset(int $iSourceLineOffset): void + { + $this->iSourceLineOffset = $iSourceLineOffset; + } } /** @@ -163,7 +179,7 @@ class MFModule { $this->sId = $sId; - list($this->sName, $this->sVersion) = ModuleDiscovery::GetModuleName($sId); + [$this->sName, $this->sVersion] = ModuleDiscovery::GetModuleName($sId); if (strlen($this->sVersion) == 0) { $this->sVersion = '1.0.0'; @@ -534,15 +550,12 @@ class ModelFactory */ 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 $oModules; - protected $oClasses; - protected $oMenus; - protected $oMeta; - protected $oDictionaries; - static protected $aLoadedClasses; static protected $aWellKnownParents = array('DBObject', 'CMDBObject', 'cmdbAbstractObject'); static protected $aLoadedModules; static protected $aLoadErrors; @@ -567,30 +580,30 @@ class ModelFactory $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); - $this->oModules = $this->oDOMDocument->CreateElement('loaded_modules'); - $this->oRoot->AppendChild($this->oModules); - $this->oClasses = $this->oDOMDocument->CreateElement('classes'); - $this->oRoot->AppendChild($this->oClasses); - $this->oDictionaries = $this->oDOMDocument->CreateElement('dictionaries'); - $this->oRoot->AppendChild($this->oDictionaries); + $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($sWellKnownParent); + $this->AddWellKnownParent($oClasses, $sWellKnownParent); } - $this->oMenus = $this->oDOMDocument->CreateElement('menus'); - $this->oRoot->AppendChild($this->oMenus); + $oMenus = $this->oDOMDocument->CreateElement('menus'); + $this->oRoot->appendChild($oMenus); - $this->oMeta = $this->oDOMDocument->CreateElement('meta'); - $this->oRoot->AppendChild($this->oMeta); - $this->oMeta = $this->oDOMDocument->CreateElement('events'); - $this->oRoot->AppendChild($this->oMeta); + $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); + $this->oRoot->appendChild($oElement); } self::$aLoadedModules = array(); self::$aLoadErrors = array(); @@ -623,9 +636,9 @@ class ModelFactory $this->oDOMDocument->load($sCacheFile); $this->oRoot = $this->oDOMDocument->firstChild; - $this->oModules = $this->oRoot->getElementsByTagName('loaded_modules')->item(0); + $oModules = $this->oRoot->getElementsByTagName('loaded_modules')->item(0); self::$aLoadedModules = array(); - foreach ($this->oModules->getElementsByTagName('module') as $oModuleNode) + foreach ($oModules->getElementsByTagName('module') as $oModuleNode) { $sId = $oModuleNode->getAttribute('id'); $sRootDir = $oModuleNode->GetChildText('root_dir'); @@ -645,149 +658,328 @@ class ModelFactory /** * To progressively replace LoadModule * - * @param \MFElement $oSourceNode + * @param DesignElement $oSourceNode * @param \MFDocument|\MFElement $oTargetParentNode * * @throws \MFException * @throws \DOMFormatException * @throws \Exception */ - public function LoadDelta($oSourceNode, $oTargetParentNode) + public function LoadDelta($oSourceNode, $oTargetParentNode, string $sMode = self::LOAD_DELTA_MODE_LAX) { - if (!$oSourceNode instanceof DOMElement) - { + if (!$oSourceNode instanceof DOMElement) { return; } - //echo "Load $oSourceNode->tagName::".$oSourceNode->getAttribute('id')." (".$oSourceNode->getAttribute('_delta').")
\n"; - $oTarget = $this->oDOMDocument; + if ($oTargetParentNode instanceof MFDocument) { + $oTargetDocument = $oTargetParentNode; + } else { + $oTargetDocument = $oTargetParentNode->ownerDocument; + } - $sDeltaSpec = $oSourceNode->getAttribute('_delta'); - if (($oSourceNode->tagName == 'class') && ($oSourceNode->parentNode->tagName == 'classes') && ($oSourceNode->parentNode->parentNode->tagName == 'itop_design')) - { - $sParentId = $oSourceNode->GetChildText('parent'); - if (($sDeltaSpec == 'define') || ($sDeltaSpec == 'force')) - { - // This tag is organized in hierarchy: determine the real parent node (as a subnode of the current node) - $oTargetParentNode = $oTarget->GetNodeById('/itop_design/classes//class', $sParentId)->item(0); - - if (!$oTargetParentNode) - { - // echo "Dumping target doc - looking for '$sParentId'
\n"; - // $this->oDOMDocument->firstChild->Dump(); - $sPath = MFDocument::GetItopNodePath($oSourceNode); - $iLine = $oSourceNode->getLineNo(); - throw new MFException($sPath.' at line '.$iLine.": parent class '$sParentId' could not be found", - MFException::PARENT_NOT_FOUND, $iLine, $sPath, $sParentId); + 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; } } - else - { - $oTargetNode = $oTarget->GetNodeById('/itop_design/classes//class', $oSourceNode->getAttribute('id'))->item(0); - if (!$oTargetNode) - { - if ($sDeltaSpec === 'if_exists') - { - // Just ignore it - } - else - { - // echo "Dumping target doc - looking for '".$oSourceNode->getAttribute('id')."'
\n"; - // $this->oDOMDocument->firstChild->Dump(); - $sPath = MFDocument::GetItopNodePath($oSourceNode); - $iLine = $oSourceNode->getLineNo(); - throw new MFException($sPath.' at line '.$iLine.": could not be found", MFException::NOT_FOUND, $iLine, $sPath); + $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 nodes and copy them under the target 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 + $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); + } } } - else - { - $oTargetParentNode = $oTargetNode->parentNode; - if (($sDeltaSpec == 'redefine') && ($oTargetParentNode->getAttribute('id') != $sParentId)) - { - // A class that has moved <=> deletion and creation elsewhere - $oTargetParentNode = $oTarget->GetNodeById('/itop_design/classes//class', $sParentId)->item(0); - $oTargetNode->Delete(); - $oSourceNode->setAttribute('_delta', 'define'); - $sDeltaSpec = 'define'; - } - } - } } + 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; + $iLine = ModelFactory::GetXMLLineNumber($oParentNode); + $sItopNodePath = DesignDocument::GetItopNodePath($oParentNode); + throw new MFException("$sItopNodePath at line $iLine: _delta=\"$sParentDeltaSpec\" not supported for classes in hierarchy", + MFException::NOT_FOUND, $iLine, $sItopNodePath); + } + } + + /** + * @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)) { + $iLine = ModelFactory::GetXMLLineNumber($oSourceParentClassNode); + $sItopNodePath = DesignDocument::GetItopNodePath($oSourceParentClassNode); + throw new MFException($sItopNodePath." at line $iLine: invalid parent class '$sParentClassName'", + MFException::NOT_FOUND, $iLine, $sItopNodePath); + } + $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'); - $sSearchId = $oSourceNode->hasAttribute('_rename_from') ? $oSourceNode->getAttribute('_rename_from') : $oSourceNode->getAttribute('id'); - $oTargetNode = $oSourceNode->MergeInto($oTargetParentNode, $sSearchId, $bMustExist, $bIfExists); + $bMustExist = ($sDeltaSpec === 'must_exist'); + $bIfExists = ($sDeltaSpec === 'if_exists'); + + /** @var MFElement $oTargetNode */ + $oTargetNode = $oTargetParentNode->_FindChildNode($oSourceNode, $sSearchId); + if (!$oTargetNode || $oTargetNode->IsRemoved()) { + // The node does not exist or is marked as removed + if ($bMustExist) { + $iLine = ModelFactory::GetXMLLineNumber($oSourceNode); + $sItopNodePath = DesignDocument::GetItopNodePath($oSourceNode); + throw new MFException($sItopNodePath.' at line '.$iLine.': could not be found or marked as removed', + MFException::NOT_FOUND, $iLine, $sItopNodePath); + } + if ($bIfExists) { + // Do not continue deeper + $oTargetNode = null; + } else { + if ($sMode === self::LOAD_DELTA_MODE_STRICT && ($sSearchId !== '' || is_null($oSourceNode->firstElementChild))) { + $iLine = ModelFactory::GetXMLLineNumber($oSourceNode); + $sItopNodePath = DesignDocument::GetItopNodePath($oSourceNode); + throw new MFException($sItopNodePath.' at line '.$iLine.': could not be found or marked as removed (strict mode)', + MFException::NOT_FOUND, $iLine, $sItopNodePath, '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 and continue deeper + $oTargetNode = $oTargetDocument->importNode($oSourceNode, false); + foreach ($oSourceNode->attributes as $oAttributeNode) { + $oTargetNode->setAttribute($oAttributeNode->name, $oAttributeNode->value); + } + if ($sSearchId !== '') { + // 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->firstElementChild; !is_null($oSourceChild); $oSourceChild = $oSourceChild->nextElementSibling) { + $this->LoadFlattenDelta($oSourceChild, $oTargetDocument, $oTargetNode, $sMode); + } + $oTargetNode = null; + } + } + } + if ($oTargetNode) { - foreach ($oSourceNode->childNodes as $oSourceChild) { - // Continue deeper - $this->LoadDelta($oSourceChild, $oTargetNode); + if (is_null($oSourceNode->firstElementChild) && $oTargetParentNode instanceof MFElement) { + // Leaf node + if ($sMode === self::LOAD_DELTA_MODE_STRICT && !$oSourceNode->hasAttribute('_rename_from') && trim($oSourceNode->GetText('')) !== '') { + $iLine = ModelFactory::GetXMLLineNumber($oSourceNode); + $sItopNodePath = DesignDocument::GetItopNodePath($oSourceNode); + throw new MFException($sItopNodePath.' at line '.$iLine.': cannot be modified without _delta flag (strict mode)', + MFException::AMBIGUOUS_LEAF, $iLine, $sItopNodePath, '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); + } + } + } else { + for ($oSourceChild = $oSourceNode->firstElementChild; !is_null($oSourceChild); $oSourceChild = $oSourceChild->nextElementSibling) { + // Continue deeper + $this->LoadFlattenDelta($oSourceChild, $oTargetDocument, $oTargetNode, $sMode); + } } } break; case 'define_if_not_exists': - $oExistingNode = $oTargetParentNode->_FindChildNode($oSourceNode); - if (($oExistingNode == null) || ($oExistingNode->getAttribute('_alteration') == 'removed')) - { + $oExistingNode = $oTargetParentNode->_FindChildNode($oSourceNode, $sSearchId); + if (($oExistingNode == null) || ($oExistingNode->IsRemoved())) { // Same as 'define' below - $oTargetNode = $oTarget->ImportNode($oSourceNode, true); + /** @var \MFElement $oTargetNode */ + $oTargetNode = $oTargetDocument->importNode($oSourceNode, true); $oTargetParentNode->AddChildNode($oTargetNode); - } - else - { + $oTargetNode->SetAlteration('needed'); + } else { $oTargetNode = $oExistingNode; } - $oTargetNode->setAttribute('_alteration', 'needed'); break; case 'define': // New node - copy child nodes as well - $oTargetNode = $oTarget->ImportNode($oSourceNode, true); + /** @var \MFElement $oTargetNode */ + $oTargetNode = $oTargetDocument->importNode($oSourceNode, true); $oTargetParentNode->AddChildNode($oTargetNode); break; case 'force': // Force node - copy child nodes as well - $oTargetNode = $oTarget->ImportNode($oSourceNode, true); - $oTargetParentNode->SetChildNode($oTargetNode, null, true); + /** @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 - $oTargetNode = $oTarget->ImportNode($oSourceNode, true); - $sSearchId = $oSourceNode->hasAttribute('_rename_from') ? $oSourceNode->getAttribute('_rename_from') : $oSourceNode->getAttribute('id'); + /** @var \MFElement $oTargetNode */ + $oTargetNode = $oTargetDocument->importNode($oSourceNode, true); $oTargetParentNode->RedefineChildNode($oTargetNode, $sSearchId); break; case 'delete_if_exists': - $oTargetNode = $oTargetParentNode->_FindChildNode($oSourceNode); - if (($oTargetNode !== null) && ($oTargetNode->getAttribute('_alteration') !== 'removed')) - { + /** @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(); + $oTargetNode->Delete(true); } // otherwise fail silently break; case 'delete': - $oTargetNode = $oTargetParentNode->_FindChildNode($oSourceNode); + /** @var \MFElement $oTargetNode */ + $oTargetNode = $oTargetParentNode->_FindChildNode($oSourceNode, $sSearchId); $sPath = MFDocument::GetItopNodePath($oSourceNode); - $iLine = $oSourceNode->getLineNo(); - if ($oTargetNode == null) - { + $iLine = $this->GetXMLLineNumber($oSourceNode); + + if ($oTargetNode == null) { throw new MFException($sPath.' at line '.$iLine.": could not be deleted (not found)", MFException::COULD_NOT_BE_DELETED, $iLine, $sPath); } - if ($oTargetNode->getAttribute('_alteration') == 'removed') - { + if ($oTargetNode->IsRemoved()) { throw new MFException($sPath.' at line '.$iLine.": could not be deleted (already marked as deleted)", MFException::ALREADY_DELETED, $iLine, $sPath); } @@ -796,24 +988,84 @@ class ModelFactory default: $sPath = MFDocument::GetItopNodePath($oSourceNode); - $iLine = $oSourceNode->getLineNo(); + $iLine = $this->GetXMLLineNumber($oSourceNode); throw new MFException($sPath.' at line '.$iLine.": unexpected value for attribute _delta: '".$sDeltaSpec."'", MFException::INVALID_DELTA, $iLine, $sPath, $sDeltaSpec); } - if ($oTargetNode) - { - if ($oSourceNode->hasAttribute('_rename_from')) - { + if ($oTargetNode && $oTargetNode->parentNode) { + if ($oSourceNode->hasAttribute('_rename_from')) { $oTargetNode->Rename($oSourceNode->getAttribute('id')); } - if ($oTargetNode->hasAttribute('_delta')) - { + 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->remove(); + } + } + + /** + * 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 * @@ -833,10 +1085,11 @@ class ModelFactory // 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())); + $oModuleNode->appendChild($this->oDOMDocument->CreateElement('root_dir', $oModule->GetRootDir())); + $oModuleNode->appendChild($this->oDOMDocument->CreateElement('label', $oModule->GetLabel())); - $this->oModules->AppendChild($oModuleNode); + $oModules = $this->oRoot->getElementsByTagName('loaded_modules')->item(0); + $oModules->appendChild($oModuleNode); foreach ($aDataModels as $sXmlFile) { @@ -924,7 +1177,7 @@ class ModelFactory $sDictFileContents = str_replace('Dict::Add', '$this->AddToTempDictionary', $sDictFileContents); eval($sDictFileContents); } - + $oDictionaries = $this->oRoot->getElementsByTagName('dictionaries')->item(0); foreach ($this->aDict as $sLanguageCode => $aDictDefinition) { if ((count($aLanguages) > 0) && !in_array($sLanguageCode, $aLanguages)) @@ -933,19 +1186,19 @@ class ModelFactory continue; } - $oNodes = $this->GetNodeById('dictionary', $sLanguageCode, $this->oDictionaries); + $oNodes = $this->GetNodeById('dictionary', $sLanguageCode, $oDictionaries); if ($oNodes->length == 0) { $oXmlDict = $this->oDOMDocument->CreateElement('dictionary'); $oXmlDict->setAttribute('id', $sLanguageCode); - $this->oDictionaries->AddChildNode($oXmlDict); + $oDictionaries->AddChildNode($oXmlDict); $oXmlEntries = $this->oDOMDocument->CreateElement('english_description', $aDictDefinition['english_description']); - $oXmlDict->AppendChild($oXmlEntries); + $oXmlDict->appendChild($oXmlEntries); $oXmlEntries = $this->oDOMDocument->CreateElement('localized_description', $aDictDefinition['localized_description']); - $oXmlDict->AppendChild($oXmlEntries); + $oXmlDict->appendChild($oXmlEntries); $oXmlEntries = $this->oDOMDocument->CreateElement('entries'); - $oXmlDict->AppendChild($oXmlEntries); + $oXmlDict->appendChild($oXmlEntries); } else { @@ -964,19 +1217,19 @@ class ModelFactory $this->aDictKeys[$sLanguageCode])) { $oMe = $this->aDictKeys[$sLanguageCode][$sCode]; - $sFlag = $oMe->getAttribute('_alteration'); + $sFlag = $oMe->GetAlteration(); $oMe->parentNode->replaceChild($oXmlEntry, $oMe); $sNewFlag = $sFlag; if ($sFlag == '') { $sNewFlag = 'replaced'; } - $oXmlEntry->setAttribute('_alteration', $sNewFlag); + $oXmlEntry->SetAlteration($sNewFlag); } else { - $oXmlEntry->setAttribute('_alteration', 'added'); + $oXmlEntry->SetAlteration('added'); $oXmlEntries->appendChild($oXmlEntry); } $this->aDictKeys[$sLanguageCode][$sCode] = $oXmlEntry; @@ -1138,29 +1391,6 @@ class ModelFactory return $this->oDOMDocument->GetNodeById($sXPath, $sId, $oContextNode); } - /** - * Check if the class specified by the given node already exists in the loaded DOM - * - * @param DOMNode $oClassNode The node corresponding to the class to load - * - * @return bool True if the class exists, false otherwise - * @throws Exception - */ - protected function ClassExists(DOMNode $oClassNode) - { - assert(false); - if ($oClassNode->hasAttribute('id')) - { - $sClassName = $oClassNode->GetAttribute('id'); - } - else - { - throw new Exception('ModelFactory::AddClass: Cannot add a class with no name'); - } - - return (array_key_exists($sClassName, self::$aLoadedClasses)); - } - /** * Check if the class specified by the given name already exists in the loaded DOM * @@ -1198,28 +1428,19 @@ class ModelFactory { throw new Exception("ModelFactory::AddClass: Cannot add the already existing class $sClassName"); } - $sParentClass = $oClassNode->GetChildText('parent', ''); - $oParentNode = $this->GetClass($sParentClass); - if ($oParentNode == null) - { + if (false === $this->ClassNameExists($sParentClass)) { throw new Exception("ModelFactory::AddClass: Cannot find the parent class of '$sClassName': '$sParentClass'"); } - else - { - if ($sModuleName != '') - { - $oClassNode->SetAttribute('_created_in', $sModuleName); - } - $oParentNode->AddChildNode($this->oDOMDocument->importNode($oClassNode, true)); - if (substr($sParentClass, 0, 1) == '/') // Convention for well known parent classes - { - // Remove the leading slash character - $oParentNameNode = $oClassNode->GetOptionalElement('parent')->firstChild; // Get the DOMCharacterData node - $oParentNameNode->data = substr($sParentClass, 1); - } + 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); } /** @@ -1292,7 +1513,7 @@ EOF */ public function ListClasses($sModuleName) { - return $this->GetNodes("/itop_design/classes//class[@id][@_created_in='$sModuleName']"); + return $this->GetNodes("/itop_design/classes/class[@id][@_created_in='$sModuleName']"); } /** @@ -1304,7 +1525,7 @@ EOF */ public function ListAllClasses($bIncludeMetas = false) { - $sXPath = "/itop_design/classes//class[@id]"; + $sXPath = "/itop_design/classes/class[@id]"; if ($bIncludeMetas === true) { $sXPath .= "|/itop_design/meta/classes/class[@id]"; @@ -1320,7 +1541,24 @@ EOF */ public function ListRootClasses() { - return $this->GetNodes("/itop_design/classes/class/class[@id][class]"); + $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]"); } /** @@ -1333,7 +1571,7 @@ EOF { // Check if class among XML classes /** @var \MFElemen|null $oClassNode */ - $oClassNode = $this->GetNodes("/itop_design/classes//class[@id='$sClassName']")->item(0); + $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)) @@ -1351,26 +1589,27 @@ EOF * @return mixed * @throws \Exception */ - public function AddWellKnownParent($sWellKnownParent) + public function AddWellKnownParent(MFElement $oClasses, $sWellKnownParent) { $oWKClass = $this->oDOMDocument->CreateElement('class'); $oWKClass->setAttribute('id', $sWellKnownParent); - $this->oClasses->AppendChild($oWKClass); + $oClasses->appendChild($oWKClass); return $oWKClass; } /** - * @param $oClassNode + * Get the direct child classes + * @param \MFElement $oClassNode * * @return \DOMNodeList */ public function GetChildClasses($oClassNode) { - return $this->GetNodes("class", $oClassNode); + $sClassId = $oClassNode->getAttribute('id'); + return $this->oDOMDocument->GetNodes("/itop_design/classes/class[parent/text()[. = '$sClassId']]"); } - /** * @param string $sClassName * @param string $sAttCode @@ -1396,7 +1635,7 @@ EOF } /** - * List all classes from the DOM + * List all fields of a class from the DOM * * @param \DOMNode $oClassNode * @@ -1434,11 +1673,11 @@ EOF } /** - * @return mixed + * @return void */ public function ApplyChanges() { - return $this->oRoot->ApplyChanges(); + $this->oRoot->ApplyChanges(); } /** @@ -1453,14 +1692,14 @@ EOF /** * Import the node into the delta * - * @param $oNodeClone + * @param DesignElement $oNodeClone * * @return mixed */ protected function SetDeltaFlags($oNodeClone) { - $sAlteration = $oNodeClone->getAttribute('_alteration'); - $oNodeClone->removeAttribute('_alteration'); + $sAlteration = $oNodeClone->GetAlteration(); + $oNodeClone->RemoveAlteration(); if ($oNodeClone->hasAttribute('_old_id')) { $oNodeClone->setAttribute('_rename_from', $oNodeClone->getAttribute('_old_id')); $oNodeClone->removeAttribute('_old_id'); @@ -1481,6 +1720,9 @@ EOF 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; @@ -1495,67 +1737,32 @@ EOF /** * Create path for the delta * - * @param array aMovedClasses The classes that have been moved in the hierarchy (deleted + created elsewhere) - * @param DOMDocument oTargetDoc Where to attach the top of the hierarchy - * @param MFElement oNode The node to import with its path + * @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($aMovedClasses, $oTargetDoc, $oNode) + protected function ImportNodeAndPathDelta($oTargetDoc, $oNode) { - // Preliminary: skip the parent if this node is organized hierarchically into the DOM - // Only class nodes are organized this way $oParent = $oNode->parentNode; - if ($oNode->IsClassNode()) - { - while (($oParent instanceof DOMElement) && ($oParent->IsClassNode())) - { - $oParent = $oParent->parentNode; - } - } + // Recursively create the path for the parent - if ($oParent instanceof DOMElement) - { - $oParentClone = $this->ImportNodeAndPathDelta($aMovedClasses, $oTargetDoc, $oParent); - } - else - { + 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->getAttribute('_alteration'); - if ($oNode->IsClassNode() && ($sAlteration != '')) - { + $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); - - // Handle the moved classes found under the root node (or the root node itself) - foreach ($oNodeClone->GetNodes("descendant-or-self::class[@id]", false) as $oClassNode) - { - if (array_key_exists($oClassNode->getAttribute('id'), $aMovedClasses)) - { - if ($sAlteration == 'removed') - { - // Remove that node: this specification will be overridden by the 'replaced' spec (see below) - $oClassNode->parentNode->removeChild($oClassNode); - } - else - { - // Move the class at the root, with the flag 'modified' - $oParentClone->appendChild($oClassNode); - $oClassNode->setAttribute('_alteration', 'replaced'); - $this->SetDeltaFlags($oClassNode); - } - } - } - } - else - { + } 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 @@ -1563,19 +1770,15 @@ EOF // 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'))) - { + 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) - { + if (!$oNodeClone) { $bCopyContents = ($sAlteration == 'replaced') || ($sAlteration == 'added') || ($sAlteration == 'needed') || ($sAlteration == 'forced'); $oNodeClone = $oTargetDoc->importNode($oNode->cloneNode($bCopyContents), $bCopyContents); $this->SetDeltaFlags($oNodeClone); @@ -1617,23 +1820,9 @@ EOF { $oDelta = new MFDocument(); - // Handle classes moved from one parent to another - // This will be done in two steps: - // 1) Identify the moved classes (marked as deleted under the original parent, and created under the new parent) - // 2) When importing those "moved" classes into the delta (see ImportNodeAndPathDelta), extract them from the hierarchy (the alteration can be done at an upper level in the hierarchy) and mark them as "modified" - $aMovedClasses = array(); - foreach ($this->GetNodes("/itop_design/classes//class/class[@_alteration='removed']", null, false) as $oNode) - { - $sId = $oNode->getAttribute('id'); - if ($this->GetNodes("/itop_design/classes//class/class[@id='$sId']/properties", null, false)->length > 0) - { - $aMovedClasses[$sId] = true; - } - } - foreach ($this->ListChanges() as $oAlteredNode) { - $this->ImportNodeAndPathDelta($aMovedClasses, $oDelta, $oAlteredNode); + $this->ImportNodeAndPathDelta($oDelta, $oAlteredNode); } foreach ($aNodesToIgnore as $sXPath) { @@ -1742,6 +1931,17 @@ EOF public function GetRootDirs() { return $this->aRootDirs; } + + /** + * @param \DOMElement $oNode + * + * @return mixed + * @Since 3.1.1 + */ + public static function GetXMLLineNumber($oNode) + { + return $oNode->getLineNo(); + } } /** @@ -1793,7 +1993,8 @@ class MFElement extends Combodo\iTop\DesignElement $oNode = null; foreach ($this->childNodes as $oChildNode) { - if (($oChildNode->nodeName == $sTagName) && (($oChildNode->getAttribute('_alteration') != 'removed'))) + /** @var MFElement $oChildNode */ + if (($oChildNode->nodeName == $sTagName) && !$oChildNode->IsRemoved()) { $oNode = $oChildNode; break; @@ -1801,7 +2002,8 @@ class MFElement extends Combodo\iTop\DesignElement } if ($bMustExist && is_null($oNode)) { - throw new DOMFormatException('Missing unique tag: '.$sTagName); + $sXPath = DesignDocument::GetItopNodePath($this); + throw new DOMFormatException("Missing unique tag: $sTagName in: $sXPath"); } return $oNode; @@ -1839,7 +2041,8 @@ class MFElement extends Combodo\iTop\DesignElement if (array_key_exists($key, $res)) { // Houston! - throw new DOMFormatException("id '$key' already used", null, null, $oItem); + $sXPath = DesignDocument::GetItopNodePath($this); + throw new DOMFormatException("id '$key' already used in $sXPath", null, null, $oItem); } $res[$key] = $oItem->GetNodeAsArrayOfItems(); } @@ -1878,12 +2081,12 @@ class MFElement extends Combodo\iTop\DesignElement if (is_array($itemValue)) { $oXmlItems = $oXmlDoc->CreateElement('items'); - $oXMLNode->AppendChild($oXmlItems); + $oXMLNode->appendChild($oXmlItems); foreach ($itemValue as $key => $item) { $oXmlItem = $oXmlDoc->CreateElement('item'); - $oXmlItems->AppendChild($oXmlItem); + $oXmlItems->appendChild($oXmlItem); if (is_string($key)) { @@ -1895,7 +2098,7 @@ class MFElement extends Combodo\iTop\DesignElement else { $oXmlText = $oXmlDoc->CreateTextNode((string)$itemValue); - $oXMLNode->AppendChild($oXmlText); + $oXMLNode->appendChild($oXmlText); } } @@ -1914,71 +2117,6 @@ class MFElement extends Combodo\iTop\DesignElement } } - /** - * Find the child node matching the given node. - * UNSAFE: may return nodes marked as _alteration="removed" - * A method with the same signature MUST exist in MFDocument for the recursion to work fine - * - * @param \MFElement $oRefNode The node to search for - * @param string $sSearchId substitutes to the value of the 'id' attribute - * - * @return \MFElement|null - * @throws \Exception - */ - public function _FindChildNode(MFElement $oRefNode, $sSearchId = null) - { - return self::_FindNode($this, $oRefNode, $sSearchId); - } - - /** - * Find the child node matching the given node under the specified parent. - * UNSAFE: may return nodes marked as _alteration="removed" - * - * @param \DOMNode $oParent - * @param \MFElement $oRefNode - * @param string $sSearchId - * - * @return \MFElement|null - * @throws Exception - */ - public static function _FindNode(DOMNode $oParent, MFElement $oRefNode, $sSearchId = null) - { - $oRes = null; - if ($oParent instanceof DOMDocument) - { - $oDoc = $oParent->firstChild->ownerDocument; - $oRoot = $oParent; - } - else - { - $oDoc = $oParent->ownerDocument; - $oRoot = $oParent; - } - - $oXPath = new DOMXPath($oDoc); - if ($oRefNode->hasAttribute('id')) - { - // Find the first element having the same tag name and id - if (!$sSearchId) - { - $sSearchId = $oRefNode->getAttribute('id'); - } - $sXPath = './'.$oRefNode->tagName."[@id='$sSearchId']"; - - /** @var \MFElement|null $oRes */ - $oRes = $oXPath->query($sXPath, $oRoot)->item(0); - } - else - { - // Get the first one having the same tag name (ignore others) - $sXPath = './'.$oRefNode->tagName; - - /** @var \MFElement|null $oRes */ - $oRes = $oXPath->query($sXPath, $oRoot)->item(0); - } - - return $oRes; - } /** * Check if the current node is under a node 'added' or 'altered' @@ -1991,7 +2129,7 @@ class MFElement extends Combodo\iTop\DesignElement // 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->getAttribute('_alteration') != '') + if ($oParent->GetAlteration() != '') { return true; } @@ -2064,12 +2202,11 @@ class MFElement extends Combodo\iTop\DesignElement $oExisting = $this->_FindChildNode($oNode); if ($oExisting) { - if ($oExisting->getAttribute('_alteration') != 'removed') { + if (!$oExisting->IsRemoved()) { $sPath = MFDocument::GetItopNodePath($oNode); - $iLine = $oNode->getLineNo(); - $sExistingPath = MFDocument::GetItopNodePath($oExisting); - $iExistingLine = $oExisting->getLineNo(); - + $iLine = ModelFactory::GetXMLLineNumber($oNode); + $sExistingPath = MFDocument::GetItopNodePath($oExisting).' created_in: ['.$oExisting->getAttribute('_created_in').']'; + $iExistingLine = ModelFactory::GetXMLLineNumber($oExisting); $sExceptionMessage = <<IsInDefinition()) { - $oNode->setAttribute('_alteration', $sFlag); + $oNode->SetAlteration($sFlag); } } @@ -2110,14 +2247,15 @@ EOF; if (!$oExisting) { $sPath = MFDocument::GetItopNodePath($this)."/".$oNode->tagName.(empty($sSearchId) ? '' : "[$sSearchId]"); - $iLine = $oNode->getLineNo(); + $iLine = ModelFactory::GetXMLLineNumber($oNode); throw new MFException($sPath." at line $iLine: could not be modified (not found)", MFException::COULD_NOT_BE_MODIFIED_NOT_FOUND, $sPath, $iLine); } - $sPrevFlag = $oExisting->getAttribute('_alteration'); - if ($sPrevFlag == 'removed') { + $sPrevFlag = $oExisting->GetAlteration(); + $sOldId = $oExisting->getAttribute('_old_id'); + if ($oExisting->IsRemoved()) { $sPath = MFDocument::GetItopNodePath($this)."/".$oNode->tagName.(empty($sSearchId) ? '' : "[$sSearchId]"); - $iLine = $oNode->getLineNo(); + $iLine = ModelFactory::GetXMLLineNumber($oNode); throw new MFException($sPath." at line $iLine: could not be modified (marked as deleted)", MFException::COULD_NOT_BE_MODIFIED_ALREADY_DELETED, $sPath, $iLine); } @@ -2126,7 +2264,10 @@ EOF; if ($sPrevFlag == '') { $sPrevFlag = 'replaced'; } - $oNode->setAttribute('_alteration', $sPrevFlag); + $oNode->SetAlteration($sPrevFlag); + if ($sOldId !== '') { + $oNode->setAttribute('_old_id', $sOldId); + } } } @@ -2155,8 +2296,8 @@ EOF; $oNode->setAttribute('_old_id', $sOldId); } - $sPrevFlag = $oExisting->getAttribute('_alteration'); - if ($sPrevFlag == 'removed') { + $sPrevFlag = $oExisting->GetAlteration(); + if ($oExisting->IsRemoved()) { $sFlag = $bForce ? 'forced' : 'replaced'; } else { $sFlag = $sPrevFlag; // added, replaced or '' @@ -2174,28 +2315,11 @@ EOF; { $sFlag = $bForce ? 'forced' : 'replaced'; } - $oNode->setAttribute('_alteration', $sFlag); + $oNode->SetAlteration($sFlag); } } - /** - * Check that the current node is actually a class node, under classes - */ - public function IsClassNode() - { - if ($this->tagName == 'class') - { - if (($this->parentNode->tagName == 'classes') && ($this->parentNode->parentNode->tagName == 'itop_design')) // Beware: classes/class also exists in the group definition - { - return true; - } - return $this->parentNode->IsClassNode(); - } - else { - return false; - } - } /** * Replaces a node by another one, making sure that recursive nodes are preserved @@ -2221,14 +2345,15 @@ EOF; /** * Remove a node and set the flags that will be used to compute the delta * + * * @throws \Exception */ - public function Delete() + public function Delete(bool $bIsConditional = false) { - switch ($this->getAttribute('_alteration')) + switch ($this->GetAlteration()) { case 'replaced': - $sFlag = 'removed'; + $sFlag = $bIsConditional ? 'remove_needed' : 'removed'; break; case 'added': case 'needed': @@ -2238,7 +2363,7 @@ EOF; throw new Exception("Attempting to remove a deleted node: $this->tagName (id: ".$this->getAttribute('id').""); default: - $sFlag = 'removed'; + $sFlag = $bIsConditional ? 'remove_needed' : 'removed'; if ($this->IsInDefinition()) { $sFlag = null; @@ -2247,7 +2372,13 @@ EOF; } if ($sFlag) { - $this->setAttribute('_alteration', $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 @@ -2260,54 +2391,6 @@ EOF; } } - /** - * Merge the current node into the given container - * - * @param \MFElement $oContainer An element or a document - * @param string $sSearchId The id to consider (could be blank) - * @param bool $bMustExist Throw an exception if the node must already be found (and not marked as deleted!) - * @param bool $bIfExists Return null if the node does not exists (or is marked as deleted) - * - * @return \MFElement|null - * @throws \Exception - */ - public function MergeInto($oContainer, $sSearchId, $bMustExist, $bIfExists = false) - { - $oTargetNode = $oContainer->_FindChildNode($this, $sSearchId); - if ($oTargetNode) - { - if ($oTargetNode->getAttribute('_alteration') == 'removed') - { - if ($bMustExist) - { - throw new Exception(MFDocument::GetItopNodePath($this).' at line '.$this->getLineNo().": could not be found (marked as deleted)"); - } - // Beware: ImportNode(xxx, false) DOES NOT copy the node's attribute on *some* PHP versions (<5.2.17) - // So use this workaround to import a node and its attributes on *any* PHP version - $oTargetNode = $oContainer->ownerDocument->ImportNode($this->cloneNode(false), true); - $oContainer->appendChild($oTargetNode); - } - } - else - { - if ($bMustExist) - { - //echo "Dumping parent node
\n"; - //$oContainer->Dump(); - throw new Exception(MFDocument::GetItopNodePath($this).' at line '.$this->getLineNo().": could not be found"); - } - if (!$bIfExists) - { - // Beware: ImportNode(xxx, false) DOES NOT copy the node's attribute on *some* PHP versions (<5.2.17) - // So use this workaround to import a node and its attributes on *any* PHP version - $oTargetNode = $oContainer->ownerDocument->ImportNode($this->cloneNode(false), true); - $oContainer->appendChild($oTargetNode); - } - } - - return $oTargetNode; - } - /** * Renames a node and set the flags that will be used to compute the delta * @@ -2315,7 +2398,8 @@ EOF; */ public function Rename($sId) { - if (($this->getAttribute('_alteration') == 'replaced') || !$this->IsInDefinition()) + $sAlteration = $this->GetAlteration(); + if (($sAlteration == 'replaced') || ($sAlteration == 'forced') || !$this->IsInDefinition()) { $sOriginalId = $this->getAttribute('_old_id'); if ($sOriginalId == '') @@ -2361,7 +2445,8 @@ EOF; 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);; + $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')) { @@ -2370,16 +2455,46 @@ EOF; if ($oNode->hasAttribute('_old_id')) { $oNode->removeAttribute('_old_id'); } - if ($oNode->hasAttribute('_alteration')) { - if ('removed' === $oNode->GetAttribute('_alteration')) { + 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->removeAttribute('_alteration'); + $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'); + } } /** @@ -2455,15 +2570,15 @@ class MFDocument extends \Combodo\iTop\DesignDocument * 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 MFElement $oRefNode The node to search for + * @param DesignElement $oRefNode The node to search for * @param string $sSearchId substitutes to the value of the 'id' attribute * - * @return \DOMElement|null + * @return DesignElement|null * @throws \Exception */ - public function _FindChildNode(MFElement $oRefNode, $sSearchId = null) + public function _FindChildNode(DesignElement $oRefNode, $sSearchId = null) { - return MFElement::_FindNode($this, $oRefNode, $sSearchId); + return DesignElement::_FindNode($this, $oRefNode, $sSearchId); } /** @@ -2480,11 +2595,12 @@ class MFDocument extends \Combodo\iTop\DesignDocument $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']"; + $sXPath = "($sXPath)[not(@_alteration) or (@_alteration!='removed' and @_alteration!='remove_needed')]"; } if (is_null($oContextNode)) @@ -2510,7 +2626,7 @@ class MFDocument extends \Combodo\iTop\DesignDocument { $oXPath = new DOMXPath($this); $sQuotedId = self::XPathQuote($sId); - $sXPath .= "[@id=$sQuotedId and(not(@_alteration) or @_alteration!='removed')]"; + $sXPath = "($sXPath)[@id=$sQuotedId and (not(@_alteration) or @_alteration!='removed' or @_alteration!='remove_needed')]"; if (is_null($oContextNode)) { diff --git a/tests/php-unit-tests/unitary-tests/setup/ModelFactoryTest.php b/tests/php-unit-tests/unitary-tests/setup/ModelFactoryTest.php index 5a3184204..7fd9f2a60 100644 --- a/tests/php-unit-tests/unitary-tests/setup/ModelFactoryTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/ModelFactoryTest.php @@ -2,11 +2,13 @@ namespace Combodo\iTop\Test\UnitTest\Setup; +use Combodo\iTop\DesignDocument; use Combodo\iTop\Test\UnitTest\ItopTestCase; use DOMDocument; use MFDocument; use MFElement; use ModelFactory; +use PHPUnit\Framework\ExpectationFailedException; /** @@ -26,6 +28,7 @@ use ModelFactory; * Rename ────────►│ │ * SetChildNode ────►│ │ * └─────────────────┘ + * * @covers ModelFactory * @covers MFElement * @@ -36,6 +39,8 @@ class ModelFactoryTest extends ItopTestCase { parent::setUp(); + static::$DEBUG_UNIT_TEST = true; + $this->RequireOnceItopFile('setup/modelfactory.class.inc.php'); } @@ -43,7 +48,7 @@ class ModelFactoryTest extends ItopTestCase * @param $sInitialXML * * @return \ModelFactory - * @throws \ReflectionException + * @throws \Exception */ protected function MakeVanillaModelFactory($sInitialXML): ModelFactory { @@ -72,34 +77,1235 @@ class ModelFactoryTest extends ItopTestCase $oExpectedDocument->preserveWhiteSpace = false; $oExpectedDocument->loadXML($sXML); $oExpectedDocument->formatOutput = true; - return $oExpectedDocument->saveXML($oExpectedDocument->firstChild); + + return $oExpectedDocument->C14N(false, true); } /** * @param $sExpected * @param $sActual + * @param string $sMessage */ - protected function AssertEqualiTopXML($sExpected, $sActual) + protected function AssertEqualiTopXML($sExpected, $sActual, string $sMessage = '') { // Note: assertEquals reports the differences in a diff which is easier to interpret (in PHPStorm) // as compared to the report given by assertEqualXMLStructure - static::assertEquals($this->CanonicalizeXML($sExpected), $this->CanonicalizeXML($sActual)); + static::assertEquals($this->CanonicalizeXML($sExpected), $this->CanonicalizeXML($sActual), $sMessage); } /** * Assertion ignoring some of the unexpected decoration brought by DOM Elements. */ - protected function AssertEqualModels(string $sExpectedXML, ModelFactory $oFactory) + protected function AssertEqualModels(string $sExpectedXML, ModelFactory $oFactory, $sMessage = '') { - return $this->AssertEqualiTopXML($sExpectedXML, $oFactory->Dump(null, true)); + $this->AssertEqualiTopXML($sExpectedXML, $oFactory->Dump(null, true), $sMessage); } /** - * @dataProvider providerDeltas - * @covers ModelFactory::LoadDelta - * @covers ModelFactory::ApplyChanges + * @test + * @dataProvider GetPreviousCommentProvider + * @covers ModelFactory::GetPreviousComment + * + * @param $sDeltaXML + * @param $sClassName + * @param $sExpectedComment + * + * @return void + * @throws \Exception */ - public function testAlterationByXMLDelta($sInitialXML, $sDeltaXML, $sExpectedXML) + public function GetPreviousCommentTest($sDeltaXML, $sClassName, $sExpectedComment) + { + $oDocument = new MFDocument(); + $oDocument->loadXML($sDeltaXML); + $oXPath = new \DOMXPath($oDocument); + $sClassName = DesignDocument::XPathQuote($sClassName); + /** @var MFElement $oClassNode */ + $oClassNode = $oXPath->query("/itop_design/classes/class[@id=$sClassName]")->item(0); + /** @var \DOMComment|null $oCommentNode */ + $oCommentNode = ModelFactory::GetPreviousComment($oClassNode); + + if (is_null($sExpectedComment)) { + $this->assertNull($oCommentNode); + } else { + $this->assertEquals($sExpectedComment, $oCommentNode->textContent); + } + } + + public function GetPreviousCommentProvider() + { + $aData = []; + + $aData['No Comment first Class'] = [ + 'sDeltaXML' => ' + + + +', + 'sClassName' => 'A', + 'sExpectedComment' => null, + ]; + + $aData['No Comment other Class'] = [ + 'sDeltaXML' => ' + + + + + +', + 'sClassName' => 'A', + 'sExpectedComment' => null, + ]; + + $aData['Comment first class'] = [ + 'sDeltaXML' => ' + + + + +', + 'sClassName' => 'A', + 'sExpectedComment' => ' Test comment ', + ]; + + $aData['Comment other Class'] = [ + 'sDeltaXML' => ' + + + + + +', + 'sClassName' => 'A', + 'sExpectedComment' => ' Test comment ', + ]; + + return $aData; + } + + /** + * @test + * @dataProvider FlattenDeltaProvider + * @covers ModelFactory::FlattenClassesInDelta + * + * @param $sDeltaXML + * @param $sExpectedXML + * + * @return void + * @throws \ReflectionException + */ + public function FlattenDeltaTest($sDeltaXML, $sExpectedXML) + { + $oFactory = new ModelFactory([]); + $oDocument = new MFDocument(); + $oDocument->loadXML($sDeltaXML); + /* @var MFElement $oDeltaRoot */ + $oDeltaRoot = $oDocument->firstChild; + /** @var MFElement $oFlattenDeltaRoot */ + if (is_null($sExpectedXML)) { + $this->expectException(\MFException::class); + } + $oFlattenDeltaRoot = $this->InvokeNonPublicMethod(ModelFactory::class, 'FlattenClassesInDelta', $oFactory, [$oDeltaRoot]); + if (!is_null($sExpectedXML)) { + $this->AssertEqualiTopXML($sExpectedXML, $oFlattenDeltaRoot->ownerDocument->saveXML()); + } + } + + public function FlattenDeltaProvider() + { + return [ + 'Empty delta' => [ + 'sDeltaXML' => ' + +', + 'sExpectedXML' => ' +', + ], + + 'Flat delete' => [ + 'sDeltaXML' => ' + + + + + + +', + 'sExpectedXML' => ' + + + + + +', + ], + + 'flat define root' => [ + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + + cmdbAbstractObject + + +', + 'sExpectedXML' => ' + + + cmdbAbstractObject + + + cmdbAbstractObject + + +', + ], + + 'flat force root' => [ + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + + cmdbAbstractObject + + +', + 'sExpectedXML' => ' + + + cmdbAbstractObject + + + cmdbAbstractObject + + +', + ], + + 'flat redefine root' => [ + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + + cmdbAbstractObject + + +', + 'sExpectedXML' => ' + + + cmdbAbstractObject + + + cmdbAbstractObject + + +', + ], + + 'Simple hierarchy define root' => [ + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + C_1 + + + +', + 'sExpectedXML' => ' + + + cmdbAbstractObject + + + + C_1 + + +', + ], + + 'Complex hierarchy delete' => [ + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + C_1 + + C_1_1 + + + + + C_1 + + + + +', + 'sExpectedXML' => ' + + + cmdbAbstractObject + + + + C_1 + + + + C_1_1 + + + + + + C_1 + + + + +', + ], + + 'Complex hierarchy define root' => [ + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + C_1 + + C_1_1 + + C_1_1_1 + + + + + C_1 + + C_1_2 + + + + +', + 'sExpectedXML' => ' + + + cmdbAbstractObject + + + + C_1 + + + + C_1_1 + + + + C_1_1_1 + + + + C_1 + + + + C_1_2 + + +', + ], + + 'Complex hierarchy define' => [ + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + C_1 + + C_1_1 + + C_1_1_1 + + + + + C_1 + + C_1_2 + + + + +', + 'sExpectedXML' => ' + + + cmdbAbstractObject + + + + C_1 + + + + C_1_1 + + + + C_1_1_1 + + + + C_1 + + + + C_1_2 + + +', + ], + + 'Complex hierarchy force' => [ + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + C_1 + + C_1_1 + + + + +', + 'sExpectedXML' => ' + + + cmdbAbstractObject + + + + C_1 + + + + C_1_1 + + +', + ], + + 'Complex hierarchy force root' => [ + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + C_1 + + C_1_1 + + + + +', + 'sExpectedXML' => ' + + + cmdbAbstractObject + + + + C_1 + + + + C_1_1 + + +', + ], + + 'Complex hierarchy redefine' => [ + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + C_1 + + C_1_1 + + + + +', + 'sExpectedXML' => ' + + + cmdbAbstractObject + + + + C_1 + + + + C_1_1 + + +', + ], + + 'Complex hierarchy redefine root' => [ + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + C_1 + + + +', + 'sExpectedXML' => ' + + + cmdbAbstractObject + + + + C_1 + + +', + ], + + 'Complex hierarchy define_if_not_exists flattening generates an error' => [ + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + C_1 + + C_1_1 + + + + +', + 'sExpectedXML' => null, + ], + + 'Complex hierarchy if_exists flattening generates an error' => [ + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + C_1 + + C_1_1 + + + + +', + 'sExpectedXML' => null, + ], + + ]; + } + + /** + * @test + * @dataProvider LoadDeltaProvider + * @covers ModelFactory::LoadDelta + * + * @param $sInitialXML + * @param $sDeltaXML + * @param $sExpectedXML + * + * @return void + * @throws \Exception + */ + public function LoadDeltaTest($sInitialXML, $sDeltaXML, $sExpectedXMLOrErrorMessage) + { + $oFactory = $this->MakeVanillaModelFactory($sInitialXML); + $oFactoryDocument = $this->GetNonPublicProperty($oFactory, 'oDOMDocument'); + $sExpectedXML = null; + if (\utils::StartsWith($sExpectedXMLOrErrorMessage, '<')) { + $sExpectedXML = $sExpectedXMLOrErrorMessage; + } + + // Load the delta + $oDocument = new MFDocument(); + $oDocument->loadXML($sDeltaXML); + /* @var MFElement $oDeltaRoot */ + $oDeltaRoot = $oDocument->firstChild; + try { + $oFactory->LoadDelta($oDeltaRoot, $oFactoryDocument); + } + catch (\Exception $e) { + $this->assertNull($sExpectedXML, 'LoadDelta() should not have failed with exception: '.$e->getMessage()); + $this->assertEquals($sExpectedXMLOrErrorMessage, $e->getMessage()); + return; + } + $this->AssertEqualModels($sExpectedXML, $oFactory, 'LoadDelta() did not produce the expected result'); + } + + public function LoadDeltaProvider() + { + return [ + 'empty delta' => [ + 'sInitialXML' => ' + + + + +', + 'sDeltaXML' => '', + 'sExpectedXMLOrErrorMessage' => ' + + + +', + ], + 'merge delta lax mode' => [ + 'sInitialXML' => ' + + + + +', + 'sDeltaXML' => ' + + + cmdbAbstractObject + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + cmdbAbstractObject + + +', + ], + 'Add a class' => [ + 'sInitialXML' => ' + + + + +', + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + cmdbAbstractObject + + +', + ], + 'Add a class if not exists (N°6660)' => [ + 'sInitialXML' => ' + + + + +', + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + cmdbAbstractObject + + +', + ], + 'Conditionally add a class but it already exists' => [ + 'sInitialXML' => ' + + + + + cmdbAbstractObject + + +', + 'sDeltaXML' => ' + + + + toto + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + cmdbAbstractObject + + +', + ], + + 'Add a class and subclass in hierarchy' => [ + 'sInitialXML' => ' + + + + +', + 'sDeltaXML' => ' + + + + cmdbAbstractObject + + C_1 + + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + cmdbAbstractObject + + + + C_1 + + +', + ], + + 'Delete a class' => [ + 'sInitialXML' => ' + + + + + cmdbAbstractObject + + +', + 'sDeltaXML' => ' + + + + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + +', + ], + + 'Redefine a recently added subclass' => [ + 'sInitialXML' => ' + + + + + cmdbAbstractObject + + + C_1 + + +', + 'sDeltaXML' => ' + + + + + C_1 + + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + cmdbAbstractObject + + + C_1 + + + +', + ], + + 'Delete a recently added class' => [ + 'sInitialXML' => ' + + + + + cmdbAbstractObject + + +', + 'sDeltaXML' => ' + + + + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + +', + ], + 'Delete hierarchically a class' => [ + 'sInitialXML' => ' + + + + + cmdbAbstractObject + + +', + 'sDeltaXML' => ' + + + + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + +', + ], + + 'Delete hierarchically a class and subclass' => [ + 'sInitialXML' => ' + + + + + cmdbAbstractObject + + + C_1 + + +', + 'sDeltaXML' => ' + + + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + +', + ], + + 'Delete hierarchically a class and subclass already deleted' => [ + 'sInitialXML' => ' + + + + + cmdbAbstractObject + + + C_1 + + + C_1 + + + C_1_2 + + +', + 'sDeltaXML' => ' + + + + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + + +', + ], + + 'Delete if exist hierarchically an existing class' => [ + 'sInitialXML' => ' + + + + + cmdbAbstractObject + + +', + 'sDeltaXML' => ' + + + + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + +', + ], + + 'Delete if exist hierarchically an non existing class' => [ + 'sInitialXML' => ' + + + + + cmdbAbstractObject + + +', + 'sDeltaXML' => ' + + + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + cmdbAbstractObject + + + +', + ], + + 'Delete if exist hierarchically a removed class' => [ + 'sInitialXML' => ' + + + + + cmdbAbstractObject + + +', + 'sDeltaXML' => ' + + + + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + +', + ], + + 'Delete if exist hierarchically an existing class and subclass' => [ + 'sInitialXML' => ' + + + + + cmdbAbstractObject + + + C_1 + + +', + 'sDeltaXML' => ' + + + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + +', + ], + + 'Delete if exist hierarchically a non existing subclass' => [ + 'sInitialXML' => ' + + + + + cmdbAbstractObject + + + C_1 + + +', + 'sDeltaXML' => ' + + + + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + + +', + ], + + 'Class comment should be preserved' => [ + 'sInitialXML' => ' + + + + +', + 'sDeltaXML' => ' + + + + + cmdbAbstractObject + + + + cmdbAbstractObject + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + + cmdbAbstractObject + + + + cmdbAbstractObject + + +', + ], + 'Delete hierarchically a class and add it again' => [ + 'sInitialXML' => ' + + + + + cmdbAbstractObject + + + C_1 + + + C_1_1 + + +', + 'sDeltaXML' => ' + + + + + cmdbAbstractObject + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + cmdbAbstractObject + + +', + ], + 'merge delta strict' => [ + 'sInitialXML' => ' + + + + +', + 'sDeltaXML' => ' + + + cmdbAbstractObject + + +', + 'sExpectedXMLOrErrorMessage' => '/itop_design/classes/class[C_1] at line 3: could not be found or marked as removed (strict mode)', + ], + 'Redefine classes and changing parent' => [ + 'sInitialXML' => ' + + + + cmdbAbstractObject + + + cmdbAbstractObject + + + Licence + + +', + 'sDeltaXML' => ' + + + FunctionalCI + + ConfigElement + + Key + + + + + Key + + +', + 'sExpectedXMLOrErrorMessage' => ' + + + + cmdbAbstractObject + + + FunctionalCI + + + + ConfigElement + + + Key + + + + Key + + +', + ], + 'Class with wrong parent should generate an error' => [ + 'sInitialXML' => ' + + + + + cmdbAbstractObject + + +', + 'sDeltaXML' => ' + + + toto + + +', + 'sExpectedXMLOrErrorMessage' => "/itop_design/classes/class[C_1]/parent at line 4: invalid parent class 'toto'", + ], + + ]; + } + + /** + * @test + * @dataProvider AlterationByXMLDeltaProvider + * @covers ModelFactory::LoadDelta + * @covers ModelFactory::ApplyChanges + */ + public function AlterationByXMLDeltaTest($sInitialXML, $sDeltaXML, $sExpectedXML) { $oFactory = $this->MakeVanillaModelFactory($sInitialXML); $oFactoryRoot = $this->GetNonPublicProperty($oFactory, 'oDOMDocument'); @@ -121,102 +1327,108 @@ class ModelFactoryTest extends ItopTestCase /** * @return array */ - public function providerDeltas() + public function AlterationByXMLDeltaProvider() { // Basic (structure) - $aDeltas['No change at all'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => << << XML - ]; - $aDeltas['No change at all - mini delta'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => << << XML - ]; - $aDeltas['_delta="merge" implicit'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => << << XML - ]; - $aDeltas['_delta="merge" explicit'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => << << XML - ]; - $aDeltas['_delta="merge" does not handle data'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << - Ghost busters!!! + Maintained Text XML - , - 'sExpectedXML' => << << - + Maintained Text XML - ]; - $aDeltas['_delta="merge" recursively'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << @@ -225,8 +1437,8 @@ XML XML - , - 'sExpectedXML' => << << @@ -235,458 +1447,489 @@ XML XML - ]; - - // Define or redefine - $aDeltas['_delta="define" without id'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << - + XML - , - 'sExpectedXML' => << << XML - ]; - $aDeltas['_delta="define" with id'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << - + XML - , - 'sExpectedXML' => << << - + XML - ]; - $aDeltas['_delta="define" but existing node'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << - + XML - , - 'sDeltaXML' => << << - + XML - , - 'sExpectedXML' => null - ]; - $aDeltas['_delta="redefine" without id'] = [ - 'sInitialXML' => << null, + ], + '_delta="redefine" without id' => [ + 'sInitialXML' => << Initial BB XML - , - 'sDeltaXML' => << << Gainsbourg XML - , - 'sExpectedXML' => << << Gainsbourg XML - ]; - $aDeltas['_delta="redefine" with id'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << Initial BB XML - , - 'sDeltaXML' => << << Gainsbourg XML - , - 'sExpectedXML' => << << Gainsbourg XML - ]; - $aDeltas['_delta="redefine" but missing node'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << Gainsbourg XML - , - 'sExpectedXML' => null - ]; - $aDeltas['_delta="force" without id + missing node'] = [ - 'sInitialXML' => << null, + ], + '_delta="force" without id + missing node' => [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << Hulk XML - , - 'sExpectedXML' => << << Hulk XML - ]; - $aDeltas['_delta="force" with id + missing node'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << Hulk XML - , - 'sExpectedXML' => << << Hulk XML - ]; - $aDeltas['_delta="force" without id + existing node'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << Initial BB XML - , - 'sDeltaXML' => << << Gainsbourg XML - , - 'sExpectedXML' => << << Gainsbourg XML - ]; - $aDeltas['_delta="force" with id + existing node'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << Initial BB XML - , - 'sDeltaXML' => << << Gainsbourg XML - , - 'sExpectedXML' => << << Gainsbourg XML - ]; - - // Rename - $aDeltas['rename'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << Kryptonite XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => << << Kryptonite XML - ]; - $aDeltas['rename but missing node NOT INTUITIVE!!!'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => << << XML - ]; - - // Delete - $aDeltas['_delta="delete" without id'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << Initial BB XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => << << XML - ]; - $aDeltas['_delta="delete" with id'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << Initial BB XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => << << XML - ]; - $aDeltas['_delta="delete" but missing node'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => null, - ]; - $aDeltas['_delta="delete_if_exists" without id + existing node'] = [ - 'sInitialXML' => << null, + ], + '_delta="delete_if_exists" without id + existing node' => [ + 'sInitialXML' => << Initial BB XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => << -XML - ]; - $aDeltas['_delta="delete_if_exists" with id + existing node'] = [ - 'sInitialXML' => << '', + ], + '_delta="delete_if_exists" with id + existing node' => [ + 'sInitialXML' => << Initial BB XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => << '', + ], + '_delta="delete_if_exists" without id + missing node' => [ + 'sInitialXML' => << XML - ]; - $aDeltas['_delta="delete_if_exists" without id + missing node'] = [ - 'sInitialXML' => << -XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => << << XML - ]; - $aDeltas['_delta="delete_if_exists" with id + missing node'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => << << XML - ]; - - // Conditionals - $aDeltas['_delta="must_exist"'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => << << XML - ]; - $aDeltas['_delta="must_exist on missing node"'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => null, - ]; - $aDeltas['_delta="if_exists on missing node"'] = [ - 'sInitialXML' => << null, + ], + '_delta="if_exists on missing node (lax)' => [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => << << XML - , - ]; - $aDeltas['_delta="if_exists on existing node"'] = [ - 'sInitialXML' => << + + + + +XML + , + 'sExpectedXML' => << + +XML + , + ], + '_delta="if_exists on existing node"' => [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << XML - , - 'sExpectedXML' => << << XML - ]; - $aDeltas['_delta="define_if_not_exists on missing node"'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << The incredible Hulk XML - , - 'sExpectedXML' => << << The incredible Hulk XML - ]; - $aDeltas['_delta="define_if_not_exists on existing node"'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << Luke Banner XML - , - 'sDeltaXML' => << << The incredible Hulk XML - , - 'sExpectedXML' => << << Luke Banner XML - ]; - $aDeltas['_delta="define_and_must_exits"'] = [ - 'sInitialXML' => << [ + 'sInitialXML' => << XML - , - 'sDeltaXML' => << << + ', - 'sExpectedXML' => ' - - - + 'sExpectedXMLInLaxMode' => ' + + cmdbAbstractObject - - Contact - - - - + + ', + 'sExpectedXMLInStrictMode' => null, ], - 'Error merging classes' => [ - 'sInitialXML' => ' - - - + 'mode specified in delta takes precedence' => [ + 'sInitialXML' => ' + + + +', + 'sDeltaXML' => ' + + cmdbAbstractObject - - Contact - - - - + + +', + 'sExpectedXMLInLaxMode' => null, + 'sExpectedXMLInStrictMode' => null, + ], + 'merge leaf nodes have different behavior depending on the mode' => [ + 'sInitialXML' => ' + + Test ', 'sDeltaXML' => ' - - - titi - - + Taste ', - 'sExpectedXML' => ' - - - - cmdbAbstractObject - - Contact - titi - - - - + 'sExpectedXMLInLaxMode' => ' + Taste ', + 'sExpectedXMLInStrictMode' => null, ], - 'Error loading module - could not be deleted' => [ - 'sInitialXML' => ' - - - - cmdbAbstractObject - - Contact - - - - + 'merge existing leaf nodes without text have same behavior' => [ + 'sInitialXML' => ' + + ', 'sDeltaXML' => ' - - - cmdbAbstractObject - - - + ', - 'sExpectedXML' => ' - - - - cmdbAbstractObject - - - Contact - - - - + 'sExpectedXMLInLaxMode' => ' + +', + 'sExpectedXMLInStrictMode' => ' + ', ], - 'redefine class with sub-class' => [ - 'sInitialXML' => ' - - - - cmdbAbstractObject - - Contact - - - - + 'merge non-existing leaf nodes without text have different behavior' => [ + 'sInitialXML' => ' + ', 'sDeltaXML' => ' - - - cmdbAbstractObject - - - Contact - - - Contact - - - + ', - 'sExpectedXML' => ' - - - - cmdbAbstractObject - - - Contact - - - Contact - - - Contact - - - - + 'sExpectedXMLInLaxMode' => ' + ', + 'sExpectedXMLInStrictMode' => null, ], - 'force class with sub-class' => [ - 'sInitialXML' => ' - - - - cmdbAbstractObject - - Contact - - - - + 'merge non-existing nodes with sub-nodes defined' => [ + 'sInitialXML' => ' + ', 'sDeltaXML' => ' - - - cmdbAbstractObject - - - + + + ', - 'sExpectedXML' => ' - - - - cmdbAbstractObject - - - Contact - - - - + 'sExpectedXMLInLaxMode' => ' + + + +', + 'sExpectedXMLInStrictMode' => ' + + + ', ], - 'Class added with hierarchy' => [ - 'sInitialXML' => ' - - - - cmdbAbstractObject - - - cmdbAbstractObject - - Licence - - - - -', - 'sDeltaXML' => ' - - - FunctionalCI - - ConfigElement - - Key - - - - -', - 'sExpectedXML' => ' - - - - cmdbAbstractObject - - FunctionalCI - - ConfigElement - - Key - - - - - - cmdbAbstractObject - - Licence - - - - -', - ], - 'Class added with hierarchy needs redefine' => [ - 'sInitialXML' => ' - - - - cmdbAbstractObject - - - cmdbAbstractObject - - Licence - - - - -', - 'sDeltaXML' => ' - - - FunctionalCI - - ConfigElement - - Key - - - - - Key - - -', - 'sExpectedXML' => ' - - - - cmdbAbstractObject - - FunctionalCI - - ConfigElement - - Key - - - Key - - - - - - -', - ], - ]; } } diff --git a/tests/php-unit-tests/unitary-tests/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.expected.xml b/tests/php-unit-tests/unitary-tests/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.expected.xml index b11e6ed7d..22f8bf3c5 100644 --- a/tests/php-unit-tests/unitary-tests/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.expected.xml +++ b/tests/php-unit-tests/unitary-tests/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.expected.xml @@ -37,6 +37,9 @@ test + + + diff --git a/tests/php-unit-tests/unitary-tests/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.input.xml b/tests/php-unit-tests/unitary-tests/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.input.xml index b7bc5de22..1eb1f3583 100644 --- a/tests/php-unit-tests/unitary-tests/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.input.xml +++ b/tests/php-unit-tests/unitary-tests/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.input.xml @@ -35,6 +35,7 @@ test +