Merge branch 'support/3.2.1' into support/3.2

This commit is contained in:
odain
2025-11-14 10:39:04 +01:00
16 changed files with 171 additions and 144 deletions

View File

@@ -199,7 +199,7 @@ class DatamodelsXmlFiles extends AbstractGlobFileVersionUpdater
libxml_clear_errors(); libxml_clear_errors();
$oFileXml->formatOutput = true; $oFileXml->formatOutput = true;
$oFileXml->preserveWhiteSpace = false; $oFileXml->preserveWhiteSpace = false;
$oFileXml->loadXML($sFileContent); $oFileXml->loadXML($sFileContent, LIBXML_BIGLINES);
$oFileItopFormat = new iTopDesignFormat($oFileXml); $oFileItopFormat = new iTopDesignFormat($oFileXml);

View File

@@ -319,6 +319,7 @@ EOF
if ($sIntroduction != null) { if ($sIntroduction != null) {
$oPage->add('<div class="ui-dialog-header">'.$sIntroduction.'</div>'); $oPage->add('<div class="ui-dialog-header">'.$sIntroduction.'</div>');
} }
$oPage->add('<div class="designer-dialog-error"></div>');
$this->Render($oPage); $this->Render($oPage);
$oPage->add('</div>'); $oPage->add('</div>');

View File

@@ -597,12 +597,12 @@ class CMDBSource
$oResult = DbConnectionWrapper::GetDbConnection(true)->query($sSql); $oResult = DbConnectionWrapper::GetDbConnection(true)->query($sSql);
} catch (mysqli_sql_exception $e) { } catch (mysqli_sql_exception $e) {
self::LogDeadLock($e, true); 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 { } finally {
$oKPI->ComputeStats('Query exec (mySQL)', $sSql); $oKPI->ComputeStats('Query exec (mySQL)', $sSql);
} }
if ($oResult === false) { if ($oResult === false) {
$aContext = ['query' => $sSql]; $aContext = ["\nstack" => (new Exception(''))->getTraceAsString(), "\nquery" => $sSql];
$iMySqlErrorNo = DbConnectionWrapper::GetDbConnection(true)->errno; $iMySqlErrorNo = DbConnectionWrapper::GetDbConnection(true)->errno;
$aMySqlHasGoneAwayErrorCodes = MySQLHasGoneAwayException::getErrorCodes(); $aMySqlHasGoneAwayErrorCodes = MySQLHasGoneAwayException::getErrorCodes();

View File

@@ -70,6 +70,11 @@ class DesignDocument extends DOMDocument
$this->preserveWhiteSpace = true; // otherwise the formatOutput option would have no effect $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 * Overload of the standard API
* *

View File

@@ -6193,12 +6193,14 @@ abstract class MetaModel
if ($bMustBeFound && empty($aRow)) { if ($bMustBeFound && empty($aRow)) {
$sNotFoundErrorMessage = "No result for the single row query"; $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, 'class' => $sClass,
'key' => $iKey, 'key' => $iKey,
'sql_query' => $sSQL, 'sql_query' => $sSQL,
'stack' => $e->getTraceAsString(),
]); ]);
throw new CoreException($sNotFoundErrorMessage); throw $e;
} }
return $aRow; return $aRow;

View File

@@ -390,16 +390,17 @@ function LinksWidget(id, sClass, sAttCode, iInputId, sSuffix, bDuplicates, oWizH
this.RegisterChange = function () { this.RegisterChange = function () {
// Listen only used inputs // 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'))) if (!($(this).hasClass('selection')))
{ {
let oCheckbox = $(this).closest('tr').find('.selection'); let oCheckbox = $(this).closest('tr').find('.selection');
let iLink = oCheckbox.attr('data-link-id'); let iLink = oCheckbox.attr('data-link-id');
let iUniqueId = oCheckbox.attr('data-unique-id'); let iUniqueId = oCheckbox.attr('data-unique-id');
let sAttCode = $(this).closest('.attribute-edit').attr('data-attcode'); let sAttCode = $(this).closest('.attribute-edit').attr('data-attcode');
let value = $(this).val();; let value = $(this).val();;
return me.OnValueChange(iLink, iUniqueId, sAttCode, value, this); return me.OnValueChange(iLink, iUniqueId, sAttCode, value, this);
} }
return true; return true;
}); });
}; };

View File

