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.oUploadDlg = $('');
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' => << [
+ 'sInitialXML' => <<
+
+XML
+ ,
+ 'sDeltaXML' => <<
+
+
+
+
+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' => << <<
@@ -694,22 +1937,23 @@ XML
XML
- ,
- 'sExpectedXML' => << <<
XML
- ];
- $aDeltas['_delta="define_then_must_exist"'] = [
- 'sInitialXML' => << [
+ 'sInitialXML' => <<
XML
- ,
- 'sDeltaXML' => << <<
@@ -721,8 +1965,8 @@ XML
XML
- ,
- 'sExpectedXML' => << <<
@@ -732,14 +1976,15 @@ XML
XML
- ];
- $aDeltas['nested _delta should be cleaned'] = [
- 'sInitialXML' => << [
+ 'sInitialXML' => <<
XML
- ,
- 'sDeltaXML' => << <<
@@ -748,8 +1993,8 @@ XML
XML
- ,
- 'sExpectedXML' => << <<
@@ -758,20 +2003,147 @@ XML
XML
+ ,
+ ],
+ 'Class comments are stripped when class is deleted' => [
+ 'sInitialXML' => '
+
+
+
+
+
+
+',
+ 'sDeltaXML' => '
+
+
+
+
+',
+ 'sExpectedXML' => '
+
+
+
+',
+ ],
+ 'Class comments are preserved' => [
+ 'sInitialXML' => '
+
+
+
+
+
+
+',
+ 'sDeltaXML' => '
+
+
+
+',
+ 'sExpectedXML' => '
+
+
+
+
+
+',
+ ],
+ 'if_exist on removed node does nothing' => [
+ 'sInitialXML' => '
+
+
+',
+ 'sDeltaXML' => '
+
+
+
+
+',
+ 'sExpectedXML' => '',
+ ],
+ 'if_exist on missing node does nothing' => [
+ 'sInitialXML' => '
+',
+ 'sDeltaXML' => '
+
+
+
+
+',
+ 'sExpectedXML' => '',
+ ],
+ 'if_exist on existing node merges' => [
+ 'sInitialXML' => '
+
+
+',
+ 'sDeltaXML' => '
+
+
+
+
+',
+ 'sExpectedXML' => '
+
+
+
+',
+ ],
+ 'must_exist on removed node does error' => [
+ 'sInitialXML' => '
+
+
+',
+ 'sDeltaXML' => '
+
+
+
+
+',
+ 'sExpectedXML' => null,
+ ],
+ 'must_exist on missing node does error' => [
+ 'sInitialXML' => '
+',
+ 'sDeltaXML' => '
+
+
+
+
+',
+ 'sExpectedXML' => null,
+ ],
+ 'must_exist on existing node merges' => [
+ 'sInitialXML' => '
+
+
+',
+ 'sDeltaXML' => '
+
+
+
+
+',
+ 'sExpectedXML' => '
+
+
+
+',
+ ],
];
-
- return $aDeltas;
}
/**
- * @dataProvider providerAlterationAPIs
- * @covers \ModelFactory::GetDelta
- * @covers \MFElement::AddChildNode
- * @covers \MFElement::RedefineChildNode
- * @covers \MFElement::SetChildNode
- * @covers \MFElement::Delete
+ * @test
+ * @dataProvider AlterationAPIsProvider
+ * @covers \ModelFactory::GetDelta
+ * @covers \MFElement::AddChildNode
+ * @covers \MFElement::RedefineChildNode
+ * @covers \MFElement::SetChildNode
+ * @covers \MFElement::Delete
+ * @throws \MFException
*/
- public function testAlterationsByAPIs($sInitialXML, $sOperation, $sExpectedXML)
+ public function AlterationsByAPIsTest($sInitialXML, $sOperation, $sExpectedXML)
{
$oFactory = $this->MakeVanillaModelFactory($sInitialXML);
@@ -826,12 +2198,12 @@ XML
/**
* @return array[]
*/
- public function providerAlterationAPIs()
+ public function AlterationAPIsProvider()
{
define('CASE_NO_FLAG', <<
-
+
XML
@@ -911,257 +2283,404 @@ XML
XML
);
$aData = [
- 'CASE_NO_FLAG Delete' => [CASE_NO_FLAG , 'Delete', << [
+ CASE_NO_FLAG,
+ 'Delete',
+ <<
XML
+ ,
],
- 'CASE_ABOVE_A_FLAG Delete' => [CASE_ABOVE_A_FLAG , 'Delete', << [
+ CASE_ABOVE_A_FLAG,
+ 'Delete',
+ <<
XML
+ ,
],
- 'CASE_IN_A_DEFINITION Delete' => [CASE_IN_A_DEFINITION , 'Delete', << [
+ CASE_IN_A_DEFINITION,
+ 'Delete',
+ <<
XML
+ ,
],
- 'CASE_FLAG_ON_TARGET_define Delete' => [CASE_FLAG_ON_TARGET_define , 'Delete', << [
+ CASE_FLAG_ON_TARGET_define,
+ 'Delete',
+ <<
XML
+ ,
],
- 'CASE_FLAG_ON_TARGET_redefine Delete' => [CASE_FLAG_ON_TARGET_redefine , 'Delete', << [
+ CASE_FLAG_ON_TARGET_redefine,
+ 'Delete',
+ <<
XML
+ ,
],
- 'CASE_FLAG_ON_TARGET_needed Delete' => [CASE_FLAG_ON_TARGET_needed , 'Delete', << [
+ CASE_FLAG_ON_TARGET_needed,
+ 'Delete',
+ <<
XML
+ ,
],
- 'CASE_FLAG_ON_TARGET_forced Delete' => [CASE_FLAG_ON_TARGET_forced , 'Delete', << [
+ CASE_FLAG_ON_TARGET_forced,
+ 'Delete',
+ <<
XML
+ ,
],
- 'CASE_FLAG_ON_TARGET_removed Delete' => [CASE_FLAG_ON_TARGET_removed , 'Delete', null
+ 'CASE_FLAG_ON_TARGET_removed Delete' => [
+ CASE_FLAG_ON_TARGET_removed,
+ 'Delete',
+ null,
],
- 'CASE_FLAG_ON_TARGET_old_id Delete' => [CASE_FLAG_ON_TARGET_old_id , 'Delete', << [
+ CASE_FLAG_ON_TARGET_old_id,
+ 'Delete',
+ <<
XML
+ ,
],
- 'CASE_NO_FLAG AddChildNode' => [CASE_NO_FLAG , 'AddChildNodeToContainer', null
+ 'CASE_NO_FLAG AddChildNode' => [
+ CASE_NO_FLAG,
+ 'AddChildNodeToContainer',
+ null,
],
- 'CASE_ABOVE_A_FLAG AddChildNode' => [CASE_ABOVE_A_FLAG , 'AddChildNodeToContainer', null
+ 'CASE_ABOVE_A_FLAG AddChildNode' => [
+ CASE_ABOVE_A_FLAG,
+ 'AddChildNodeToContainer',
+ null,
],
- 'CASE_IN_A_DEFINITION AddChildNode' => [CASE_IN_A_DEFINITION , 'AddChildNodeToContainer', null
+ 'CASE_IN_A_DEFINITION AddChildNode' => [
+ CASE_IN_A_DEFINITION,
+ 'AddChildNodeToContainer',
+ null,
],
- 'CASE_FLAG_ON_TARGET_define AddChildNode' => [CASE_FLAG_ON_TARGET_define , 'AddChildNodeToContainer', null
+ 'CASE_FLAG_ON_TARGET_define AddChildNode' => [
+ CASE_FLAG_ON_TARGET_define,
+ 'AddChildNodeToContainer',
+ null,
],
- 'CASE_FLAG_ON_TARGET_redefine AddChildNode' => [CASE_FLAG_ON_TARGET_redefine , 'AddChildNodeToContainer', null
+ 'CASE_FLAG_ON_TARGET_redefine AddChildNode' => [
+ CASE_FLAG_ON_TARGET_redefine,
+ 'AddChildNodeToContainer',
+ null,
],
- 'CASE_FLAG_ON_TARGET_needed AddChildNode' => [CASE_FLAG_ON_TARGET_needed , 'AddChildNodeToContainer', null
+ 'CASE_FLAG_ON_TARGET_needed AddChildNode' => [
+ CASE_FLAG_ON_TARGET_needed,
+ 'AddChildNodeToContainer',
+ null,
],
- 'CASE_FLAG_ON_TARGET_forced AddChildNode' => [CASE_FLAG_ON_TARGET_forced , 'AddChildNodeToContainer', null
+ 'CASE_FLAG_ON_TARGET_forced AddChildNode' => [
+ CASE_FLAG_ON_TARGET_forced,
+ 'AddChildNodeToContainer',
+ null,
],
- 'CASE_FLAG_ON_TARGET_removed AddChildNode' => [CASE_FLAG_ON_TARGET_removed , 'AddChildNodeToContainer', << [
+ CASE_FLAG_ON_TARGET_removed,
+ 'AddChildNodeToContainer',
+ <<
Hello, I'm a newly added node
XML
+ ,
],
- 'CASE_FLAG_ON_TARGET_old_id AddChildNode' => [CASE_FLAG_ON_TARGET_old_id , 'AddChildNodeToContainer', null
+ 'CASE_FLAG_ON_TARGET_old_id AddChildNode' => [
+ CASE_FLAG_ON_TARGET_old_id,
+ 'AddChildNodeToContainer',
+ null,
],
- 'CASE_MISSING_TARGET AddChildNode' => [CASE_MISSING_TARGET , 'AddChildNodeToContainer', << [
+ CASE_MISSING_TARGET,
+ 'AddChildNodeToContainer',
+ <<
Hello, I'm a newly added node
XML
+ ,
],
- 'CASE_NO_FLAG RedefineChildNode' => [CASE_NO_FLAG , 'RedefineChildNodeToContainer', << [
+ CASE_NO_FLAG,
+ 'RedefineChildNodeToContainer',
+ <<
Hello, I'm replacing the previous node
XML
+ ,
],
- 'CASE_ABOVE_A_FLAG RedefineChildNode' => [CASE_ABOVE_A_FLAG , 'RedefineChildNodeToContainer', << [
+ CASE_ABOVE_A_FLAG,
+ 'RedefineChildNodeToContainer',
+ <<
Hello, I'm replacing the previous node
XML
+ ,
],
- 'CASE_IN_A_DEFINITION RedefineChildNode' => [CASE_IN_A_DEFINITION , 'RedefineChildNodeToContainer', << [
+ CASE_IN_A_DEFINITION,
+ 'RedefineChildNodeToContainer',
+ <<
Hello, I'm replacing the previous node
XML
+ ,
],
- 'CASE_FLAG_ON_TARGET_define RedefineChildNode' => [CASE_FLAG_ON_TARGET_define , 'RedefineChildNodeToContainer', << [
+ CASE_FLAG_ON_TARGET_define,
+ 'RedefineChildNodeToContainer',
+ <<
Hello, I'm replacing the previous node
XML
+ ,
],
- 'CASE_FLAG_ON_TARGET_redefine RedefineChildNode' => [CASE_FLAG_ON_TARGET_redefine , 'RedefineChildNodeToContainer', << [
+ CASE_FLAG_ON_TARGET_redefine,
+ 'RedefineChildNodeToContainer',
+ <<
Hello, I'm replacing the previous node
XML
+ ,
],
// Note: buggy case ?
- 'CASE_FLAG_ON_TARGET_needed RedefineChildNode' => [CASE_FLAG_ON_TARGET_needed , 'RedefineChildNodeToContainer', << [
+ CASE_FLAG_ON_TARGET_needed,
+ 'RedefineChildNodeToContainer',
+ <<
Hello, I'm replacing the previous node
XML
+ ,
],
- 'CASE_FLAG_ON_TARGET_forced RedefineChildNode' => [CASE_FLAG_ON_TARGET_forced , 'RedefineChildNodeToContainer', << [
+ CASE_FLAG_ON_TARGET_forced,
+ 'RedefineChildNodeToContainer',
+ <<
Hello, I'm replacing the previous node
XML
+ ,
],
- 'CASE_FLAG_ON_TARGET_removed RedefineChildNode' => [CASE_FLAG_ON_TARGET_removed , 'RedefineChildNodeToContainer', null
+ 'CASE_FLAG_ON_TARGET_removed RedefineChildNode' => [
+ CASE_FLAG_ON_TARGET_removed,
+ 'RedefineChildNodeToContainer',
+ null,
],
- 'CASE_FLAG_ON_TARGET_old_id RedefineChildNode' => [CASE_FLAG_ON_TARGET_old_id , 'RedefineChildNodeToContainer', << [
+ CASE_FLAG_ON_TARGET_old_id,
+ 'RedefineChildNodeToContainer',
+ <<
+
+ Hello, I'm replacing the previous node
+
+
+XML
+ ,
+ ],
+ 'CASE_MISSING_TARGET RedefineChildNode' => [
+ CASE_MISSING_TARGET,
+ 'RedefineChildNodeToContainer',
+ null,
+ ],
+ 'CASE_NO_FLAG SetChildNode' => [
+ CASE_NO_FLAG,
+ 'SetChildNodeToContainer',
+ <<
Hello, I'm replacing the previous node
XML
+ ,
],
- 'CASE_MISSING_TARGET RedefineChildNode' => [CASE_MISSING_TARGET , 'RedefineChildNodeToContainer', null
- ],
- 'CASE_NO_FLAG SetChildNode' => [CASE_NO_FLAG , 'SetChildNodeToContainer', << [
+ CASE_ABOVE_A_FLAG,
+ 'SetChildNodeToContainer',
+ <<
Hello, I'm replacing the previous node
XML
+ ,
],
- 'CASE_ABOVE_A_FLAG SetChildNode' => [CASE_ABOVE_A_FLAG , 'SetChildNodeToContainer', <<
-
- Hello, I'm replacing the previous node
-
-
-XML
- ],
- 'CASE_IN_A_DEFINITION SetChildNode' => [CASE_IN_A_DEFINITION , 'SetChildNodeToContainer', << [
+ CASE_IN_A_DEFINITION,
+ 'SetChildNodeToContainer',
+ <<