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 + + + + + + + + 100 + WelcomeMenu + $$http://fr.wikipedia.org/ + true + + + 30 + + + + + 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 + + + + + + + + + 100 + WelcomeMenu + $$http://fr.wikipedia.org/ + true + + + 30 + + + + + + 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'), ); }