diff --git a/.make/release/update.classes.inc.php b/.make/release/update.classes.inc.php index 8f1499439..5b67edad3 100644 --- a/.make/release/update.classes.inc.php +++ b/.make/release/update.classes.inc.php @@ -199,7 +199,7 @@ class DatamodelsXmlFiles extends AbstractGlobFileVersionUpdater libxml_clear_errors(); $oFileXml->formatOutput = true; $oFileXml->preserveWhiteSpace = false; - $oFileXml->loadXML($sFileContent); + $oFileXml->loadXML($sFileContent, LIBXML_BIGLINES); $oFileItopFormat = new iTopDesignFormat($oFileXml); diff --git a/application/forms.class.inc.php b/application/forms.class.inc.php index ba5dc97dc..d1fd70fc8 100644 --- a/application/forms.class.inc.php +++ b/application/forms.class.inc.php @@ -319,6 +319,7 @@ EOF if ($sIntroduction != null) { $oPage->add('
'.$sIntroduction.'
'); } + $oPage->add('
'); $this->Render($oPage); $oPage->add(''); diff --git a/core/cmdbsource.class.inc.php b/core/cmdbsource.class.inc.php index b688bc2bc..8b17a931d 100644 --- a/core/cmdbsource.class.inc.php +++ b/core/cmdbsource.class.inc.php @@ -584,12 +584,12 @@ class CMDBSource $oResult = DbConnectionWrapper::GetDbConnection(true)->query($sSql); } catch (mysqli_sql_exception $e) { self::LogDeadLock($e, true); - throw new MySQLException('Failed to issue SQL query', ['query' => $sSql, $e]); + throw new MySQLException('Failed to issue SQL query', ['query' => $sSql, $e, 'stack' => $e->getTraceAsString()]); } finally { $oKPI->ComputeStats('Query exec (mySQL)', $sSql); } if ($oResult === false) { - $aContext = ['query' => $sSql]; + $aContext = ["\nstack" => (new Exception(''))->getTraceAsString(), "\nquery" => $sSql]; $iMySqlErrorNo = DbConnectionWrapper::GetDbConnection(true)->errno; $aMySqlHasGoneAwayErrorCodes = MySQLHasGoneAwayException::getErrorCodes(); diff --git a/core/datamodel.core.xml b/core/datamodel.core.xml index cdbcaf002..872b4cef8 100644 --- a/core/datamodel.core.xml +++ b/core/datamodel.core.xml @@ -2,9 +2,10 @@ - cmdbAbstractObject + DBObject - core/cmdb,application + 1 + core/cmdb,view_in_gui false autoincrement priv_lnk_action_notif_to_contact @@ -42,18 +43,21 @@ ActionNotification false + DEL_AUTO contact_id Contact false + DEL_AUTO trigger_id Trigger false + DEL_AUTO subscribed @@ -66,18 +70,17 @@ - - - - 10 - - - 20 - - - 30 - - + + 10 + + + 20 + + + 30 + + + 40 @@ -91,11 +94,30 @@ 20 - + 30 + + 40 + + + + + 10 + + + 20 + + + 30 + + + 40 + + + diff --git a/core/designdocument.class.inc.php b/core/designdocument.class.inc.php index 1606439dc..f3afab27b 100644 --- a/core/designdocument.class.inc.php +++ b/core/designdocument.class.inc.php @@ -70,6 +70,11 @@ class DesignDocument extends DOMDocument $this->preserveWhiteSpace = true; // otherwise the formatOutput option would have no effect } + public function loadXML(string $source, int $options = 0) + { + return parent::loadXML($source, $options | LIBXML_BIGLINES); + } + /** * Overload of the standard API * diff --git a/core/metamodel.class.php b/core/metamodel.class.php index c5d6ae1be..318fe3775 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -6119,12 +6119,14 @@ abstract class MetaModel if ($bMustBeFound && empty($aRow)) { $sNotFoundErrorMessage = "No result for the single row query"; - IssueLog::Info($sNotFoundErrorMessage, LogChannels::CMDB_SOURCE, [ + $e = new CoreException($sNotFoundErrorMessage); + IssueLog::Error($sNotFoundErrorMessage, LogChannels::CMDB_SOURCE, [ 'class' => $sClass, 'key' => $iKey, 'sql_query' => $sSQL, + 'stack' => $e->getTraceAsString(), ]); - throw new CoreException($sNotFoundErrorMessage); + throw $e; } return $aRow; diff --git a/dictionaries/en.dictionary.itop.core.php b/dictionaries/en.dictionary.itop.core.php index a03ab7a7e..582c37e69 100644 --- a/dictionaries/en.dictionary.itop.core.php +++ b/dictionaries/en.dictionary.itop.core.php @@ -562,7 +562,24 @@ Dict::Add('EN US', 'English', 'English', [ 'Class:ActionNotification' => 'Notification Action', 'Class:ActionNotification+' => 'Notification Action (abstract)', 'Class:ActionNotification/Attribute:language' => 'Language', - 'Class:ActionNotification/Attribute:language+' => '', + 'Class:ActionNotification/Attribute:language+' => 'Language to use for placeholders ($xxx$) inside the message (state, importance, priority, etc)', +]); + +// +// Class: lnkActionNotificationToContact +// + +Dict::Add('EN US', 'English', 'English', [ + 'Class:lnkActionNotificationToContact' => 'Link ActionNotification / Contact', + 'Class:lnkActionNotificationToContact+' => 'Contact subscription to Notification Action', + 'Class:lnkActionNotificationToContact/Attribute:contact_id' => 'Contact', + 'Class:lnkActionNotificationToContact/Attribute:contact_id+' => 'Contact who subscribed (or not) to the notification', + 'Class:lnkActionNotificationToContact/Attribute:action_id' => 'Action', + 'Class:lnkActionNotificationToContact/Attribute:action_id+' => 'The notification that the contact received at least once, and to which he can subscribe or unsubscribe', + 'Class:lnkActionNotificationToContact/Attribute:trigger_id' => 'Trigger', + 'Class:lnkActionNotificationToContact/Attribute:trigger_id+' => 'The trigger that fired the notification', + 'Class:lnkActionNotificationToContact/Attribute:subscribed' => 'Subscribed', + 'Class:lnkActionNotificationToContact/Attribute:subscribed+' => 'If the contact unsubscribed (no) or is subscribed (yes and default) to the notification', ]); // diff --git a/dictionaries/en_gb.dictionary.itop.core.php b/dictionaries/en_gb.dictionary.itop.core.php index e02fa6352..330ed0744 100644 --- a/dictionaries/en_gb.dictionary.itop.core.php +++ b/dictionaries/en_gb.dictionary.itop.core.php @@ -545,7 +545,24 @@ Dict::Add('EN GB', 'British English', 'British English', [ 'Class:ActionNotification' => 'Notification Action', 'Class:ActionNotification+' => 'Notification Action (abstract)', 'Class:ActionNotification/Attribute:language' => 'Language', - 'Class:ActionNotification/Attribute:language+' => '', + 'Class:ActionNotification/Attribute:language+' => 'Language to use for placeholders ($xxx$) inside the message (state, importance, priority, etc)', +]); + +// +// Class: lnkActionNotificationToContact +// + +Dict::Add('EN GB', 'British English', 'British English', [ + 'Class:lnkActionNotificationToContact' => 'Link ActionNotification / Contact', + 'Class:lnkActionNotificationToContact+' => 'Contact subscription to Notification Action', + 'Class:lnkActionNotificationToContact/Attribute:contact_id' => 'Contact', + 'Class:lnkActionNotificationToContact/Attribute:contact_id+' => 'Contact who subscribed (or not) to the notification', + 'Class:lnkActionNotificationToContact/Attribute:action_id' => 'Action', + 'Class:lnkActionNotificationToContact/Attribute:action_id+' => 'The notification that the contact received at least once, and to which he can subscribe or unsubscribe', + 'Class:lnkActionNotificationToContact/Attribute:trigger_id' => 'Trigger', + 'Class:lnkActionNotificationToContact/Attribute:trigger_id+' => 'The trigger that fired the notification', + 'Class:lnkActionNotificationToContact/Attribute:subscribed' => 'Subscribed', + 'Class:lnkActionNotificationToContact/Attribute:subscribed+' => 'If the contact unsubscribed (no) or is subscribed (yes and default) to the notification', ]); // diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php index 05049363b..cc675b318 100644 --- a/dictionaries/fr.dictionary.itop.core.php +++ b/dictionaries/fr.dictionary.itop.core.php @@ -503,7 +503,24 @@ Dict::Add('FR FR', 'French', 'Français', [ 'Class:ActionNotification' => 'Action de notification', 'Class:ActionNotification+' => '', 'Class:ActionNotification/Attribute:language' => 'Langue', - 'Class:ActionNotification/Attribute:language+' => '', + 'Class:ActionNotification/Attribute:language+' => 'Langue utilisée pour les placeholders ($xxx$) dans le message (statut, importance, priorité, etc)', +]); + +// +// Class: lnkActionNotificationToContact +// + +Dict::Add('FR FR', 'French', 'Français', [ + 'Class:lnkActionNotificationToContact' => 'Lien Action de Notification / Contact', + 'Class:lnkActionNotificationToContact+' => 'Abonnement des contacts aux notifications', + 'Class:lnkActionNotificationToContact/Attribute:contact_id' => 'Contact', + 'Class:lnkActionNotificationToContact/Attribute:contact_id+' => 'Contact abonné à la notification', + 'Class:lnkActionNotificationToContact/Attribute:action_id' => 'Action', + 'Class:lnkActionNotificationToContact/Attribute:action_id+' => 'La notification à laquelle le contact est abonné', + 'Class:lnkActionNotificationToContact/Attribute:trigger_id' => 'Déclencheur', + 'Class:lnkActionNotificationToContact/Attribute:trigger_id+' => 'Le déclencheur à l\'origine de cette notification', + 'Class:lnkActionNotificationToContact/Attribute:subscribed' => 'Abonné', + 'Class:lnkActionNotificationToContact/Attribute:subscribed+' => 'Si le contact est abonné ou non à cette notification', ]); // diff --git a/dictionaries/zh_cn.dictionary.itop.core.php b/dictionaries/zh_cn.dictionary.itop.core.php index 142948e14..ced119b56 100644 --- a/dictionaries/zh_cn.dictionary.itop.core.php +++ b/dictionaries/zh_cn.dictionary.itop.core.php @@ -505,6 +505,23 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [ 'Class:ActionNotification/Attribute:language+' => '', ]); +// +// Class: lnkActionNotificationToContact +// + +Dict::Add('ZH CN', 'Chinese', '简体中文', [ + 'Class:lnkActionNotificationToContact' => 'Link ActionNotification / Contact~~', + 'Class:lnkActionNotificationToContact+' => 'Contact subscription to Notification Action~~', + 'Class:lnkActionNotificationToContact/Attribute:contact_id' => 'Contact~~', + 'Class:lnkActionNotificationToContact/Attribute:contact_id+' => 'Contact who subscribed (or not) to the notification~~', + 'Class:lnkActionNotificationToContact/Attribute:action_id' => 'Action~~', + 'Class:lnkActionNotificationToContact/Attribute:action_id+' => 'The notification that the contact received at least once, and to which he can subscribe or unsubscribe~~', + 'Class:lnkActionNotificationToContact/Attribute:trigger_id' => 'Trigger~~', + 'Class:lnkActionNotificationToContact/Attribute:trigger_id+' => 'The trigger that fired the notification~~', + 'Class:lnkActionNotificationToContact/Attribute:subscribed' => 'Subscribed~~', + 'Class:lnkActionNotificationToContact/Attribute:subscribed+' => 'If the contact unsubscribed (no) or is subscribed (yes and default) to the notification~~', +]); + // // Class: ActionEmail // diff --git a/js/links/links_widget.js b/js/links/links_widget.js index 8c35cbb04..430d33890 100644 --- a/js/links/links_widget.js +++ b/js/links/links_widget.js @@ -390,16 +390,17 @@ function LinksWidget(id, sClass, sAttCode, iInputId, sSuffix, bDuplicates, oWizH this.RegisterChange = function () { // Listen only used inputs - $('#linkedset_'+me.id+' :input[name^="attr_'+me.sAttCode+'["]').off('change').on('change', function () { + $('body').off('change', '#linkedset_'+me.id+' :input[name^="attr_'+me.sAttCode+'["]') + .on('change', '#linkedset_'+me.id+' :input[name^="attr_'+me.sAttCode+'["]', function () { if (!($(this).hasClass('selection'))) - { - let oCheckbox = $(this).closest('tr').find('.selection'); - let iLink = oCheckbox.attr('data-link-id'); - let iUniqueId = oCheckbox.attr('data-unique-id'); - let sAttCode = $(this).closest('.attribute-edit').attr('data-attcode'); - let value = $(this).val();; - return me.OnValueChange(iLink, iUniqueId, sAttCode, value, this); - } + { + let oCheckbox = $(this).closest('tr').find('.selection'); + let iLink = oCheckbox.attr('data-link-id'); + let iUniqueId = oCheckbox.attr('data-unique-id'); + let sAttCode = $(this).closest('.attribute-edit').attr('data-attcode'); + let value = $(this).val();; + return me.OnValueChange(iLink, iUniqueId, sAttCode, value, this); + } return true; }); }; diff --git a/js/property_field.js b/js/property_field.js index 88a204a86..cd51fd585 100644 --- a/js/property_field.js +++ b/js/property_field.js @@ -548,6 +548,7 @@ function ValidateWithPattern(sFieldId, bMandatory, sPattern, sFormId, aForbidden if (sMessage) { $('#'+sFieldId).attr('data-tooltip-content', sMessage); + $('#'+sFieldId).attr('data-tooltip-theme', 'error'); CombodoTooltip.InitTooltipFromMarkup($('#'+sFieldId), true); $('#'+sFieldId)[0]._tippy.show(); } diff --git a/setup/backup.class.inc.php b/setup/backup.class.inc.php index c2214f7d4..92f380b0f 100644 --- a/setup/backup.class.inc.php +++ b/setup/backup.class.inc.php @@ -595,13 +595,13 @@ EOF; $sMySQLCommand = $sCmd; } else { $sMySQLBinDir = escapeshellcmd($sMySQLBinDir); - $sMySQLCommand = '"'.$sMySQLBinDir.'/$sCmd"'; + $sMySQLCommand = $sMySQLBinDir.'/'.$sCmd; if (!file_exists($sMySQLCommand)) { throw new BackupException("$sCmd not found in $sMySQLBinDir"); } } - return $sMySQLCommand; + return '"'.$sMySQLCommand.'"'; } } diff --git a/setup/modelfactory.class.inc.php b/setup/modelfactory.class.inc.php index b1919b41d..f4f48e3fc 100644 --- a/setup/modelfactory.class.inc.php +++ b/setup/modelfactory.class.inc.php @@ -70,14 +70,37 @@ class MFException extends Exception * MFException constructor. * * @inheritDoc + * + * @param $message + * @param $code: error code + * @param $oNode: dom node + * @param $sXPath: XML xpath: if provided used in exception message. otherwise computed via $oNode + * @param $sExtraInfo: additional information stored in exception + * @param $oParentFallbackNode: fallback dom node (usually parent). in case $oNode XML line is wrong (set to 0), line number computed/displayed in error message comes from $oParentFallbackNode */ - public function __construct($message = null, $code = 0, $iSourceLineNumber = 0, $sXPath = '', $sExtraInfo = '', $previous = null) + public function __construct($message = null, $code = 0, $oNode, $sXPath = null, $sExtraInfo = '', $oParentFallbackNode = null) { - parent::__construct($message, $code, $previous); + $iSourceLineNumber = ModelFactory::GetXMLLineNumber($oNode); + if ($iSourceLineNumber == 0 && ! is_null($oParentFallbackNode)) { + $iSourceLineNumber = ModelFactory::GetXMLLineNumber($oParentFallbackNode); + } + + if (is_null($sXPath)) { + $sXPath = DesignDocument::GetItopNodePath($oNode); + } + $this->iSourceLineNumber = $iSourceLineNumber; $this->iSourceLineOffset = 0; $this->sXPath = $sXPath; $this->sExtraInfo = $sExtraInfo; + parent::__construct("$sXPath at line $iSourceLineNumber: $message", $code); + + $aContext = [ + 'error' => $code, + 'stack' => $this->getTraceAsString(), + 'extra_info' => $sExtraInfo, + ]; + \IssueLog::Error($this->getMessage(), null, $aContext); } /** @@ -196,6 +219,10 @@ class MFModule return; } + if (!is_dir($sRootDir)) { + $sRootDir = APPROOT.$sRootDir; + } + // Scan the module's root directory to find the datamodel(*).xml files if ($hDir = opendir($sRootDir)) { // This is the correct way to loop over the directory. (according to the documentation) @@ -735,14 +762,7 @@ class ModelFactory 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 - ); + throw new MFException("_delta=\"$sParentDeltaSpec\" not supported for classes in hierarchy", MFException::NOT_FOUND, $oParentNode); } } @@ -798,14 +818,7 @@ class ModelFactory // 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 - ); + throw new MFException("invalid parent class '$sParentClassName'", MFException::NOT_FOUND, $oSourceParentClassNode); } $oNextParentSibling = $oNodeForTargetParent->nextSibling; if ($oNextParentSibling) { @@ -833,29 +846,14 @@ class ModelFactory 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 - ); + throw new MFException("could not be found or marked as removed", MFException::NOT_FOUND, $oSourceNode); } if ($bIfExists) { // Do not continue deeper $oTargetNode = null; } else { if (!$bSpecifiedMerge && $sMode === self::LOAD_DELTA_MODE_STRICT && ($sSearchId !== '' || is_null($oSourceNode->GetFirstElementChild()))) { - $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' - ); + throw new MFException("could not be found or marked as removed (strict mode)", MFException::NOT_FOUND, $oSourceNode, null, 'strict mode'); } // Ignore renaming non-existant node @@ -904,15 +902,7 @@ class ModelFactory if (is_null($oSourceNode->GetFirstElementChild()) && $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' - ); + throw new MFException("cannot be modified without _delta flag (strict mode)", MFException::AMBIGUOUS_LEAF, $oSourceNode, null, 'strict mode'); } else { // Lax mode: same as redefine // Replace the existing node by the given node - copy child nodes as well @@ -920,7 +910,7 @@ class ModelFactory 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); + $oTargetParentNode->RedefineChildNode($oTargetNode, $sSearchId, $oSourceNode); } } } else { @@ -964,7 +954,7 @@ class ModelFactory // Replace the existing node by the given node - copy child nodes as well /** @var \MFElement $oTargetNode */ $oTargetNode = $oTargetDocument->importNode($oSourceNode, true); - $oTargetParentNode->RedefineChildNode($oTargetNode, $sSearchId); + $oTargetParentNode->RedefineChildNode($oTargetNode, $sSearchId, $oSourceNode); break; case 'delete_if_exists': @@ -984,38 +974,18 @@ class ModelFactory case 'delete': /** @var \MFElement $oTargetNode */ $oTargetNode = $oTargetParentNode->_FindChildNode($oSourceNode, $sSearchId); - $sPath = MFDocument::GetItopNodePath($oSourceNode); - $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 - ); + throw new MFException("could not be deleted (not found)", MFException::COULD_NOT_BE_DELETED, $oSourceNode); } if ($oTargetNode->IsRemoved()) { - throw new MFException( - $sPath.' at line '.$iLine.": could not be deleted (already marked as deleted)", - MFException::ALREADY_DELETED, - $iLine, - $sPath - ); + throw new MFException("could not be deleted (already marked as deleted)", MFException::ALREADY_DELETED, $oSourceNode); } $oTargetNode->Delete(); break; default: - $sPath = MFDocument::GetItopNodePath($oSourceNode); - $iLine = $this->GetXMLLineNumber($oSourceNode); - throw new MFException( - $sPath.' at line '.$iLine.": unexpected value for attribute _delta: '".$sDeltaSpec."'", - MFException::INVALID_DELTA, - $iLine, - $sPath, - $sDeltaSpec - ); + throw new MFException("unexpected value for attribute _delta: '".$sDeltaSpec."'", MFException::INVALID_DELTA, $oSourceNode, null, $sDeltaSpec); } if ($oTargetNode && $oTargetNode->parentNode) { @@ -2116,14 +2086,12 @@ class MFElement extends Combodo\iTop\DesignElement $oExisting = $this->_FindChildNode($oNode); if ($oExisting) { if (!$oExisting->IsRemoved()) { - $sPath = MFDocument::GetItopNodePath($oNode); - $iLine = ModelFactory::GetXMLLineNumber($oNode); $sExistingPath = MFDocument::GetItopNodePath($oExisting).' created_in: ['.$oExisting->getAttribute('_created_in').']'; $iExistingLine = ModelFactory::GetXMLLineNumber($oExisting); $sExceptionMessage = <<ReplaceWithSingleNode($oNode); $sFlag = 'replaced'; @@ -2141,13 +2109,14 @@ EOF; * * @param MFElement $oNode The node (including all subnodes) to set * @param string|null $sSearchId + * @param mixed $oParentFallbackNode: provided to print accurate line number in case $oNode line is 0 * * @return void * * @throws MFException * @throws \Exception */ - public function RedefineChildNode(MFElement $oNode, $sSearchId = null) + public function RedefineChildNode(MFElement $oNode, $sSearchId = null, $oParentFallbackNode = null) { // First: cleanup any flag behind the new node, and eventually add trace data $oNode->ApplyChanges(); @@ -2156,25 +2125,13 @@ EOF; $oExisting = $this->_FindChildNode($oNode, $sSearchId); if (!$oExisting) { $sPath = MFDocument::GetItopNodePath($this)."/".$oNode->tagName.(empty($sSearchId) ? '' : "[$sSearchId]"); - $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 - ); + throw new MFException('could not be modified (not found)', MFException::COULD_NOT_BE_MODIFIED_NOT_FOUND, $oNode, $sPath, $oParentFallbackNode); } $sPrevFlag = $oExisting->GetAlteration(); $sOldId = $oExisting->getAttribute('_old_id'); if ($oExisting->IsRemoved()) { $sPath = MFDocument::GetItopNodePath($this)."/".$oNode->tagName.(empty($sSearchId) ? '' : "[$sSearchId]"); - $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 - ); + throw new MFException('could not be modified (marked as deleted)', MFException::COULD_NOT_BE_MODIFIED_ALREADY_DELETED, $oNode, $sPath, $oParentFallbackNode); } $oExisting->ReplaceWithSingleNode($oNode); if (!$this->IsInDefinition()) { diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index 9fd0ababf..26dff485f 100644 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -130,9 +130,6 @@ class ModuleDiscovery $aArgs['module_file'] = $sFilePath; list($sModuleName, $sModuleVersion) = static::GetModuleName($sId); - if ($sModuleVersion == '') { - $sModuleVersion = '1.0.0'; - } if (array_key_exists($sModuleName, self::$m_aModuleVersionByName)) { if (version_compare($sModuleVersion, self::$m_aModuleVersionByName[$sModuleName]['version'], '>')) { @@ -229,7 +226,7 @@ class ModuleDiscovery } ksort($aDependencies); $aOrderedModules = []; - $iLoopCount = 1; + $iLoopCount = 0; while (($iLoopCount < count($aModules)) && (count($aDependencies) > 0)) { foreach ($aDependencies as $sId => $aRemainingDeps) { $bDependenciesSolved = true; @@ -301,13 +298,8 @@ class ModuleDiscovery $aModuleVersions = []; // Separate the module names from their version for an easier comparison later foreach ($aOrderedModules as $sModuleId) { - $aMatches = []; - if (preg_match('|^([^/]+)/(.*)$|', $sModuleId, $aMatches)) { - $aModuleVersions[$aMatches[1]] = $aMatches[2]; - } else { - // No version number found, assume 1.0.0 - $aModuleVersions[$sModuleId] = '1.0.0'; - } + list($sModuleName, $sVersion) = self::GetModuleName($sModuleId); + $aModuleVersions[$sModuleName] = $sVersion; } if (preg_match_all('/([^\(\)&| ]+)/', $sDepString, $aMatches)) { $aReplacements = []; @@ -422,10 +414,14 @@ class ModuleDiscovery if (preg_match('!^(.*)/(.*)$!', $sModuleId, $aMatches)) { $sName = $aMatches[1]; $sVersion = $aMatches[2]; + if ($sVersion === "") { + $sVersion = "1.0.0"; + } } else { $sName = $sModuleId; - $sVersion = ""; + $sVersion = "1.0.0"; } + return [$sName, $sVersion]; } diff --git a/sources/Controller/Base/Layout/ObjectController.php b/sources/Controller/Base/Layout/ObjectController.php index e5573d655..7062c62db 100644 --- a/sources/Controller/Base/Layout/ObjectController.php +++ b/sources/Controller/Base/Layout/ObjectController.php @@ -632,6 +632,7 @@ JS; $aResult['data'] = ['error_message' => $e->getHtmlMessage()]; } else { $oPage->AddHeaderMessage($e->getHtmlMessage(), 'message_error'); + $oObj->Reload(); $oObj->DisplayModifyForm( $oPage, ['wizard_container' => true] diff --git a/synchro/synchrodatasource.class.inc.php b/synchro/synchrodatasource.class.inc.php index 90ea57b27..c0f824eff 100644 --- a/synchro/synchrodatasource.class.inc.php +++ b/synchro/synchrodatasource.class.inc.php @@ -2400,7 +2400,9 @@ class SynchroReplica extends DBObject implements iDisplay } // Really modified ? if ($oDestObj->IsModified()) { - $oDestObj::SetCurrentChange($oChange); + if (method_exists(get_class($oDestObj), "SetCurrentChange")) { + $oDestObj::SetCurrentChange($oChange); + } $oDestObj->DBUpdate(); $bModified = true; $oStatLog->AddTrace('Updated object - Values: {'.implode(', ', $aValueTrace).'}', $this); @@ -2450,7 +2452,10 @@ class SynchroReplica extends DBObject implements iDisplay $aValueTrace[] = "$sAttCode: $value"; } } - $oDestObj::SetCurrentChange($oChange); + + if (method_exists(get_class($oDestObj), "SetCurrentChange")) { + $oDestObj::SetCurrentChange($oChange); + } $iNew = $oDestObj->DBInsert(); $this->Set('dest_id', $oDestObj->GetKey()); diff --git a/tests/php-unit-tests/unitary-tests/modulediscovery/ModuleDiscoveryTest.php b/tests/php-unit-tests/unitary-tests/modulediscovery/ModuleDiscoveryTest.php new file mode 100644 index 000000000..d863c6d64 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/modulediscovery/ModuleDiscoveryTest.php @@ -0,0 +1,50 @@ + [ + 'sModuleId' => 'a/1.2.3', + 'name' => 'a', + 'version' => '1.2.3', + ], + 'develop' => [ + 'sModuleId' => 'a/1.2.3-dev', + 'name' => 'a', + 'version' => '1.2.3-dev', + ], + 'missing version => 1.0.0' => [ + 'sModuleId' => 'a/', + 'name' => 'a', + 'version' => '1.0.0', + ], + 'missing everything except name' => [ + 'sModuleId' => 'a', + 'name' => 'a', + 'version' => '1.0.0', + ], + ]; + } + + protected function setUp(): void + { + parent::setUp(); + + $this->RequireOnceItopFile('setup/modulediscovery.class.inc.php'); + } + + /** + * @dataProvider GetModuleNameProvider + */ + public function testGetModuleName($sModuleId, $expectedName, $expectedVersion) + { + $this->assertEquals([$expectedName, $expectedVersion], \ModuleDiscovery::GetModuleName($sModuleId)); + } + +}