diff --git a/.gitignore b/.gitignore index 462770aad..e0f65ee58 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,9 @@ test/vendor/* !/log/index.php !/log/web.config +# PHPUnit cache file +/test/.phpunit.result.cache + # Jetbrains /.idea/** diff --git a/application/applicationextension.inc.php b/application/applicationextension.inc.php index 94254dc95..5b658d3ee 100644 --- a/application/applicationextension.inc.php +++ b/application/applicationextension.inc.php @@ -1402,11 +1402,11 @@ interface iBackofficeDictEntriesPrefixesExtension /** * Implement this interface to add content to any enhanced portal page * - * IMPORTANT! Experimental API, may be removed at anytime, we don't recommend to use it just now! - * * @api * @package Extensibility - * @since 2.4.0 + * + * @since 2.4.0 interface creation + * @since 2.7.0 change method signatures due to Silex to Symfony migration */ interface iPortalUIExtension { @@ -1479,7 +1479,11 @@ interface iPortalUIExtension } /** - * IMPORTANT! Experimental API, may be removed at anytime, we don't recommend to use it just now! + * Extend this class instead of iPortalUIExtension if you don't need to overload all methods + * + * @api + * @package Extensibility + * @since 2.4.0 */ abstract class AbstractPortalUIExtension implements iPortalUIExtension { diff --git a/application/forms.class.inc.php b/application/forms.class.inc.php index d9280a03b..d0665625b 100644 --- a/application/forms.class.inc.php +++ b/application/forms.class.inc.php @@ -1272,7 +1272,7 @@ class DesignerComboField extends DesignerFormField $sChecked = $this->defaultValue ? 'checked' : ''; $sMandatory = $this->bMandatory ? 'true' : 'false'; $sReadOnly = $this->IsReadOnly() ? 'disabled="disabled"' : ''; - if ($this->IsSorted()) + if ($this->IsSorted() && isset($this->aAllowedValues) ) { asort($this->aAllowedValues); } @@ -1320,19 +1320,17 @@ class DesignerComboField extends DesignerFormField $sHtml .= ""; } } - foreach($this->aAllowedValues as $sKey => $sDisplayValue) - { - if ($this->bMultipleSelection) - { - $sSelected = in_array($sKey, $this->defaultValue) ? 'selected' : ''; + if ( isset($this->aAllowedValues) ) { + foreach ($this->aAllowedValues as $sKey => $sDisplayValue) { + if ($this->bMultipleSelection) { + $sSelected = in_array($sKey, $this->defaultValue) ? 'selected' : ''; + } else { + $sSelected = ($sKey == $this->defaultValue) ? 'selected' : ''; + } + // Quick and dirty: display the menu parents as a tree + $sHtmlValue = str_replace(' ', ' ', $sDisplayValue); + $sHtml .= ""; } - else - { - $sSelected = ($sKey == $this->defaultValue) ? 'selected' : ''; - } - // Quick and dirty: display the menu parents as a tree - $sHtmlValue = str_replace(' ', ' ', htmlentities($sDisplayValue, ENT_QUOTES, 'UTF-8')); - $sHtml .= ""; } $sHtml .= ""; if ($this->bOtherChoices) diff --git a/core/cmdbchangeop.class.inc.php b/core/cmdbchangeop.class.inc.php index 85dbb9283..d4d90e308 100644 --- a/core/cmdbchangeop.class.inc.php +++ b/core/cmdbchangeop.class.inc.php @@ -78,23 +78,31 @@ class CMDBChangeOp extends DBObject implements iCMDBChangeOp } /** - * @inheritDoc - */ + * Describe (as a text string) the modifications corresponding to this change + */ public function GetDescription() { return ''; } /** - * Safety net: in case the change is not given, let's guarantee that it will - * be set to the current ongoing change (or create a new one) - */ + * Safety net: + * * if change isn't persisted yet, use the current change and persist it if needed + * * in case the change is not given, let's guarantee that it will be set to the current ongoing change (or create a new one) + * + * @since 2.7.7 3.0.2 3.1.0 N°3717 do persist the current change if needed + */ protected function OnInsert() { - if ($this->Get('change') <= 0) - { - $this->Set('change', CMDBObject::GetCurrentChange()); + $iChange = $this->Get('change'); + if (($iChange <= 0) || (is_null($iChange))) { + $oChange = CMDBObject::GetCurrentChange(); + if ($oChange->IsNew()) { + $oChange->DBWrite(); + } + $this->Set('change', $oChange); } + parent::OnInsert(); } } diff --git a/core/cmdbobject.class.inc.php b/core/cmdbobject.class.inc.php index 4a070d239..ed687e9ca 100644 --- a/core/cmdbobject.class.inc.php +++ b/core/cmdbobject.class.inc.php @@ -113,6 +113,26 @@ abstract class CMDBObject extends DBObject self::$m_oCurrChange = $oChange; } + /** + * @param string $sUserInfo + * @param string $sOrigin + * @param \DateTime $oDate + * + * @throws \CoreException + * + * @since 2.7.7 3.0.2 3.1.0 N°3717 new method to reset current change + */ + public static function SetCurrentChangeFromParams($sUserInfo, $sOrigin = null, $oDate = null) + { + static::SetTrackInfo($sUserInfo); + static::SetTrackOrigin($sOrigin); + static::CreateChange(); + + if (!is_null($oDate)) { + static::$m_oCurrChange->Set("date", $oDate); + } + } + // // Todo: simplify the APIs and do not pass the current change as an argument anymore // SetTrackInfo to be invoked in very few cases (UI.php, CSV import, Data synchro) @@ -144,6 +164,8 @@ abstract class CMDBObject extends DBObject * $oMyChange->Set("userinfo", 'this is done by ... for ...'); * $iChangeId = $oMyChange->DBInsert(); * + * **warning** : this will do nothing if current change already exists ! + * * @see SetCurrentChange to specify a CMDBObject instance instead * * @param string $sInfo @@ -171,6 +193,8 @@ abstract class CMDBObject extends DBObject /** * Provides information about the origin of the change * + * **warning** : this will do nothing if current change already exists ! + * * @see SetTrackInfo * @see SetCurrentChange to specify a CMDBObject instance instead * @@ -181,18 +205,15 @@ abstract class CMDBObject extends DBObject { self::$m_sOrigin = $sOrigin; } - + /** * Get the additional information (defaulting to user name) - */ - protected static function GetTrackInfo() + */ + public static function GetTrackInfo() { - if (is_null(self::$m_sInfo)) - { + if (is_null(self::$m_sInfo)) { return CMDBChange::GetCurrentUserName(); - } - else - { + } else { return self::$m_sInfo; } } @@ -243,6 +264,9 @@ abstract class CMDBObject extends DBObject * @throws \CoreWarning * @throws \MySQLException * @throws \OQLException + * + * @since 2.7.7 3.0.2 3.1.0 N°3717 {@see CMDBChange} **will be persisted later** in {@see \CMDBChangeOp::OnInsert} (was done previously directly here) + * This will avoid creating in DB CMDBChange lines without any corresponding CMDBChangeOp */ protected static function CreateChange() { @@ -251,7 +275,6 @@ abstract class CMDBObject extends DBObject self::$m_oCurrChange->Set("userinfo", self::GetTrackInfo()); self::$m_oCurrChange->Set("user_id", self::GetTrackUserId()); self::$m_oCurrChange->Set("origin", self::GetTrackOrigin()); - self::$m_oCurrChange->DBInsert(); } /** diff --git a/css/light-grey.scss b/css/light-grey.scss index 85d4bc350..c3d0522ed 100644 --- a/css/light-grey.scss +++ b/css/light-grey.scss @@ -2582,6 +2582,12 @@ } } } + + &[data-attribute-type="AttributeDuration"] { + .field_value_container { + white-space: nowrap; + } + } } .one-col-details .details .field_container.field_small { diff --git a/datamodels/2.x/combodo-db-tools/dbtools.php b/datamodels/2.x/combodo-db-tools/dbtools.php index bfdd797b4..e48b9dbf0 100644 --- a/datamodels/2.x/combodo-db-tools/dbtools.php +++ b/datamodels/2.x/combodo-db-tools/dbtools.php @@ -359,9 +359,7 @@ function DisplayLostAttachments(iTopWebPage &$oP, ApplicationContext &$oAppConte $sHistoryEntry = Dict::Format('DBTools:LostAttachments:History', $oOrmDocument->GetFileName()); CMDBObject::SetTrackInfo(UserRights::GetUserFriendlyName()); $oChangeOp = MetaModel::NewObject('CMDBChangeOpPlugin'); - /** @var \Change $oChange */ - $oChange = CMDBObject::GetCurrentChange(); - $oChangeOp->Set('change', $oChange->GetKey()); + // CMDBChangeOp.change will be automatically filled $oChangeOp->Set('objclass', $sTargetClass); $oChangeOp->Set('objkey', $sTargetId); $oChangeOp->Set('description', $sHistoryEntry); diff --git a/js/forms-json-utils.js b/js/forms-json-utils.js index daaf94d0c..8ff1956b2 100644 --- a/js/forms-json-utils.js +++ b/js/forms-json-utils.js @@ -304,75 +304,61 @@ function ValidateField(sFieldId, sPattern, bMandatory, sFormId, nullValue, origi function ValidateCKEditField(sFieldId, sPattern, bMandatory, sFormId, nullValue, originalValue) { - var bValid; - var sExplain = ''; - var sTextContent; + if ($('#'+sFieldId).size() > 0) { + var bValid; + var sExplain = ''; + var sTextContent; - if ($('#'+sFieldId).prop('disabled')) - { - bValid = true; // disabled fields are not checked - } - else - { - // Get the contents without the tags - var oFormattedContents = $("#cke_"+sFieldId+" iframe"); - if (oFormattedContents.length == 0) - { - var oSourceContents = $("#cke_"+sFieldId+" textarea.cke_source"); - sTextContent = oSourceContents.val(); - } - else - { - sTextContent = oFormattedContents.contents().find("body").text(); - - if (sTextContent == '') - { - // No plain text, maybe there is just an image... - var oImg = oFormattedContents.contents().find("body img"); - if (oImg.length != 0) - { - sTextContent = 'image'; + if ($('#'+sFieldId).prop('disabled')) { + bValid = true; // disabled fields are not checked + } else { + // Get the contents without the tags + var oFormattedContents = $("#cke_"+sFieldId+" iframe"); + if (oFormattedContents.length == 0) { + var oSourceContents = $("#cke_"+sFieldId+" textarea.cke_source"); + sTextContent = oSourceContents.val(); + } else { + sTextContent = oFormattedContents.contents().find("body").text(); + + if (sTextContent == '') { + // No plain text, maybe there is just an image... + var oImg = oFormattedContents.contents().find("body img"); + if (oImg.length != 0) { + sTextContent = 'image'; + } } } - } - // Get the original value without the tags - var oFormattedOriginalContents = (originalValue !== undefined) ? $('
').html(originalValue) : undefined; - var sTextOriginalContents = (oFormattedOriginalContents !== undefined) ? oFormattedOriginalContents.text() : undefined; - - if (bMandatory && (sTextContent == nullValue)) - { - bValid = false; - sExplain = Dict.S('UI:ValueMustBeSet'); - } - else if ((sTextOriginalContents != undefined) && (sTextContent == sTextOriginalContents)) - { - bValid = false; - if (sTextOriginalContents == nullValue) - { + // Get the original value without the tags + var oFormattedOriginalContents = (originalValue !== undefined) ? $('
').html(originalValue) : undefined; + var sTextOriginalContents = (oFormattedOriginalContents !== undefined) ? oFormattedOriginalContents.text() : undefined; + + if (bMandatory && (sTextContent == nullValue)) { + bValid = false; sExplain = Dict.S('UI:ValueMustBeSet'); - } - else - { - // Note: value change check is not working well yet as the HTML to Text conversion is not exactly the same when done from the PHP value or the CKEditor value. - sExplain = Dict.S('UI:ValueMustBeChanged'); + } else if ((sTextOriginalContents != undefined) && (sTextContent == sTextOriginalContents)) { + bValid = false; + if (sTextOriginalContents == nullValue) { + sExplain = Dict.S('UI:ValueMustBeSet'); + } else { + // Note: value change check is not working well yet as the HTML to Text conversion is not exactly the same when done from the PHP value or the CKEditor value. + sExplain = Dict.S('UI:ValueMustBeChanged'); + } + } else { + bValid = true; } } - else - { - bValid = true; + + ReportFieldValidationStatus(sFieldId, sFormId, bValid, sExplain); + + if ($('#'+sFieldId).data('timeout_validate') == undefined) { + // We need to check periodically as CKEditor doesn't trigger our events. More details in UIHTMLEditorWidget::Display() @ line 92 + var iTimeoutValidate = setInterval(function () { + ValidateCKEditField(sFieldId, sPattern, bMandatory, sFormId, nullValue, originalValue); + }, 500); + $('#'+sFieldId).data('timeout_validate', iTimeoutValidate); } } - - ReportFieldValidationStatus(sFieldId, sFormId, bValid, sExplain); - - if ($('#'+sFieldId).data('timeout_validate') == undefined) { - // We need to check periodically as CKEditor doesn't trigger our events. More details in UIHTMLEditorWidget::Display() @ line 92 - var iTimeoutValidate = setInterval(function () { - ValidateCKEditField(sFieldId, sPattern, bMandatory, sFormId, nullValue, originalValue); - }, 500); - $('#'+sFieldId).data('timeout_validate', iTimeoutValidate); - } } function ResetPwd(id) diff --git a/pages/ajax.render.php b/pages/ajax.render.php index 81a5a50ff..cc043316c 100644 --- a/pages/ajax.render.php +++ b/pages/ajax.render.php @@ -1118,7 +1118,16 @@ EOF $aUpdatedDecoded = array(); foreach ($aUpdatedProperties as $sProp) { $sDecodedProp = str_replace('attr_', '', $sProp); // Remove the attr_ prefix - $aCurrentValues[$sDecodedProp] = (isset($aPreviousValues[$sProp]) ? $aPreviousValues[$sProp] : ''); // Set the previous value + // Set the previous value + if ( isset($aPreviousValues[$sProp]) && $aPreviousValues[$sProp] != '' ){ + $aCurrentValues[$sDecodedProp] = $aPreviousValues[$sProp]; + } else { + if(gettype($aCurrentValues[$sDecodedProp]) == "array") { + $aCurrentValues[$sDecodedProp] = []; + } else { + $aCurrentValues[$sDecodedProp] = ''; + } + } $aUpdatedDecoded[] = $sDecodedProp; } diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index 1330f4a2c..cfbfb3653 100644 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -19,9 +19,47 @@ * */ -class MissingDependencyException extends Exception +class MissingDependencyException extends CoreException { + /** + * @see \ModuleDiscovery::OrderModulesByDependencies property init + * @var array> + * module id as key + * another array as value, containing : 'module' with module info, 'dependencies' with missing dependencies + */ public $aModulesInfo; + + /** + * @return string HTML to print to the user the modules impacted + * @since 2.7.7 3.0.2 3.1.0 PR #280 + */ + public function getHtmlDesc($sHighlightHtmlBegin = null, $sHighlightHtmlEnd = null) + { + $sErrorMessage = <<The following modules have unmet dependencies:

+