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('
');
}
+ $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));
+ }
+
+}