@@ -548,6 +548,7 @@ function ValidateWithPattern(sFieldId, bMandatory, sPattern, sFormId, aForbidden
if (sMessage) if (sMessage)
{ {
$('#'+sFieldId).attr('data-tooltip-content', sMessage); $('#'+sFieldId).attr('data-tooltip-content', sMessage);
$('#'+sFieldId).attr('data-tooltip-theme', 'error');
CombodoTooltip.InitTooltipFromMarkup($('#'+sFieldId), true); CombodoTooltip.InitTooltipFromMarkup($('#'+sFieldId), true);
$('#'+sFieldId)[0]._tippy.show(); $('#'+sFieldId)[0]._tippy.show();
} }

View File

@@ -70,14 +70,37 @@ class MFException extends Exception
* MFException constructor. * MFException constructor.
* *
* @inheritDoc * @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->iSourceLineNumber = $iSourceLineNumber;
$this->iSourceLineOffset = 0; $this->iSourceLineOffset = 0;
$this->sXPath = $sXPath; $this->sXPath = $sXPath;
$this->sExtraInfo = $sExtraInfo; $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; return;
} }
if (!is_dir($sRootDir)) {
$sRootDir = APPROOT.$sRootDir;
}
// Scan the module's root directory to find the datamodel(*).xml files // Scan the module's root directory to find the datamodel(*).xml files
if ($hDir = opendir($sRootDir)) { if ($hDir = opendir($sRootDir)) {
// This is the correct way to loop over the directory. (according to the documentation) // 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': case 'define_if_not_exists':
/** @var \MFElement $oParentNode */ /** @var \MFElement $oParentNode */
$oParentNode = $oSubClassNode->parentNode; $oParentNode = $oSubClassNode->parentNode;
$iLine = ModelFactory::GetXMLLineNumber($oParentNode); throw new MFException("_delta=\"$sParentDeltaSpec\" not supported for classes in hierarchy", MFException::NOT_FOUND, $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
);
} }
} }
@@ -798,14 +818,7 @@ class ModelFactory
// Move class after new parent class (before its next sibling) // Move class after new parent class (before its next sibling)
$oNodeForTargetParent = $oTargetDocument->GetNodes("/itop_design/classes/class[@id=\"$sParentClassName\"]")->item(0); $oNodeForTargetParent = $oTargetDocument->GetNodes("/itop_design/classes/class[@id=\"$sParentClassName\"]")->item(0);
if (is_null($oNodeForTargetParent)) { if (is_null($oNodeForTargetParent)) {
$iLine = ModelFactory::GetXMLLineNumber($oSourceParentClassNode); throw new MFException("invalid parent class '$sParentClassName'", MFException::NOT_FOUND, $oSourceParentClassNode);
$sItopNodePath = DesignDocument::GetItopNodePath($oSourceParentClassNode);
throw new MFException(
$sItopNodePath." at line $iLine: invalid parent class '$sParentClassName'",
MFException::NOT_FOUND,
$iLine,
$sItopNodePath
);
} }
$oNextParentSibling = $oNodeForTargetParent->nextSibling; $oNextParentSibling = $oNodeForTargetParent->nextSibling;
if ($oNextParentSibling) { if ($oNextParentSibling) {
@@ -833,29 +846,14 @@ class ModelFactory
if (!$oTargetNode || $oTargetNode->IsRemoved()) { if (!$oTargetNode || $oTargetNode->IsRemoved()) {
// The node does not exist or is marked as removed // The node does not exist or is marked as removed
if ($bMustExist) { if ($bMustExist) {
$iLine = ModelFactory::GetXMLLineNumber($oSourceNode); throw new MFException("could not be found or marked as removed", MFException::NOT_FOUND, $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) { if ($bIfExists) {
// Do not continue deeper // Do not continue deeper
$oTargetNode = null; $oTargetNode = null;
} else { } else {
if (!$bSpecifiedMerge && $sMode === self::LOAD_DELTA_MODE_STRICT && ($sSearchId !== '' || is_null($oSourceNode->GetFirstElementChild()))) { if (!$bSpecifiedMerge && $sMode === self::LOAD_DELTA_MODE_STRICT && ($sSearchId !== '' || is_null($oSourceNode->GetFirstElementChild()))) {
$iLine = ModelFactory::GetXMLLineNumber($oSourceNode); throw new MFException("could not be found or marked as removed (strict mode)", MFException::NOT_FOUND, $oSourceNode, null, 'strict mode');
$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 // Ignore renaming non-existant node
@@ -904,15 +902,7 @@ class ModelFactory
if (is_null($oSourceNode->GetFirstElementChild()) && $oTargetParentNode instanceof MFElement) { if (is_null($oSourceNode->GetFirstElementChild()) && $oTargetParentNode instanceof MFElement) {
// Leaf node // Leaf node
if ($sMode === self::LOAD_DELTA_MODE_STRICT && !$oSourceNode->hasAttribute('_rename_from') && trim($oSourceNode->GetText('')) !== '') { if ($sMode === self::LOAD_DELTA_MODE_STRICT && !$oSourceNode->hasAttribute('_rename_from') && trim($oSourceNode->GetText('')) !== '') {
$iLine = ModelFactory::GetXMLLineNumber($oSourceNode); throw new MFException("cannot be modified without _delta flag (strict mode)", MFException::AMBIGUOUS_LEAF, $oSourceNode, null, 'strict mode');
$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 { } else {
// Lax mode: same as redefine // Lax mode: same as redefine
// Replace the existing node by the given node - copy child nodes as well // Replace the existing node by the given node - copy child nodes as well
@@ -920,7 +910,7 @@ class ModelFactory
if (trim($oSourceNode->GetText('')) !== '') { if (trim($oSourceNode->GetText('')) !== '') {
$oTargetNode = $oTargetDocument->importNode($oSourceNode, true); $oTargetNode = $oTargetDocument->importNode($oSourceNode, true);
$sSearchId = $oSourceNode->hasAttribute('_rename_from') ? $oSourceNode->getAttribute('_rename_from') : $oSourceNode->getAttribute('id'); $sSearchId = $oSourceNode->hasAttribute('_rename_from') ? $oSourceNode->getAttribute('_rename_from') : $oSourceNode->getAttribute('id');
$oTargetParentNode->RedefineChildNode($oTargetNode, $sSearchId); $oTargetParentNode->RedefineChildNode($oTargetNode, $sSearchId, $oSourceNode);
} }
} }
} else { } else {
@@ -964,7 +954,7 @@ class ModelFactory
// Replace the existing node by the given node - copy child nodes as well // Replace the existing node by the given node - copy child nodes as well
/** @var \MFElement $oTargetNode */ /** @var \MFElement $oTargetNode */
$oTargetNode = $oTargetDocument->importNode($oSourceNode, true); $oTargetNode = $oTargetDocument->importNode($oSourceNode, true);
$oTargetParentNode->RedefineChildNode($oTargetNode, $sSearchId); $oTargetParentNode->RedefineChildNode($oTargetNode, $sSearchId, $oSourceNode);
break; break;
case 'delete_if_exists': case 'delete_if_exists':
@@ -984,38 +974,18 @@ class ModelFactory
case 'delete': case 'delete':
/** @var \MFElement $oTargetNode */ /** @var \MFElement $oTargetNode */
$oTargetNode = $oTargetParentNode->_FindChildNode($oSourceNode, $sSearchId); $oTargetNode = $oTargetParentNode->_FindChildNode($oSourceNode, $sSearchId);
$sPath = MFDocument::GetItopNodePath($oSourceNode);
$iLine = $this->GetXMLLineNumber($oSourceNode);
if ($oTargetNode == null) { if ($oTargetNode == null) {
throw new MFException( throw new MFException("could not be deleted (not found)", MFException::COULD_NOT_BE_DELETED, $oSourceNode);
$sPath.' at line '.$iLine.": could not be deleted (not found)",
MFException::COULD_NOT_BE_DELETED,
$iLine,
$sPath
);
} }
if ($oTargetNode->IsRemoved()) { if ($oTargetNode->IsRemoved()) {
throw new MFException( throw new MFException("could not be deleted (already marked as deleted)", MFException::ALREADY_DELETED, $oSourceNode);
$sPath.' at line '.$iLine.": could not be deleted (already marked as deleted)",
MFException::ALREADY_DELETED,
$iLine,
$sPath
);
} }
$oTargetNode->Delete(); $oTargetNode->Delete();
break; break;
default: default:
$sPath = MFDocument::GetItopNodePath($oSourceNode); throw new MFException("unexpected value for attribute _delta: '".$sDeltaSpec."'", MFException::INVALID_DELTA, $oSourceNode, null, $sDeltaSpec);
$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 && $oTargetNode->parentNode) { if ($oTargetNode && $oTargetNode->parentNode) {
@@ -2139,14 +2109,12 @@ class MFElement extends Combodo\iTop\DesignElement
$oExisting = $this->_FindChildNode($oNode); $oExisting = $this->_FindChildNode($oNode);
if ($oExisting) { if ($oExisting) {
if (!$oExisting->IsRemoved()) { if (!$oExisting->IsRemoved()) {
$sPath = MFDocument::GetItopNodePath($oNode);
$iLine = ModelFactory::GetXMLLineNumber($oNode);
$sExistingPath = MFDocument::GetItopNodePath($oExisting).' created_in: ['.$oExisting->getAttribute('_created_in').']'; $sExistingPath = MFDocument::GetItopNodePath($oExisting).' created_in: ['.$oExisting->getAttribute('_created_in').']';
$iExistingLine = ModelFactory::GetXMLLineNumber($oExisting); $iExistingLine = ModelFactory::GetXMLLineNumber($oExisting);
$sExceptionMessage = <<<EOF $sExceptionMessage = <<<EOF
`{$sPath}` at line {$iLine} could not be added : already exists in `{$sExistingPath}` at line {$iExistingLine} could not be added : already exists in `{$sExistingPath}` at line {$iExistingLine}
EOF; EOF;
throw new MFException($sExceptionMessage, MFException::COULD_NOT_BE_ADDED, $iLine, $sPath); throw new MFException($sExceptionMessage, MFException::COULD_NOT_BE_ADDED, $oNode);
} }
$oExisting->ReplaceWithSingleNode($oNode); $oExisting->ReplaceWithSingleNode($oNode);
$sFlag = 'replaced'; $sFlag = 'replaced';
@@ -2164,13 +2132,14 @@ EOF;
* *
* @param MFElement $oNode The node (including all subnodes) to set * @param MFElement $oNode The node (including all subnodes) to set
* @param string|null $sSearchId * @param string|null $sSearchId
* @param mixed $oParentFallbackNode: provided to print accurate line number in case $oNode line is 0
* *
* @return void * @return void
* *
* @throws MFException * @throws MFException
* @throws \Exception * @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 // First: cleanup any flag behind the new node, and eventually add trace data
$oNode->ApplyChanges(); $oNode->ApplyChanges();
@@ -2179,25 +2148,13 @@ EOF;
$oExisting = $this->_FindChildNode($oNode, $sSearchId); $oExisting = $this->_FindChildNode($oNode, $sSearchId);
if (!$oExisting) { if (!$oExisting) {
$sPath = MFDocument::GetItopNodePath($this)."/".$oNode->tagName.(empty($sSearchId) ? '' : "[$sSearchId]"); $sPath = MFDocument::GetItopNodePath($this)."/".$oNode->tagName.(empty($sSearchId) ? '' : "[$sSearchId]");
$iLine = ModelFactory::GetXMLLineNumber($oNode); throw new MFException('could not be modified (not found)', MFException::COULD_NOT_BE_MODIFIED_NOT_FOUND, $oNode, $sPath, $oParentFallbackNode);
throw new MFException(
$sPath." at line $iLine: could not be modified (not found)",
MFException::COULD_NOT_BE_MODIFIED_NOT_FOUND,
$sPath,
$iLine
);
} }
$sPrevFlag = $oExisting->GetAlteration(); $sPrevFlag = $oExisting->GetAlteration();
$sOldId = $oExisting->getAttribute('_old_id'); $sOldId = $oExisting->getAttribute('_old_id');
if ($oExisting->IsRemoved()) { if ($oExisting->IsRemoved()) {
$sPath = MFDocument::GetItopNodePath($this)."/".$oNode->tagName.(empty($sSearchId) ? '' : "[$sSearchId]"); $sPath = MFDocument::GetItopNodePath($this)."/".$oNode->tagName.(empty($sSearchId) ? '' : "[$sSearchId]");
$iLine = ModelFactory::GetXMLLineNumber($oNode); throw new MFException('could not be modified (marked as deleted)', MFException::COULD_NOT_BE_MODIFIED_ALREADY_DELETED, $oNode, $sPath, $oParentFallbackNode);
throw new MFException(
$sPath." at line $iLine: could not be modified (marked as deleted)",
MFException::COULD_NOT_BE_MODIFIED_ALREADY_DELETED,
$sPath,
$iLine
);
} }
$oExisting->ReplaceWithSingleNode($oNode); $oExisting->ReplaceWithSingleNode($oNode);
if (!$this->IsInDefinition()) { if (!$this->IsInDefinition()) {

View File

@@ -118,9 +118,6 @@ class ModuleDiscovery
$aArgs['module_file'] = $sFilePath; $aArgs['module_file'] = $sFilePath;
list($sModuleName, $sModuleVersion) = static::GetModuleName($sId); list($sModuleName, $sModuleVersion) = static::GetModuleName($sId);
if ($sModuleVersion == '') {
$sModuleVersion = '1.0.0';
}
if (array_key_exists($sModuleName, self::$m_aModuleVersionByName)) { if (array_key_exists($sModuleName, self::$m_aModuleVersionByName)) {
if (version_compare($sModuleVersion, self::$m_aModuleVersionByName[$sModuleName]['version'], '>')) { if (version_compare($sModuleVersion, self::$m_aModuleVersionByName[$sModuleName]['version'], '>')) {
@@ -217,9 +214,11 @@ class ModuleDiscovery
} }
ksort($aDependencies); ksort($aDependencies);
$aOrderedModules = []; $aOrderedModules = [];
$iLoopCount = 1; $iLoopCount = 0;
while (($iLoopCount < count($aModules)) && (count($aDependencies) > 0)) { while(($iLoopCount < count($aModules)) && (count($aDependencies) > 0) )
foreach ($aDependencies as $sId => $aRemainingDeps) { {
foreach($aDependencies as $sId => $aRemainingDeps)
{
$bDependenciesSolved = true; $bDependenciesSolved = true;
foreach ($aRemainingDeps as $sDepId) { foreach ($aRemainingDeps as $sDepId) {
if (!self::DependencyIsResolved($sDepId, $aOrderedModules, $aSelectedModules)) { if (!self::DependencyIsResolved($sDepId, $aOrderedModules, $aSelectedModules)) {
@@ -279,14 +278,10 @@ class ModuleDiscovery
$bResult = false; $bResult = false;
$aModuleVersions = []; $aModuleVersions = [];
// Separate the module names from their version for an easier comparison later // Separate the module names from their version for an easier comparison later
foreach ($aOrderedModules as $sModuleId) { foreach($aOrderedModules as $sModuleId)
$aMatches = []; {
if (preg_match('|^([^/]+)/(.*)$|', $sModuleId, $aMatches)) { list($sModuleName, $sVersion) = self::GetModuleName($sModuleId);
$aModuleVersions[$aMatches[1]] = $aMatches[2]; $aModuleVersions[$sModuleName] = $sVersion;
} else {
// No version number found, assume 1.0.0
$aModuleVersions[$sModuleId] = '1.0.0';
}
} }
if (preg_match_all('/([^\(\)&| ]+)/', $sDepString, $aMatches)) { if (preg_match_all('/([^\(\)&| ]+)/', $sDepString, $aMatches)) {
$aReplacements = []; $aReplacements = [];
@@ -400,10 +395,16 @@ class ModuleDiscovery
if (preg_match('!^(.*)/(.*)$!', $sModuleId, $aMatches)) { if (preg_match('!^(.*)/(.*)$!', $sModuleId, $aMatches)) {
$sName = $aMatches[1]; $sName = $aMatches[1];
$sVersion = $aMatches[2]; $sVersion = $aMatches[2];
} else { if ($sVersion === ""){
$sName = $sModuleId; $sVersion = "1.0.0";
$sVersion = ""; }
} }
else
{
$sName = $sModuleId;
$sVersion = "1.0.0";
}
return [$sName, $sVersion]; return [$sName, $sVersion];
} }

View File

@@ -754,7 +754,7 @@ abstract class Controller extends AbstractController
{ {
// iTop 3.1 and older compatibility, if not an URI we don't know if its relative to app root or module root // iTop 3.1 and older compatibility, if not an URI we don't know if its relative to app root or module root
if (strpos($sLinkedScript, "://") === false) { if (strpos($sLinkedScript, "://") === false) {
$this->m_oPage->add_linked_script($sLinkedScript); $this->m_oPage->LinkScriptFromAppRoot(utils::LocalPath($sLinkedScript, APPROOT));
return; return;
} }

View File

@@ -1268,19 +1268,20 @@ JS;
*/ */
protected function AddCompatibilityFiles(string $sFileType, string $sMode): void protected function AddCompatibilityFiles(string $sFileType, string $sMode): void
{ {
$sConstantName = 'COMPATIBILITY_'.strtoupper($sMode).'_LINKED_'.($sFileType === static::ENUM_COMPATIBILITY_FILE_TYPE_CSS ? 'STYLESHEETS' : 'SCRIPTS').'_REL_PATH'; $sConstantName = 'COMPATIBILITY_'.strtoupper($sMode).'_LINKED_'. ($sFileType === static::ENUM_COMPATIBILITY_FILE_TYPE_CSS ? 'STYLESHEETS' : 'SCRIPTS') .'_REL_PATH';
$sMethodName = 'add_linked_'.($sFileType === static::ENUM_COMPATIBILITY_FILE_TYPE_CSS ? 'stylesheet' : 'script'); $sMethodName = 'Link'.($sFileType === static::ENUM_COMPATIBILITY_FILE_TYPE_CSS ? 'Resource' : 'Script').'FromAppRoot';
// Add ancestors files // Add ancestors files
foreach (array_reverse(class_parents(static::class)) as $sParentClass) { foreach (array_reverse(class_parents(static::class)) as $sParentClass) {
foreach (constant($sParentClass.'::'.$sConstantName) as $sFile) { foreach (constant($sParentClass.'::'.$sConstantName) as $sFile) {
$this->$sMethodName(utils::GetAbsoluteUrlAppRoot().$sFile); $this->$sMethodName($sFile);
} }
} }
// Add current class files // Add current class files
foreach (constant('static::'.$sConstantName) as $sFile) { foreach (constant('static::'.$sConstantName) as $sFile) {
$this->$sMethodName(utils::GetAbsoluteUrlAppRoot().$sFile); $this->$sMethodName($sFile);
} }
} }

View File

@@ -632,6 +632,7 @@ JS;
$aResult['data'] = ['error_message' => $e->getHtmlMessage()]; $aResult['data'] = ['error_message' => $e->getHtmlMessage()];
} else { } else {
$oPage->AddHeaderMessage($e->getHtmlMessage(), 'message_error'); $oPage->AddHeaderMessage($e->getHtmlMessage(), 'message_error');
$oObj->Reload();
$oObj->DisplayModifyForm( $oObj->DisplayModifyForm(
$oPage, $oPage,
['wizard_container' => true] ['wizard_container' => true]

View File

@@ -139,15 +139,15 @@ class SessionHandler extends \SessionHandler
// - Data corruption (not a json / not an array / no previous creation_time key) // - Data corruption (not a json / not an array / no previous creation_time key)
$iCreationTime = time(); $iCreationTime = time();
$aJson = []; $aJson=[];
if (! is_null($sPreviousFileVersionContent)) { if (! is_null($sPreviousFileVersionContent)) {
$aJson = json_decode($sPreviousFileVersionContent, true); $aJson = json_decode($sPreviousFileVersionContent, true);
if (is_array($aJson)) { if (is_array($aJson)){
if (array_key_exists('creation_time', $aJson)) { if (array_key_exists('creation_time', $aJson)) {
$iCreationTime = $aJson['creation_time']; $iCreationTime = $aJson['creation_time'];
} }
} else { } else {
$aJson = []; $aJson=[];
} }
} }
@@ -155,42 +155,42 @@ class SessionHandler extends \SessionHandler
'login_mode' => Session::Get('login_mode'), 'login_mode' => Session::Get('login_mode'),
'user_id' => $sUserId, 'user_id' => $sUserId,
'creation_time' => $iCreationTime, 'creation_time' => $iCreationTime,
'context' => implode('|', ContextTag::GetStack()), 'context' => implode('|', ContextTag::GetStack())
]; ];
$oiSessionHandlerExtension = $this->GetSessionHandlerExtension(); $oiSessionHandlerExtension = $this->GetSessionHandlerExtension();
if (! is_null($oiSessionHandlerExtension)) { if (! is_null($oiSessionHandlerExtension)){
$oiSessionHandlerExtension->CompleteSessionData($aJson, $aData); $oiSessionHandlerExtension->CompleteSessionData($aJson, $aData);
} }
return json_encode( return json_encode (
$aData $aData
); );
} catch (Exception $e) { } catch(Exception $e) {
IssueLog::Error(__METHOD__, null, [ 'error' => $e->getMessage() ]); IssueLog::Error(__METHOD__, null, [ 'error' => $e->getMessage() ]);
} }
return null; return null;
} }
private function GetSessionHandlerExtension(): ?iSessionHandlerExtension private function GetSessionHandlerExtension() : ?iSessionHandlerExtension
{ {
$sSessionHandlerExtensionClass = utils::GetConfig()->Get('sessions_tracking.session_handler_extension'); $sSessionHandlerExtensionClass = utils::GetConfig()->Get('sessions_tracking.session_handler_extension');
if (strlen($sSessionHandlerExtensionClass) != 0) { if (strlen($sSessionHandlerExtensionClass) !=0){
try { try{
if (! class_exists($sSessionHandlerExtensionClass)) { if (! class_exists($sSessionHandlerExtensionClass)){
throw new \Exception("Cannot find class"); throw new \Exception("Cannot find class");
} }
/** @var iSessionHandlerExtension $oSessionHandlerExtension */ /** @var iSessionHandlerExtension $oSessionHandlerExtension */
$oSessionHandlerExtension = new $sSessionHandlerExtensionClass(); $oSessionHandlerExtension = new $sSessionHandlerExtensionClass;
if ($oSessionHandlerExtension instanceof iSessionHandlerExtension) { if ($oSessionHandlerExtension instanceof iSessionHandlerExtension){
return $oSessionHandlerExtension; return $oSessionHandlerExtension;
} }
throw new \Exception("Not an instance of iSessionHandlerExtension"); throw new \Exception("Not an instance of iSessionHandlerExtension");
} catch (\Exception $e) { } catch(\Exception $e) {
IssueLog::Error(__METHOD__.': cannot instanciate iSessionHandlerExtension', null, ['sessions_tracking.session_handler_extension' => $sSessionHandlerExtensionClass]); IssueLog::Error(__METHOD__ . ': cannot instanciate iSessionHandlerExtension', null, ['sessions_tracking.session_handler_extension' => $sSessionHandlerExtensionClass]);
} }
} }

View File

@@ -2397,8 +2397,11 @@ class SynchroReplica extends DBObject implements iDisplay
} }
} }
// Really modified ? // Really modified ?
if ($oDestObj->IsModified()) { if ($oDestObj->IsModified())
$oDestObj::SetCurrentChange($oChange); {
if(method_exists(get_class($oDestObj), "SetCurrentChange")){
$oDestObj::SetCurrentChange($oChange);
}
$oDestObj->DBUpdate(); $oDestObj->DBUpdate();
$bModified = true; $bModified = true;
$oStatLog->AddTrace('Updated object - Values: {'.implode(', ', $aValueTrace).'}', $this); $oStatLog->AddTrace('Updated object - Values: {'.implode(', ', $aValueTrace).'}', $this);
@@ -2448,7 +2451,10 @@ class SynchroReplica extends DBObject implements iDisplay
$aValueTrace[] = "$sAttCode: $value"; $aValueTrace[] = "$sAttCode: $value";
} }
} }
$oDestObj::SetCurrentChange($oChange);
if(method_exists(get_class($oDestObj), "SetCurrentChange")){
$oDestObj::SetCurrentChange($oChange);
}
$iNew = $oDestObj->DBInsert(); $iNew = $oDestObj->DBInsert();
$this->Set('dest_id', $oDestObj->GetKey()); $this->Set('dest_id', $oDestObj->GetKey());

View File

@@ -0,0 +1,51 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
class ModuleDiscoveryTest extends ItopTestCase
{
public function GetModuleNameProvider()
{
return [
'nominal' => [
'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));
}
}

View File

@@ -325,4 +325,4 @@ class SessionHandlerTest extends ItopDataTestCase
$sContent = $this->GenerateSessionContent($oSessionHandler, json_encode(null)); $sContent = $this->GenerateSessionContent($oSessionHandler, json_encode(null));
$this->assertNotNull($sContent, "Call to CompleteSessionData should NOT fail due to Argument #1 (\$aJson) must be of type array, null given"); $this->assertNotNull($sContent, "Call to CompleteSessionData should NOT fail due to Argument #1 (\$aJson) must be of type array, null given");
} }
} }