diff --git a/setup/itopdesignformat.class.inc.php b/setup/itopdesignformat.class.inc.php
index ccc268b45..819156c6f 100644
--- a/setup/itopdesignformat.class.inc.php
+++ b/setup/itopdesignformat.class.inc.php
@@ -17,6 +17,8 @@
* You should have received a copy of the GNU Affero General Public License
*/
+use Combodo\iTop\DesignDocument;
+
/**
* Utility to upgrade the format of a given XML datamodel to the latest version
@@ -109,7 +111,9 @@ class iTopDesignFormat
*/
protected $aLog;
protected $bStatus;
-
+ protected $bKeepObsoleteNodes;
+ protected $sKeepVersion;
+
/**
* Creation from a loaded DOMDocument
* @param DOMDocument $oDocument The document to transform
@@ -210,6 +214,20 @@ class iTopDesignFormat
return self::GetItopNodePath($oNode->parentNode).'/'.$sNodeDesc;
}
+ /**
+ * Compute a real xpath from iTop one
+ *
+ * @param \Combodo\iTop\DesignElement $oNode
+ *
+ * @return string
+ */
+ public static function GetNodeXPath($oNode)
+ {
+ $sITopXPath = DesignDocument::GetItopNodePath($oNode);
+
+ return preg_replace(["@\[@", "@]@"], ["[@id=\"", "\"]"], $sITopXPath);
+ }
+
/**
* Test the conversion without altering the DOM
*
@@ -227,19 +245,21 @@ class iTopDesignFormat
}
/**
- * Make adjustements to the DOM to migrate it to the specified version (default is latest)
+ * Make adjustments to the DOM to migrate it to the specified version (default is latest)
* For now only the conversion from version 1.0 to 1.1 is supported.
*
* @param string $sTargetVersion The desired version (or the latest possible version if not specified)
- * @param object $oFactory Full data model (not yet used, aimed at allowing conversion that could not be performed without knowing the
+ * @param \ModelFactory|null $oFactory Full data model (not yet used, aimed at allowing conversion that could not be performed without knowing the
* whole data model)
+ * @param bool $bKeepObsoleteNodes
*
* @return bool True on success, False if errors have been encountered (still the DOM may be altered!)
*/
- public function Convert($sTargetVersion = ITOP_DESIGN_LATEST_VERSION, $oFactory = null)
+ public function Convert($sTargetVersion = ITOP_DESIGN_LATEST_VERSION, $oFactory = null, $bKeepObsoleteNodes = true)
{
$this->aLog = array();
$this->bStatus = true;
+ $this->bKeepObsoleteNodes = $bKeepObsoleteNodes;
$oXPath = new DOMXPath($this->oDocument);
// Retrieve the version number
@@ -304,6 +324,7 @@ class iTopDesignFormat
$sIntermediate = self::$aVersions[$sFrom]['next'];
$sTransform = self::$aVersions[$sFrom]['go_to_next'];
$this->LogInfo("Upgrading from $sFrom to $sIntermediate ($sTransform)");
+ $this->sKeepVersion = $sFrom;
}
else
{
@@ -311,12 +332,16 @@ class iTopDesignFormat
$sIntermediate = self::$aVersions[$sFrom]['previous'];
$sTransform = self::$aVersions[$sFrom]['go_to_previous'];
$this->LogInfo("Downgrading from $sFrom to $sIntermediate ($sTransform)");
+ $this->sKeepVersion = null;
}
// Transform to the intermediate format
$aCallSpec = array($this, $sTransform);
try
{
call_user_func($aCallSpec, $oFactory);
+ if ($iFrom > $iTo && $this->bKeepObsoleteNodes) {
+ $this->RestorePreviousNodes($sIntermediate);
+ }
// Recurse
$this->DoConvert($sIntermediate, $sTo, $oFactory);
@@ -325,7 +350,6 @@ class iTopDesignFormat
{
$this->LogError($e->getMessage());
}
- return;
}
/**
@@ -947,13 +971,78 @@ class iTopDesignFormat
}
}
}
-
+
/**
- * @param string $sPath
+ * @param string $sNodeMetaVersion
*
* @return void
*/
- private function RemoveNodeFromXPath($sPath)
+ private function RestorePreviousNodes($sNodeMetaVersion)
+ {
+ $oXPath = new DOMXPath($this->oDocument);
+ $oTrashedNodes = $oXPath->query("/itop_design/meta/previous_versions/previous_version[@id='$sNodeMetaVersion']/trashed_nodes/trashed_node");
+ foreach ($oTrashedNodes as $oTrashedNode) {
+ if ($oTrashedNode->nodeType == XML_ELEMENT_NODE) {
+ $oXPathNode = $oXPath->query('parent_xpath', $oTrashedNode)->item(0);
+ $oNodeTreeNode = $oXPath->query('node_tree', $oTrashedNode)->item(0);
+ if (!is_null($oXPathNode) && !is_null($oNodeTreeNode)) {
+ $sXPath = $this->GetText($oXPathNode, '');
+ $oParentNode = $oXPath->query($sXPath)->item(0);
+ if ($oParentNode) {
+ $oNode = $oNodeTreeNode->firstChild;
+ while ($oNode) {
+ $oNextNode = $oNode->nextSibling;
+ if ($oNode->nodeType == XML_ELEMENT_NODE) {
+ // Restore the modification flags
+ $oModifiedNodeList = $oXPath->query('descendant-or-self::*[@_disabled_delta or @_disabled_rename_from]', $oNode);
+ foreach ($oModifiedNodeList as $oModifiedNode) {
+ foreach (['_delta', '_rename_from'] as $sModificationFlag) {
+ $sCurrentFlag = $oNode->getAttribute('_disabled'.$sModificationFlag);
+ if (!empty($sCurrentFlag)) {
+ $oModifiedNode->setAttribute($sModificationFlag, $sCurrentFlag);
+ $oModifiedNode->removeAttribute('_disabled'.$sModificationFlag);
+ }
+ }
+ }
+ // Move the node back in place
+ $oParentNode->appendChild($oNode);
+ }
+ $oNode = $oNextNode;
+ }
+ }
+ }
+ }
+ }
+ // Clean up the mess
+ $this->RemoveNodeFromXPath("/itop_design/meta/previous_versions/previous_version[@id='$sNodeMetaVersion']", false);
+ $this->RemoveEmptyNodeFromXPath("/itop_design/meta/previous_versions");
+ $this->RemoveEmptyNodeFromXPath("/itop_design/meta");
+ }
+
+ private function RemoveEmptyNodeFromXPath($sXPath, $bStoreThisNodeInMetaVersion = false)
+ {
+ $oXPath = new DOMXPath($this->oDocument);
+ $oNodeToRemove = $oXPath->query($sXPath)->item(0);
+ if (is_null($oNodeToRemove)) {
+ return;
+ }
+ $oNode = $oNodeToRemove->firstChild;
+ while ($oNode) {
+ if ($oNode->nodeType == XML_ELEMENT_NODE) {
+ return;
+ }
+ $oNode = $oNode->nextSibling;
+ }
+ $this->RemoveNodeFromXPath($sXPath, $bStoreThisNodeInMetaVersion);
+ }
+
+ /**
+ * @param string $sPath
+ * @param bool $bStoreThisNodeInMetaVersion
+ *
+ * @return void
+ */
+ private function RemoveNodeFromXPath($sPath, $bStoreThisNodeInMetaVersion = true)
{
$oXPath = new DOMXPath($this->oDocument);
@@ -961,10 +1050,68 @@ class iTopDesignFormat
foreach ($oNodeList as $oNode)
{
$this->LogWarning('Node '.self::GetItopNodePath($oNode).' is irrelevant in this version, it will be removed.');
- $this->DeleteNode($oNode);
+ if ($bStoreThisNodeInMetaVersion && $this->bKeepObsoleteNodes && $this->sKeepVersion) {
+ // Move the node to to keep it safe for backward migration
+ $oItopDesignNode = $this->GetOrCreateNode('/itop_design', 'itop_design', null);
+ $oMetaNode = $this->GetOrCreateNode('meta', 'meta', $oItopDesignNode);
+ $oPreviousVersionsNode = $this->GetOrCreateNode('previous_versions', 'previous_versions', $oMetaNode);
+ $oPreviousVersionNode = $this->GetOrCreateNode("previous_version[@id='$this->sKeepVersion']", 'previous_version', $oPreviousVersionsNode);
+ $oPreviousVersionNode->setAttribute('id', $this->sKeepVersion);
+ $oPreviousVersionNode->setAttribute('_delta', 'define');
+ $oTrashedNodeList = $this->GetOrCreateNode('trashed_nodes', 'trashed_nodes', $oPreviousVersionNode);
+
+ $iMaxIndex = 0;
+ $oTrashedNodes = $oXPath->query('trashed_node', $oTrashedNodeList);
+ foreach ($oTrashedNodes as $oTrashedNode) {
+ if ($oTrashedNode->nodeType == XML_ELEMENT_NODE) {
+ $iId = $oTrashedNode->getAttribute('id');
+ if ($iId > $iMaxIndex) {
+ $iMaxIndex = $iId;
+ }
+ }
+ }
+ $iNextId = $iMaxIndex + 1;
+ $oTrashedNode = $this->GetOrCreateNode("trashed_node[@id='$iNextId']", 'trashed_node', $oTrashedNodeList);
+ $oTrashedNode->setAttribute('id', $iNextId);
+
+ $oXPathNode = $this->GetOrCreateNode('parent_xpath', 'parent_xpath', $oTrashedNode);
+ $oParentNode = $oNode->parentNode;
+ if ($oParentNode instanceof DOMElement) {
+ $sParentXPath = static::GetNodeXPath($oParentNode);
+ $oXPathNode->appendChild(new DOMText($sParentXPath));
+ }
+ $oNodeTreeNode = $this->GetOrCreateNode('node_tree', 'node_tree', $oTrashedNode);
+
+ // Store the modification flags
+ $oModifiedNodeList = $oXPath->query('descendant-or-self::*[@_delta or @_rename_from]', $oNode);
+ foreach ($oModifiedNodeList as $oModifiedNode) {
+ foreach (['_delta', '_rename_from'] as $sModificationFlag) {
+ $sCurrentFlag = $oNode->getAttribute($sModificationFlag);
+ if (!empty($sCurrentFlag)) {
+ $oModifiedNode->setAttribute('_disabled'.$sModificationFlag, $sCurrentFlag);
+ $oModifiedNode->removeAttribute($sModificationFlag);
+ }
+ }
+ }
+
+ $oNodeTreeNode->appendChild($oNode);
+ } else {
+ $this->DeleteNode($oNode);
+ }
}
}
+ private function GetOrCreateNode($sXPath, $sName, $oRootNode)
+ {
+ $oXPath = new DOMXPath($this->oDocument);
+ $oNode = $oXPath->query($sXPath, $oRootNode)->item(0);
+ if (is_null($oNode)) {
+ $oNode = $oRootNode->ownerDocument->createElement($sName);
+ $oRootNode->appendChild($oNode);
+ }
+ return $oNode;
+ }
+
/**
* Clean a collection node by removing the _delta="define" on it and moving it to the item nodes.
*
@@ -1151,4 +1298,31 @@ class iTopDesignFormat
return null;
}
+
+ /**
+ * Returns the TEXT of the current node (possibly from several child nodes)
+ * @param null $sDefault
+ * @return null|string
+ */
+ protected function GetText($oNode, $sDefault = null)
+ {
+ $sText = null;
+ foreach($oNode->childNodes as $oChildNode)
+ {
+ if ($oChildNode instanceof \DOMText)
+ {
+ if (is_null($sText)) $sText = '';
+ $sText .= $oChildNode->wholeText;
+ }
+ }
+ if (is_null($sText))
+ {
+ return $sDefault;
+ }
+ else
+ {
+ return $sText;
+ }
+ }
+
}
diff --git a/test/setup/iTopDesignFormat/Convert-samples/1.6_to_1.7_acl.expected.xml b/test/setup/iTopDesignFormat/Convert-samples/1.6_to_1.7_acl.expected.xml
new file mode 100644
index 000000000..6f4f3dff9
--- /dev/null
+++ b/test/setup/iTopDesignFormat/Convert-samples/1.6_to_1.7_acl.expected.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ service_id AND child.id= :this->org_id AND slt.request_type = :request_type AND slt.priority = :this->priority]]>
+
+
+
+
+
+
+ /itop_design/constants
+
+
+
+
+
+ /itop_design/constants
+
+
+
+
+
+
+
+
+
diff --git a/test/setup/iTopDesignFormat/Convert-samples/1.6_to_1.7_acl.input.xml b/test/setup/iTopDesignFormat/Convert-samples/1.6_to_1.7_acl.input.xml
new file mode 100644
index 000000000..080650363
--- /dev/null
+++ b/test/setup/iTopDesignFormat/Convert-samples/1.6_to_1.7_acl.input.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ service_id AND child.id= :this->org_id AND slt.request_type = :request_type AND slt.priority = :this->priority]]>
+
+
+
+
diff --git a/test/setup/iTopDesignFormat/Convert-samples/1.7_to_1.6_acl.expected.xml b/test/setup/iTopDesignFormat/Convert-samples/1.7_to_1.6_acl.expected.xml
new file mode 100644
index 000000000..673f9865b
--- /dev/null
+++ b/test/setup/iTopDesignFormat/Convert-samples/1.7_to_1.6_acl.expected.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ service_id AND child.id= :this->org_id AND slt.request_type = :request_type AND slt.priority = :this->priority]]>
+
+
+
+
diff --git a/test/setup/iTopDesignFormat/Convert-samples/1.7_to_1.6_acl.input.xml b/test/setup/iTopDesignFormat/Convert-samples/1.7_to_1.6_acl.input.xml
new file mode 100644
index 000000000..6f4f3dff9
--- /dev/null
+++ b/test/setup/iTopDesignFormat/Convert-samples/1.7_to_1.6_acl.input.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ service_id AND child.id= :this->org_id AND slt.request_type = :request_type AND slt.priority = :this->priority]]>
+
+
+
+
+
+
+ /itop_design/constants
+
+
+
+
+
+ /itop_design/constants
+
+
+
+
+
+
+
+
+
diff --git a/test/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.expected.xml b/test/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.expected.xml
index f3ddfdd1b..7694137e2 100644
--- a/test/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.expected.xml
+++ b/test/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.expected.xml
@@ -106,4 +106,45 @@
+
+
+
+
+
+ /itop_design/branding/themes
+
+
+
+
+ ../css/css-variables.scss
+
+
+ ../css/ui-lightness/jqueryui.scss
+ ../css/light-grey.scss
+
+
+
+
+
+ /itop_design/branding/themes/theme[@id="test-red"]/imports
+
+ ../css/css-variables.scss
+
+
+
+ /itop_design/branding/themes/theme[@id="test-red"]/stylesheets
+
+ ../css/ui-lightness/jqueryui.scss
+
+
+
+ /itop_design/branding/themes/theme[@id="test-red"]/stylesheets
+
+ ../css/main.scss
+
+
+
+
+
+
diff --git a/test/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.input.xml b/test/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.input.xml
index 467e02660..b7bc5de22 100644
--- a/test/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.input.xml
+++ b/test/setup/iTopDesignFormat/Convert-samples/1.7_to_3.0.input.xml
@@ -17,6 +17,16 @@
../css/custom.scss
+
+
+
+ ../css/css-variables.scss
+
+
+ ../css/ui-lightness/jqueryui.scss
+ ../css/light-grey.scss
+
+
diff --git a/test/setup/iTopDesignFormat/Convert-samples/3.0_to_1.7.expected.xml b/test/setup/iTopDesignFormat/Convert-samples/3.0_to_1.7.expected.xml
index 9ced4254b..68499ba8c 100644
--- a/test/setup/iTopDesignFormat/Convert-samples/3.0_to_1.7.expected.xml
+++ b/test/setup/iTopDesignFormat/Convert-samples/3.0_to_1.7.expected.xml
@@ -9,9 +9,22 @@
../css/scss-variables.scss
+ ../css/css-variables.scss
../css/custom.scss
+ ../css/ui-lightness/jqueryui.scss
+ ../css/main.scss
+
+
+
+
+
+ ../css/css-variables.scss
+
+
+ ../css/ui-lightness/jqueryui.scss
+ ../css/light-grey.scss
diff --git a/test/setup/iTopDesignFormat/Convert-samples/3.0_to_1.7.input.xml b/test/setup/iTopDesignFormat/Convert-samples/3.0_to_1.7.input.xml
index 56bc62c19..836ce6ade 100644
--- a/test/setup/iTopDesignFormat/Convert-samples/3.0_to_1.7.input.xml
+++ b/test/setup/iTopDesignFormat/Convert-samples/3.0_to_1.7.input.xml
@@ -133,4 +133,45 @@
images/itop-logo.png
images/itop-logo-square.png
+
+
+
+
+
+ /itop_design/branding/themes
+
+
+
+
+ ../css/css-variables.scss
+
+
+ ../css/ui-lightness/jqueryui.scss
+ ../css/light-grey.scss
+
+
+
+
+
+ /itop_design/branding/themes/theme[@id="test-red"]/imports
+
+ ../css/css-variables.scss
+
+
+
+ /itop_design/branding/themes/theme[@id="test-red"]/stylesheets
+
+ ../css/ui-lightness/jqueryui.scss
+
+
+
+ /itop_design/branding/themes/theme[@id="test-red"]/stylesheets
+
+ ../css/main.scss
+
+
+
+
+
+
diff --git a/test/setup/iTopDesignFormat/Convert-samples/3.0_to_1.7_no_previous.expected.xml b/test/setup/iTopDesignFormat/Convert-samples/3.0_to_1.7_no_previous.expected.xml
new file mode 100644
index 000000000..9ced4254b
--- /dev/null
+++ b/test/setup/iTopDesignFormat/Convert-samples/3.0_to_1.7_no_previous.expected.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+ #C53030
+ #C53030
+
+
+ ../css/scss-variables.scss
+
+
+ ../css/custom.scss
+
+
+
+
+
+
+
+ images/class-with-lifecycle.png
+
+
+
+
+ images/class-with-lifecycle.png
+
+
+ foo
+
+
+
+
+ images/class-with-lifecycle.png
+
+
+
+
+ images/class-with-lifecycle.png
+
+
+
+
+
+
+
+
+
+
+ true
+
+ new
+ waiting_for_approval
+
+
+
+
+ ongoing
+ resolved
+
+
+
+
+
+
+
+
+
+
+ images/itop-logo.png
+
+
diff --git a/test/setup/iTopDesignFormat/Convert-samples/3.0_to_1.7_no_previous.input.xml b/test/setup/iTopDesignFormat/Convert-samples/3.0_to_1.7_no_previous.input.xml
new file mode 100644
index 000000000..56bc62c19
--- /dev/null
+++ b/test/setup/iTopDesignFormat/Convert-samples/3.0_to_1.7_no_previous.input.xml
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+ #C53030
+ #C53030
+
+
+ ../css/scss-variables.scss
+
+
+ ../css/custom.scss
+
+
+
+
+
+
+
+
+
+ foo
+
+
+
+
+
+
+
+ foo
+
+
+
+
+
+
+
+
+ foo
+ bar
+
+
+
+
+
+
+
+ bar
+
+
+
+
+
+
+
+
+
+ true
+
+
+ new
+
+
+
+ waiting_for_approval
+
+
+
+ #2B6CB0
+ #FFFFFF
+
+
+
+
+
+
+ ongoing
+
+
+
+ resolved
+
+
+
+ #2B6CB0
+ #FFFFFF
+
+
+
+
+
+
+
+
+
+
+
+ images/itop-logo.png
+ images/itop-logo-square.png
+
+
diff --git a/test/setup/iTopDesignFormat/iTopDesignFormatTest.php b/test/setup/iTopDesignFormat/iTopDesignFormatTest.php
index f26cb1dc8..c5f28bf49 100644
--- a/test/setup/iTopDesignFormat/iTopDesignFormatTest.php
+++ b/test/setup/iTopDesignFormat/iTopDesignFormatTest.php
@@ -56,9 +56,12 @@ class TestForITopDesignFormatClass extends ItopTestCase
public function ConvertProvider()
{
return array(
+ '1.6 to 1.7 acl' => array('1.7', '1.6_to_1.7_acl'),
+ '1.7 to 1.6 acl' => array('1.6', '1.7_to_1.6_acl'),
'1.7 to 1.6' => array('1.6', '1.7_to_1.6'),
'1.7 to 3.0' => array('3.0', '1.7_to_3.0'),
'3.0 to 1.7' => array('1.7', '3.0_to_1.7'),
+ '3.0 to 1.7 no previous' => array('1.7', '3.0_to_1.7_no_previous'),
);
}