Compare commits

..

1 Commits

Author SHA1 Message Date
Anne-Catherine
e793b02f8b N°6766 - Fix dependent fields not updated due to WizardHelper.UpdateFields() being triggered too early (#548)
* N°6766 - Javascript : function WizardHelper.UpdateFields triggered to early does not update fields

* N°6766 - Code review

---------

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2023-09-26 12:25:56 +02:00
31 changed files with 146 additions and 389 deletions

View File

@@ -37,7 +37,7 @@ iTop also offers mass import tools to help you being even more efficient.
- [iTop Forums][1]: community support
- [iTop Tickets][2]: for feature requests and bug reports
- [Releases download][3]
- [iTop requirements][4]
- [Software requirements][4]
- [Documentation][5] covering both iTop and its official extensions
- [iTop Hub][6] : discover and install extensions !
@@ -45,7 +45,7 @@ iTop also offers mass import tools to help you being even more efficient.
[1]: https://sourceforge.net/p/itop/discussion/
[2]: https://sourceforge.net/p/itop/tickets/
[3]: https://sourceforge.net/projects/itop/files/itop/
[4]: https://www.itophub.io/wiki/page?id=latest:install:requirements
[4]: https://www.itophub.io/wiki/page?id=latest:install:upgrading_itop
[5]: https://www.itophub.io/wiki
[6]: https://store.itophub.io/en_US/

View File

@@ -3054,16 +3054,7 @@ EOF
// Hook the cancel button via jQuery so that it can be unhooked easily as well if needed
$sDefaultUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=search_form&class='.$sClass.'&'.$oAppContext->GetForLink();
$sCancelButtonOnClickScript = "let fOnClick{$this->m_iFormId}CancelButton = ";
if(isset($aExtraParams['js_handlers']['cancel_button_on_click'])){
$sCancelButtonOnClickScript .= $aExtraParams['js_handlers']['cancel_button_on_click'];
} else {
$sCancelButtonOnClickScript .= "function() { BackToDetails('$sClass', $iKey, '$sDefaultUrl', $sJSToken)};";
}
$sCancelButtonOnClickScript .= "$('#form_{$this->m_iFormId} button.cancel').on('click', fOnClick{$this->m_iFormId}CancelButton);";
$oPage->add_ready_script($sCancelButtonOnClickScript);
$oPage->add_ready_script("$('#form_{$this->m_iFormId} button.cancel').on('click', function() { BackToDetails('$sClass', $iKey, '$sDefaultUrl', $sJSToken)} );");
$iFieldsCount = count($aFieldsMap);
$sJsonFieldsMap = json_encode($aFieldsMap);

View File

@@ -386,7 +386,7 @@ EOF;
if (!$oPage->IsPrintableVersion())
{
$sMenuTitle = Dict::S('UI:ConfigureThisList');
$sHtml = '<div class="itop_popup toolkit_menu" id="tk_'.$this->iListId.'"><ul><li aria-label="'.Dict::S('UI:Menu:Toolkit').'"><i class="fas fa-tools"></i><i class="fas fa-caret-down"></i><ul>';
$sHtml = '<div class="itop_popup toolkit_menu" id="tk_'.$this->iListId.'"><ul><li><i class="fas fa-tools"></i><i class="fas fa-caret-down"></i><ul>';
$oMenuItem1 = new JSPopupMenuItem('iTop::ConfigureList', $sMenuTitle, "$('#datatable_dlg_".$this->iListId."').dialog('open');");
$aActions = array(

View File

@@ -304,7 +304,7 @@ class DisplayBlock
}
}
}
public function GetFilter()
{
return $this->m_oFilter;
@@ -1045,7 +1045,7 @@ JS
$aCount = $aCounts[$sStateValue];
$sHyperlink = $aCount['link'];
$sCountLabel = $aCount['label'];
$oPill = PillFactory::MakeForState($sClass, $sStateValue);
// N°5849 - Unencode label for ExternalKey attribute because friendlyname is already html encoded thanks to DBObject::GetName() in AttributeExternalKey::GetAllowedValues(). (A fix in this function may have too much impact).
if ($oAttDef instanceof AttributeExternalKey) {
@@ -1611,7 +1611,6 @@ JS
$iTotalCount = 0;
$aURLs = array();
foreach ($aRes as $iRow => $aRow) {
$sValue = $aRow['grouped_by_1'];
$sHtmlValue = $oGroupByExp->MakeValueLabel($this->m_oFilter, $sValue, $sValue);
@@ -1622,7 +1621,6 @@ JS
'value' => (float)$aRow[$sFctVar],
);
// Build the search for this subset
$oSubsetSearch = $this->m_oFilter->DeepClone();
$oCondition = new BinaryExpression($oGroupByExp, '=', new ScalarExpression($sValue));
@@ -1639,13 +1637,9 @@ JS
switch ($sChartType) {
case 'bars':
$iMaxNbCharsInLabel = 0;
$aNames = [];
$aNames = array();
foreach ($aValues as $idx => $aValue) {
$aNames[$idx] = $aValue['label'];
if ($iMaxNbCharsInLabel < mb_strlen($aValue['label'])) {
$iMaxNbCharsInLabel = mb_strlen($aValue['label']);
}
}
$oBlock = new BlockChartAjaxBars();
$oBlock->sJSNames = json_encode($aNames);
@@ -1653,31 +1647,21 @@ JS
$oBlock->sId = $sId;
$oBlock->sJSURLs = $sJSURLs;
$oBlock->sURLForRefresh = str_replace("'", "\'", $sUrl);
$oBlock->iMaxNbCharsInLabel = $iMaxNbCharsInLabel;
break;
case 'pie':
$aColumns = [];
$aNames = [];
$aColumns = array();
$aNames = array();
foreach ($aValues as $idx => $aValue) {
$aColumns[] = array('series_'.$idx, (float)$aValue['value']);
$aNames['series_'.$idx] = $aValue['label'];
}
$iNbLinesToAddForName = 0;
if (count($aNames) > 50) {
// Calculation of the number of legends line add to the height of the graph to have a maximum of 5 legend columns
$iNbLinesIncludedInChartHeight = 10;
$iNbLinesToAddForName = ceil(count($aNames) / 5) - $iNbLinesIncludedInChartHeight;
}
$oBlock = new BlockChartAjaxPie();
$oBlock->sJSColumns = json_encode($aColumns);
$oBlock->sJSNames = json_encode($aNames);
$oBlock->sId = $sId;
$oBlock->sJSURLs = $sJSURLs;
$oBlock->sURLForRefresh = str_replace("'", "\'", $sUrl);
$oBlock->iNbLinesToAddForName = $iNbLinesToAddForName;
break;
}
if (isset($aExtraParams["surround_with_panel"]) && $aExtraParams["surround_with_panel"]) {

View File

@@ -211,23 +211,7 @@ class UILinksWidgetDirect
$oObj = DBObject::MakeDefaultInstance($sRealClass);
$aPrefillParam = array('source_obj' => $oSourceObj);
$oObj->PrefillForm('creation_from_editinplace', $aPrefillParam);
$aFormExtraParams = array(
'formPrefix' => $this->sInputid,
'noRelations' => true,
'fieldsFlags' => $aFieldFlags,
'js_handlers' => [
'cancel_button_on_click' =>
<<<JS
function() {
// Do nothing, already handled by linksdirectwidget.js
};
JS
,
],
);
cmdbAbstractObject::DisplayCreationForm($oPage, $sRealClass, $oObj, array(), $aFormExtraParams);
cmdbAbstractObject::DisplayCreationForm($oPage, $sRealClass, $oObj, array(), array('formPrefix' => $this->sInputid, 'noRelations' => true, 'fieldsFlags' => $aFieldFlags));
}
else
{

View File

@@ -347,6 +347,7 @@ class WizardHelper
/**
* @return string JS code to be executed for fields update
* @since 3.0.0 N°3198
* @deprecated 3.0.3-2 3.0.4 3.1.1 3.2.0 Use {@see \WizardHelper::AddJsForUpdateFields()} instead
*/
public function GetJsForUpdateFields()
{
@@ -359,15 +360,39 @@ class WizardHelper
JS;
}
/**
* Add necessary JS snippets (to the page) to be executed for fields update
*
* @param \WebPage $oPage
* @return void
* @since 3.0.3-2 3.0.4 3.1.1 3.2.0 N°6766
*/
public function AddJsForUpdateFields(WebPage $oPage)
{
$sWizardHelperJsVar = (!is_null($this->m_aData['m_sWizHelperJsVarName'])) ? utils::Sanitize($this->m_aData['m_sWizHelperJsVarName'], '', utils::ENUM_SANITIZATION_FILTER_PARAMETER) : 'oWizardHelper'.$this->GetFormPrefix();
$sWizardHelperJson = $this->ToJSON();
$oPage->add_script(<<<JS
{$sWizardHelperJsVar}.m_oData = {$sWizardHelperJson};
{$sWizardHelperJsVar}.UpdateFields();
JS
);
$oPage->add_ready_script(<<<JS
if ({$sWizardHelperJsVar}.m_oDependenciesUpdatedPromiseResolve !== null){
{$sWizardHelperJsVar}.m_oDependenciesUpdatedPromiseResolve();
}
JS
);
}
static function ParseJsonSet($oMe, $sLinkClass, $sExtKeyToMe, $sJsonSet)
{
$aSet = json_decode($sJsonSet, true); // true means hash array instead of object
$oSet = CMDBObjectSet::FromScratch($sLinkClass);
foreach ($aSet as $aLinkObj)
{
foreach ($aSet as $aLinkObj) {
$oLink = MetaModel::NewObject($sLinkClass);
foreach ($aLinkObj as $sAttCode => $value)
{
foreach ($aLinkObj as $sAttCode => $value) {
$oAttDef = MetaModel::GetAttributeDef($sLinkClass, $sAttCode);
if (($oAttDef->IsExternalKey()) && ($value != '') && ($value > 0))
{

View File

@@ -48,9 +48,10 @@ abstract class HTMLSanitizer
$sSanitizerClass = 'HTMLDOMSanitizer';
} else if (!is_subclass_of($sSanitizerClass, 'HTMLSanitizer')) {
if ($sConfigKey === 'html_sanitizer') {
IssueLog::Warning('The configured "'.$sConfigKey.'" class "'.$sSanitizerClass.'" is not a subclass of '.HTMLSanitizer::class.'. Will use HTMLDOMSanitizer as the default sanitizer.');
IssueLog::Warning('The configured "'.$sConfigKey.'" class "'.$sSanitizerClass.'" is not a subclass of HTMLSanitizer. Will use HTMLDOMSanitizer as the default sanitizer.');
$sSanitizerClass = 'HTMLDOMSanitizer';
} else {
}
if ($sConfigKey === 'svg_sanitizer') {
IssueLog::Error('The configured "'.$sConfigKey.'" class "'.$sSanitizerClass.'" is not a subclass of '.HTMLSanitizer::class.' ! Won\'t sanitize string.');
return $sHTML;

View File

@@ -961,9 +961,7 @@ class ToolsLog extends LogAPI
/**
* @see \CMDBSource::LogDeadLock()
* @since 2.7.1 PR #139
*
* @link https://dev.mysql.com/doc/refman/5.7/en/innodb-deadlocks.html
* @since 2.7.1
*/
class DeadLockLog extends LogAPI
{
@@ -988,10 +986,10 @@ class DeadLockLog extends LogAPI
{
switch ($iMysqlErrorNo)
{
case CMDBSource::MYSQL_ERRNO_WAIT_TIMEOUT:
case 1205:
return self::CHANNEL_WAIT_TIMEOUT;
break;
case CMDBSource::MYSQL_ERRNO_DEADLOCK:
case 1213:
return self::CHANNEL_DEADLOCK_FOUND;
break;
default:
@@ -1021,14 +1019,7 @@ class DeadLockLog extends LogAPI
/**
* Starting with the WARNING level we will log in a dedicated file (/log/deprecated-calls.log) :
* - iTop deprecated files or code
* - protected trigger_error calls with E_DEPRECATED or E_USER_DEPRECATED
*
* For the last category, if {@see utils::IsDevelopmentEnvironment()} is true we will do a trigger_error()
*
* @since 3.0.0 N°3731 first implementation
* @link https://www.itophub.io/wiki/page?id=latest:admin:log:channels#deprecated_calls channel used
* @since 3.0.0 N°3731
*/
class DeprecatedCallsLog extends LogAPI
{

View File

@@ -6,6 +6,10 @@
$ibo-attachment--datatable--icon-preview--max-height: 44px !default;
$ibo-attachment--datatable--icon-preview--max-width: $ibo-attachment--datatable--icon-preview--max-height !default;
$ibo-attachment--datatable--line-height: $ibo-attachment--datatable--icon-preview--max-height !default;
$ibo-attachment--datatable--first-column--line-height: 0px !default;
$ibo-attachment--drag-in--border: 2px $ibo-color-grey-400 dashed !default;
$ibo-attachment--upload-file--drop-zone-hint--max-height: 200px !default;
$ibo-attachment--upload-file--drop-zone-hint--margin: 22px $ibo-spacing-0 !default;
@@ -29,7 +33,10 @@ $ibo-attachment--tab-header--drop-in--icon--color: $ibo-color-blue-600 !default;
max-width: $ibo-attachment--datatable--icon-preview--max-width;
}
.ibo-attachment--datatable tbody tr td {
vertical-align: middle;
line-height: $ibo-attachment--datatable--line-height;
}
.ibo-attachment--datatable tbody tr td:nth-child(1){
line-height: $ibo-attachment--datatable--first-column--line-height;
}
.ibo-attachment--upload-file--drop-zone-hint{

View File

@@ -3,7 +3,7 @@
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
@@ -69,7 +69,7 @@ class UserLocal extends UserInternal
const EXPIRE_NEVER = 'never_expire';
const EXPIRE_FORCE = 'force_expire';
const EXPIRE_ONE_TIME_PWD = 'otp_expire';
/** @var UserLocalPasswordValidity|null */
protected $m_oPasswordValidity = null;
@@ -160,7 +160,7 @@ class UserLocal extends UserInternal
/**
* Use with care!
*/
*/
public function SetPassword($sNewPassword)
{
$this->Set('password', $sNewPassword);
@@ -197,39 +197,19 @@ class UserLocal extends UserInternal
protected function OnWrite()
{
if (empty($this->m_oPasswordValidity))
{
return;
}
if (array_key_exists('password_renewed_date', $this->ListChanges()))
{
return;
}
if (empty($this->m_oPasswordValidity))
{
//password unchanged
if (is_null($this->Get('password_renewed_date')))
{
//initialize password_renewed_date with User creation date
$sKey = $this->GetKey();
$sOql = <<<OQL
SELECT CMDBChangeOpCreate AS ccc
JOIN CMDBChange AS c ON ccc.change = c.id
WHERE ccc.objclass="UserLocal" AND ccc.objkey="$sKey"
OQL;
$oCmdbChangeOpSearch = \DBObjectSearch::FromOQL($sOql);
$oSet = new \DBObjectSet($oCmdbChangeOpSearch);
$oCMDBChangeOpCreate = $oSet->Fetch();
if (! is_null($oCMDBChangeOpCreate))
{
$oUserCreationDateTime = \DateTime::createFromFormat(AttributeDateTime::GetInternalFormat(), $oCMDBChangeOpCreate->Get('date'));
$sCreationDate = $oUserCreationDateTime->format(\AttributeDate::GetInternalFormat());
$this->Set('password_renewed_date', $sCreationDate);
}
}
return;
}
$sNow = date(\AttributeDate::GetInternalFormat());
$this->Set('password_renewed_date', $sNow);
// Reset the "force" expiration flag when the user updates her/his own password!
if ($this->IsCurrentUser())
{
@@ -314,7 +294,7 @@ OQL;
{
$this->m_aCheckIssues[] = $this->m_oPasswordValidity->getPasswordValidityMessage();
}
// A User cannot force a one-time password on herself/himself
if ($this->IsCurrentUser()) {
if (array_key_exists('expiration', $this->ListChanges()) && ($this->Get('expiration') == self::EXPIRE_ONE_TIME_PWD)) {

View File

@@ -137,9 +137,6 @@ try
* As a result we're setting a token file to make sure the restore is called by an authenticated user with the correct rights !
*/
case 'restore_get_token':
$oPage = new JsonPage();
$oPage->SetOutputDataOnly(true);
$sEnvironment = utils::ReadParam('environment', 'production', false, 'raw_data');
$oRestoreMutex = new iTopMutex('restore.'.$sEnvironment);
if ($oRestoreMutex->IsLocked())
@@ -152,7 +149,12 @@ try
$sTokenFile = APPROOT.'/data/restore.'.$sToken.'.tok';
file_put_contents($sTokenFile, $sFile);
$oPage->SetData(['token' => $sToken]);
$oPage->add_ready_script(
<<<JS
$("#restore_token").val('$sToken');
JS
);
$oPage->output();
break;

View File

@@ -471,7 +471,7 @@ function LaunchRestoreNow(sBackupFile, sConfirmationMessage)
$.post(GetAbsoluteUrlModulePage('itop-backup', 'ajax.backup.php'), oParams, function(data){
// Get the value of restore_token
$('#restore_token').val(data.token);
$('#backup_errors').append(data);
var oParams = {};
oParams.operation = 'restore_exec';

View File

@@ -40,7 +40,6 @@ use Dict;
use DOMDocument;
use DOMXPath;
use Exception;
use ExceptionLog;
use InlineImage;
use IssueLog;
use MetaModel;
@@ -1143,7 +1142,6 @@ class ObjectFormManager extends FormManager
$sObjectClass = get_class($this->oObject);
$bExceptionLogged = false;
try {
// modification flags
$bIsNew = $this->oObject->IsNew();
@@ -1165,14 +1163,6 @@ class ObjectFormManager extends FormManager
throw new Exception($e->getHtmlMessage());
}
catch (Exception $e) {
$aContext = [
'origin' => __CLASS__.'::'.__METHOD__,
'obj_class' => get_class($this->oObject),
'obj_key' => $this->oObject->GetKey(),
];
ExceptionLog::LogException($e, $aContext);
$bExceptionLogged = true;
if ($bIsNew) {
throw new Exception(Dict::S('Portal:Error:ObjectCannotBeCreated'));
}
@@ -1232,12 +1222,11 @@ class ObjectFormManager extends FormManager
}
}
}
catch (Exception $e) {
catch (Exception $e)
{
$aData['valid'] = false;
$aData['messages']['error'] += array('_main' => array($e->getMessage()));
if (false === $bExceptionLogged) {
IssueLog::Error(__METHOD__.' at line '.__LINE__.' : '.$e->getMessage());
}
IssueLog::Error(__METHOD__.' at line '.__LINE__.' : '.$e->getMessage());
}
return $aData;

View File

@@ -323,7 +323,7 @@ $(function()
oParams.dashletid = sTempDashletId;
$.post(this.options.new_dashletid_endpoint, oParams, function (data) {
me.add_dashlet_prepare(options, data.trim());
me.add_dashlet_prepare(options, data);
});
},
add_dashlet_ajax: function (options, sDashletId) {

View File

@@ -794,12 +794,8 @@ const CombodoTooltip = {
InitTooltipFromMarkup: function (oElem, bForce = false) {
const oOptions = {};
// First, check if the jQuery element actually represent DOM elements
if (oElem.length === 0) {
return false;
}
// Then, check if the tooltip isn't already instantiated
else if ((oElem.attr('data-tooltip-instantiated') === 'true') && (bForce === false)) {
// First, check if the tooltip isn't already instantiated
if ((oElem.attr('data-tooltip-instantiated') === 'true') && (bForce === false)) {
return false;
}
else if((oElem.attr('data-tooltip-instantiated') === 'true') && (bForce === true) && (oElem[0]._tippy !== undefined)){

View File

@@ -72,6 +72,11 @@ function WizardHelper(sClass, sFormPrefix, sState, sInitialState, sStimulus) {
'm_sWizHelperJsVarName': null // if set will use this name when server returns JS code in \WizardHelper::GetJsForUpdateFields
};
this.m_oData.m_sClass = sClass;
/**
* Promise resolve callback when dependencies have been updated
* @since 3.0.3-2 3.0.4 3.1.1 3.2.0 N°6766
* */
this.m_oDependenciesUpdatedPromiseResolve = null;
// Setting optional transition data
if (sInitialState !== undefined)
@@ -138,6 +143,7 @@ function WizardHelper(sClass, sFormPrefix, sState, sInitialState, sStimulus) {
};
this.UpdateFields = function () {
const me = this;
var aRefreshed = [];
//console.log('** UpdateFields **');
// Set the full HTML for the input field
@@ -171,10 +177,17 @@ function WizardHelper(sClass, sFormPrefix, sState, sInitialState, sStimulus) {
}
}
// For each "refreshed" field, asynchronously trigger a change in case there are dependent fields to update
for (i = 0; i < aRefreshed.length; i++)
{
for (i = 0; i < aRefreshed.length; i++) {
var sString = "$('#"+aRefreshed[i]+"').trigger('change').trigger('update');";
window.setTimeout(sString, 1); // Synchronous 'trigger' does nothing, call it asynchronously
const oPromise = new Promise(function (resolve) {
// Store the resolve callback so we can call it later from outside
me.m_oDependenciesUpdatedPromiseResolve = resolve;
});
oPromise.then(function () {
window.setTimeout(sString, 1); // Synchronous 'trigger' does nothing, call it asynchronously
// Resolve callback is reinitialized in case the redirection fails for any reason and we might need to retry
me.m_oDependenciesUpdatedPromiseResolve = null;
});
}
if($('[data-field-status="blocked"]').length === 0) {
$('.disabledDuringFieldLoading').prop("disabled", false).removeClass('disabledDuringFieldLoading');

View File

@@ -575,7 +575,7 @@ try
}
}
}
$oPage->add_script($oWizardHelper->GetJsForUpdateFields());
$oWizardHelper->AddJsForUpdateFields($oPage);
break;
case 'obj_creation_form':

View File

@@ -59,43 +59,8 @@ class ConsoleSimpleFieldRenderer extends FieldRenderer
else
{
$oBlock = FieldUIBlockFactory::MakeStandard($this->oField->GetLabel());
$oBlock->SetAttLabel($this->oField->GetLabel())
->AddDataAttribute("input-id",$this->oField->GetGlobalId())
->AddDataAttribute("input-type",$sFieldClass);
// Propagate data attribute from Field to UIBlock
// Note: This might no longer be necessary after the upcoming attributes rework project
foreach ($this->oField->GetMetadata() as $sMetadataKey => $sMetadataValue) {
switch ($sMetadataKey) {
// Important: Only some data attributes can be overloaded, this is done on purpose (eg. "input-type" set previously by an AttributeCustomFields)
case 'attribute-code':
case 'attribute-type':
case 'input-type':
if (utils::IsNotNullOrEmptyString($sMetadataValue)) {
switch ($sMetadataKey) {
case 'attribute-code':
$oBlock->SetAttCode($sMetadataValue);
break;
case 'attribute-type':
$oBlock->SetAttType($sMetadataValue ?? '');
break;
case 'input-type':
$oBlock->AddDataAttribute($sMetadataKey, $sMetadataValue ?? '');
break;
}
}
break;
default:
if (false === $oBlock->HasDataAttribute($sMetadataKey)) {
$oBlock->AddDataAttribute($sMetadataKey, $sMetadataValue ?? '');
}
break;
}
}
$oBlock->AddDataAttribute("input-id",$this->oField->GetGlobalId());
$oBlock->AddDataAttribute("input-type",$sFieldClass);
switch ($sFieldClass)
{
case 'Combodo\\iTop\\Form\\Field\\DateTimeField':

View File

@@ -5,7 +5,6 @@
*/
namespace Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\CaseLogEntryForm;
use AttributeCaseLog;
use cmdbAbstractObject;
use Combodo\iTop\Application\UI\Base\Component\Input\RichText\RichText;
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
@@ -106,15 +105,6 @@ class CaseLogEntryForm extends UIContentBlock
return $this->sAttCode;
}
/**
* @return string
* @since 3.0.4 3.1.0 N°6139
*/
public function GetAttType(): string
{
return AttributeCaseLog::class;
}
/**
* @see static::$sAttCode
* @return string

View File

@@ -599,18 +599,6 @@ abstract class UIBlock implements iUIBlock
return $this;
}
/**
* @param string $sName Name of the data attribute
*
* @return bool True if $sName is already defined (even as a null value) in the UIBLock data attributes, false otherwise
* @see static::$aDataAttributes
* @since 3.0.4 3.1.0 N°6140
*/
public function HasDataAttribute(string $sName): bool
{
return array_key_exists($sName, $this->aDataAttributes);
}
/**
* @return bool
* @see static::$aDataAttributes

View File

@@ -28,9 +28,7 @@ class BlockChartAjaxBars extends UIBlock
public $sId;
/** @var string */
public $sJSURLs;
/** @var string */
public $sURLForRefresh;
/** @var int */
public $iMaxNbCharsInLabel;
}

View File

@@ -28,8 +28,6 @@ class BlockChartAjaxPie extends UIBlock
public $sJSURLs;
/** @var string */
public $sJSNames;
/** @var string */
public $sURLForRefresh;
/** @var int */
public $iNbLinesToAddForName;
}

View File

@@ -1566,7 +1566,7 @@ JS;
*/
protected function output_dict_entries($bReturnOutput = false)
{
if ($this->sContentType != 'text/plain' && $this->sContentType != 'application/json' && $this->sContentType != 'application/javascript') {
if ($this->sContentType != 'text/plain' && $this->sContentType != 'application/json') {
/** @var \iBackofficeDictEntriesExtension $oExtensionInstance */
foreach (MetaModel::EnumPlugins('iBackofficeDictEntriesExtension') as $oExtensionInstance) {
foreach ($oExtensionInstance->GetDictEntries() as $sDictEntry) {

View File

@@ -1,11 +1,5 @@
{# @copyright Copyright (C) 2010-2021 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
var iChartDefaultHeight = 200,
iChartLegendHeight = 6 * {{ oUIBlock.iMaxNbCharsInLabel }},
iChartTotalHeight = iChartDefaultHeight+iChartLegendHeight;
$('#my_chart_{{ oUIBlock.sId }}').height(iChartTotalHeight+'px');
var chart = c3.generate({
bindto: d3.select('#my_chart_{{ oUIBlock.sId }}'),
data: {

View File

@@ -1,12 +1,6 @@
{# @copyright Copyright (C) 2010-2021 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
// Calculate height of graph : 200px (minimum height for the chart) + 20*iNbLinesToAddForName for the legend
var iChartDefaultHeight = 200,
iChartLegendHeight = 20 * {{ oUIBlock.iNbLinesToAddForName }} ,
iChartTotalHeight = (iChartDefaultHeight+iChartLegendHeight);
$('#my_chart_{{ oUIBlock.sId }}').height(iChartTotalHeight+'px');
var chart = c3.generate({
bindto: d3.select('#my_chart_{{ oUIBlock.sId }}'),
data: {
@@ -15,7 +9,7 @@ var chart = c3.generate({
names: {{ oUIBlock.sJSNames|raw }},
onclick: function (d) {
var aURLs = {{ oUIBlock.sJSURLs|raw }};
window.location.href = aURLs[d.index];
window.location.href= aURLs[d.index];
},
order: null
},
@@ -25,19 +19,17 @@ var chart = c3.generate({
},
tooltip: {
format: {
value: function (value) {
return value;
}
value: function (value) { return value; }
}
},
}
});
if (typeof (charts) === "undefined")
if (typeof(charts) === "undefined")
{
charts = [];
refreshChart = [];
refreshChart = [];
}
var idxChart = charts.length;
var idxChart=charts.length;
charts.push(chart);
var refreshChart{{ oUIBlock.sId|sanitize(constant('utils::ENUM_SANITIZATION_FILTER_VARIABLE_NAME')) }}=' $.post("{{ oUIBlock.sURLForRefresh|raw }}&refresh='+idxChart+'","", function (data) {'+
'charts['+idxChart+'].unload();'+

View File

@@ -6,9 +6,6 @@
data-object-id="{{ oUIBlock.GetObjectId() }}"
data-attribute-code="{{ oUIBlock.GetAttCode() }}"
data-attribute-label="{{ oUIBlock.GetAttLabel() }}"
data-attribute-type="{{ oUIBlock.GetAttType() }}"
data-input-type="{{ constant('cmdbAbstractObject::ENUM_INPUT_TYPE_HTML_EDITOR') }}"
data-input-id="{{ oUIBlock.GetTextInput().GetId() }}"
data-submit-mode="{{ oUIBlock.GetSubmitMode() }}"
method="post">
<div class="ibo-caselog-entry-form--actions">

View File

@@ -294,31 +294,25 @@ class ItopDataTestCase extends ItopTestCase
* Create a UserRequest in database
*
* @param int $iNum
* @param array $aUserRequestCustomParams set fields values for the UserRequest : attcode as key, att value as value.
* If the attcode is already present in the default values, custom value will be kept (see array_merge documentation)
* @param int $iTimeSpent
* @param int $iOrgId
* @param int $iCallerId
*
* @return \UserRequest
* @throws \ArchivedObjectException
* @throws \CoreException
*
* @link https://www.php.net/manual/en/function.array-merge.php array_merge PHP function documentation
*
* @uses \array_merge()
* @uses createObject
* @throws Exception
*/
protected function CreateUserRequest($iNum, $aUserRequestCustomParams = []) {
$aUserRequestDefaultParams = [
protected function CreateUserRequest($iNum, $iTimeSpent = 0, $iOrgId = 0, $iCallerId = 0)
{
/** @var \UserRequest $oTicket */
$oTicket = $this->createObject('UserRequest', array(
'ref' => 'Ticket_'.$iNum,
'title' => 'BUG 1161_'.$iNum,
//'request_type' => 'incident',
'description' => 'Add aggregate functions',
'org_id' => $this->getTestOrgId(),
];
$aUserRequestParams = array_merge($aUserRequestDefaultParams, $aUserRequestCustomParams);
/** @var \UserRequest $oTicket */
$oTicket = $this->createObject('UserRequest', $aUserRequestParams);
'time_spent' => $iTimeSpent,
'caller_id' => $iCallerId,
'org_id' => ($iOrgId == 0 ? $this->getTestOrgId() : $iOrgId),
));
$this->debug("Created {$oTicket->Get('title')} ({$oTicket->Get('ref')})");
return $oTicket;

View File

@@ -56,7 +56,7 @@ class DeadLockInjection
if ($this->iRequestCount == $this->iFailAt) {
echo "Generating a FAKE DEADLOCK\n";
IssueLog::Trace("Generating a FAKE DEADLOCK", 'cmdbsource');
throw new MySQLException("FAKE DEADLOCK", [], new Exception("FAKE DEADLOCK", CMDBSource::MYSQL_ERRNO_DEADLOCK));
throw new MySQLException("FAKE DEADLOCK", [], new Exception("FAKE DEADLOCK", 1213));
}
}
}

View File

@@ -163,11 +163,7 @@ class DBSearchTest extends ItopDataTestCase
$i = 0;
foreach($aReq as $aParams)
{
$oObj = $this->CreateUserRequest($i, [
'time_spent' => $aParams[0],
'org_id' => $aOrgIds[$aParams[1]],
'caller_id' => $aPersonIds[$aParams[2]],
]);
$oObj = $this->CreateUserRequest($i, $aParams[0], $aOrgIds[$aParams[1]], $aPersonIds[$aParams[2]]);
self::assertNotNull($oObj);
$i++;
}

View File

@@ -8,13 +8,7 @@
namespace Combodo\iTop\Test\UnitTest\Module\AuthentLocal;
use AttributeDate;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Config;
use Dict;
use MetaModel;
use ormLinkSet;
use URP_UserProfile;
use UserLocal;
/**
@@ -40,23 +34,23 @@ class UserLocalTest extends ItopDataTestCase
*/
public function testValidatePassword($sPassword, $aValidatorNames, $aConfigValueMap, $bExpectedCheckStatus, $expectedCheckIssues = null, $sUserLanguage = null)
{
$configMock = $this->createMock(Config::class);
$configMock = $this->createMock(\Config::class);
$configMock
->method('GetModuleSetting')
->willReturnMap($aConfigValueMap);
restore_error_handler();
if (isset($sUserLanguage)) {
Dict::SetUserLanguage($sUserLanguage);
\Dict::SetUserLanguage($sUserLanguage);
}
/** @var UserLocal $oUserLocal */
$oUserLocal = MetaModel::NewObject(UserLocal::class, array('login' => 'john'));
/** @var ormLinkSet $oProfileSet */
$oUserLocal = \MetaModel::NewObject('UserLocal', array('login' => 'john'));
/** @var \ormLinkSet $oProfileSet */
$oProfileSet = $oUserLocal->Get('profile_list');
$oProfileSet->AddItem(
MetaModel::NewObject(URP_UserProfile::class, array('profileid' => 1))
\MetaModel::NewObject('URP_UserProfile', array('profileid' => 1))
);
$aValidatorCollection = array();
@@ -248,10 +242,9 @@ class UserLocalTest extends ItopDataTestCase
*/
public function testPasswordRenewal($sBefore, $sExpectedAfter)
{
$sDateFormat = AttributeDate::GetInternalFormat();
$oBefore = is_null($sBefore) ? null : date($sDateFormat, strtotime($sBefore));
$oNow = date($sDateFormat);
$oExpectedAfter = is_null($sExpectedAfter) ? null : date($sDateFormat, strtotime($sExpectedAfter));
$oBefore = is_null($sBefore) ? null : date(\AttributeDate::GetInternalFormat(), strtotime($sBefore));
$oNow = date(\AttributeDate::GetInternalFormat());
$oExpectedAfter = is_null($sExpectedAfter) ? null : date(\AttributeDate::GetInternalFormat(), strtotime($sExpectedAfter));
$aUserLocalValues = array('login' => 'john');
if (!is_null($oBefore))
@@ -260,14 +253,15 @@ class UserLocalTest extends ItopDataTestCase
}
/** @var UserLocal $oUserLocal */
$oUserLocal = MetaModel::NewObject(UserLocal::class, $aUserLocalValues);
/** @var ormLinkSet $oProfileSet */
$oUserLocal = \MetaModel::NewObject('UserLocal', $aUserLocalValues);
/** @var \ormLinkSet $oProfileSet */
$oProfileSet = $oUserLocal->Get('profile_list');
$oProfileSet->AddItem(
MetaModel::NewObject(URP_UserProfile::class, array('profileid' => 1))
\MetaModel::NewObject('URP_UserProfile', array('profileid' => 1))
);
$this->assertEquals($oBefore, $oUserLocal->Get('password_renewed_date'));
//INSERT
@@ -276,19 +270,17 @@ class UserLocalTest extends ItopDataTestCase
$this->assertEquals($oNow, $oUserLocal->Get('password_renewed_date'), 'INSERT sets the "password_renewed_date" to the current date');
//UPDATE password_renewed_date
$oUserLocal = MetaModel::GetObject(UserLocal::class, $oUserLocal->GetKey());
$oUserLocal->Set('password_renewed_date', $oBefore);
$oUserLocal->DBWrite();
$this->assertEquals($oBefore, $oUserLocal->Get('password_renewed_date'), 'UPDATE can target and change the "password_renewed_date"');
//UPDATE password
$oUserLocal = MetaModel::GetObject(UserLocal::class, $oUserLocal->GetKey());
$oUserLocal->Set('password', 'fooBar1???1');
$oUserLocal->DBWrite();
$this->assertEquals($oExpectedAfter, $oUserLocal->Get('password_renewed_date'), 'UPDATE "password" fields trigger automatic change of the "password_renewed_date" field');
//UPDATE both password & password_renewed_date
$oUserLocal = MetaModel::GetObject(UserLocal::class, $oUserLocal->GetKey());
$oUserLocal->Set('password', 'fooBar1???2');
$oUserLocal->Set('password_renewed_date', $oBefore);
$oUserLocal->DBWrite();
@@ -312,85 +304,5 @@ class UserLocalTest extends ItopDataTestCase
),
);
}
/**
* @dataProvider CanExpireFixProvider
*
*/
public function testCanExpireFix($sExpirationMode, $sBefore, bool $bRenewedDateTouched)
{
$oBefore = is_null($sBefore) ? null : date(AttributeDate::GetInternalFormat(), strtotime($sBefore));
$oNow = date(AttributeDate::GetInternalFormat());
$oExpectedAfter = $bRenewedDateTouched ? $oNow : $oBefore;
$aUserLocalValues = array('login' => 'john');
if (!is_null($oBefore))
{
$aUserLocalValues['password_renewed_date'] = $oBefore;
}
/** @var UserLocal $oUserLocal */
$oUserLocal = MetaModel::NewObject(UserLocal::class, $aUserLocalValues);
/** @var ormLinkSet $oProfileSet */
$oProfileSet = $oUserLocal->Get('profile_list');
$oProfileSet->AddItem(
MetaModel::NewObject(URP_UserProfile::class, array('profileid' => 1))
);
$this->assertEquals($oBefore, $oUserLocal->Get('password_renewed_date'));
//INSERT
$oUserLocal->Set('password', 'fooBar1???');
$oUserLocal->DBWrite();
$this->assertEquals($oNow, $oUserLocal->Get('password_renewed_date'), 'INSERT sets the "password_renewed_date" to the current date');
$oUserLocal = MetaModel::GetObject(UserLocal::class, $oUserLocal->GetKey());
$oUserLocal->Set('password_renewed_date', $oBefore);
$oUserLocal->DBWrite();
$this->assertEquals($oBefore, $oUserLocal->Get('password_renewed_date'), 'UPDATE can target and change the "password_renewed_date"');
//UPDATE password
$oUserLocal = MetaModel::GetObject(UserLocal::class, $oUserLocal->GetKey());
$oUserLocal->Set('expiration', $sExpirationMode);
$oUserLocal->DBWrite();
$this->assertEquals($oExpectedAfter, $oUserLocal->Get('password_renewed_date'), 'UPDATE "password" fields trigger automatic change of the "password_renewed_date" field');
}
public function CanExpireFixProvider()
{
return array(
'EXPIRE_CAN: nominal case' => array(
'sExpirationMode' => 'can_expire',
'oExpectedBefore' => null,
'bRenewedDateTouched' => true,
),
'EXPIRE_NEVER (default mode): nothing changed on UserLocal' => array(
'sExpirationMode' => 'never_expire',
'oExpectedBefore' => null,
'bRenewedDateTouched' => false,
),
'EXPIRE_FORCE: nominal case' => array(
'sExpirationMode' => 'force_expire',
'oExpectedBefore' => null,
'bRenewedDateTouched' => true,
),
'EXPIRE_ONE_TIME_PWD: nominal case' => array(
'sExpirationMode' => 'otp_expire',
'oExpectedBefore' => null,
'bRenewedDateTouched' => true,
),
'date initiated' => array(
'sExpirationMode' => 'can_expire',
'oBefore' => '-1 day',
'bRenewedDateTouched' => false,
),
'date initiated in the future' => array(
'sExpirationMode' => 'can_expire',
'oBefore' => '+1 day',
'bRenewedDateTouched' => false,
),
);
}
}

View File

@@ -4,8 +4,6 @@ namespace Combodo\iTop\Test\UnitTest\Webservices;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Exception;
use MetaModel;
use utils;
/**
@@ -21,13 +19,11 @@ class RestTest extends ItopDataTestCase
{
const USE_TRANSACTION = false;
const ENUM_JSONDATA_AS_STRING = 0;
const ENUM_JSONDATA_AS_FILE = 1;
const ENUM_JSONDATA_NONE = 2;
const MODE = [ 'JSONDATA_AS_STRING' => 0, 'JSONDATA_AS_FILE' => 1 , 'NO_JSONDATA' => 2 ];
private $sTmpFile = "";
/** @var int $iJsonDataMode */
private $iJsonDataMode = self::ENUM_JSONDATA_AS_STRING;
private $sJsonDataMode;
private $sUrl;
private $sLogin;
private $sPassword = "Iuytrez9876543ç_è-(";
@@ -46,10 +42,10 @@ class RestTest extends ItopDataTestCase
unlink($this->sTmpFile);
}
$this->sUrl = MetaModel::GetConfig()->Get('app_root_url');
$this->sUrl = \MetaModel::GetConfig()->Get('app_root_url');
$oRestProfile = MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'REST Services User'), true);
$oAdminProfile = MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'Administrator'), true);
$oRestProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'REST Services User'), true);
$oAdminProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'Administrator'), true);
if (is_object($oRestProfile) && is_object($oAdminProfile)) {
$oUser = $this->CreateUser($this->sLogin, $oRestProfile->GetKey(), $this->sPassword);
@@ -57,28 +53,6 @@ class RestTest extends ItopDataTestCase
}
}
public function testJSONPCallback()
{
$sCallbackName = 'fooCallback';
$sJsonData = <<<JSON
{
"operation": "list_operations"
}
JSON;
// Test regular JSON result
$sJSONResult = $this->CallRestApi($sJsonData);
// - Try to decode JSON to array to check if it is well-formed
$aJSONResultAsArray = json_decode($sJSONResult, true);
if (false === is_array($aJSONResultAsArray)) {
$this->fail('JSON result could not be decoded as array, it might be malformed');
}
// Test JSONP with callback by checking that it is the same as the regular JSON but within the JS callback
$sJSONPResult = $this->CallRestApi($sJsonData, $sCallbackName);
$this->assertEquals($sCallbackName.'('.$sJSONResult.')', $sJSONPResult, 'JSONP response callback does not match expected result');
}
/**
* @dataProvider BasicProvider
* @param int $iJsonDataMode
@@ -93,7 +67,7 @@ JSON;
$aJson = json_decode($sOuputJson, true);
$this->assertNotNull($aJson, "Cannot decode returned JSON : $sOuputJson");
if ($this->iJsonDataMode === static::ENUM_JSONDATA_NONE){
if ($this->iJsonDataMode === self::MODE['NO_JSONDATA']){
$this->assertStringContainsString("3", "".$aJson['code'], $sOuputJson);
$this->assertStringContainsString("Error: Missing parameter 'json_data'", "".$aJson['message'], $sOuputJson);
return;
@@ -147,7 +121,7 @@ JSON;
$sOuputJson = $this->CreateTicketViaApi($description);
$aJson = json_decode($sOuputJson, true);
if ($this->iJsonDataMode === static::ENUM_JSONDATA_NONE){
if ($this->iJsonDataMode === self::MODE['NO_JSONDATA']){
$this->assertStringContainsString("3", "".$aJson['code'], $sOuputJson);
$this->assertStringContainsString("Error: Missing parameter 'json_data'", "".$aJson['message'], $sOuputJson);
return;
@@ -186,7 +160,7 @@ JSON;
$sOuputJson = $this->CreateTicketViaApi($description);
$aJson = json_decode($sOuputJson, true);
if ($this->iJsonDataMode === static::ENUM_JSONDATA_NONE){
if ($this->iJsonDataMode === self::MODE['NO_JSONDATA']){
$this->assertStringContainsString("3", "".$aJson['code'], $sOuputJson);
$this->assertStringContainsString("Error: Missing parameter 'json_data'", "".$aJson['message'], $sOuputJson);
return;
@@ -224,9 +198,9 @@ JSON;
public function BasicProvider(){
return [
'call rest call' => [ 'sJsonDataMode' => static::ENUM_JSONDATA_AS_STRING],
'pass json_data as file' => [ 'sJsonDataMode' => static::ENUM_JSONDATA_AS_FILE],
'no json data' => [ 'sJsonDataMode' => static::ENUM_JSONDATA_NONE]
'call rest call' => [ 'sJsonDataMode' => self::MODE['JSONDATA_AS_STRING']],
'pass json_data as file' => [ 'sJsonDataMode' => self::MODE['JSONDATA_AS_FILE']],
'no json data' => [ 'sJsonDataMode' => self::MODE['NO_JSONDATA']]
];
}
@@ -272,7 +246,7 @@ JSON;
}
private function CallRestApi(string $sJsonDataContent, string $sCallbackName = null){
private function CallRestApi($sJsonDataContent){
$ch = curl_init();
$aPostFields = [
'version' => '1.3',
@@ -280,20 +254,16 @@ JSON;
'auth_pwd' => $this->sPassword,
];
if ($this->iJsonDataMode === static::ENUM_JSONDATA_AS_STRING) {
if ($this->iJsonDataMode === self::MODE['JSONDATA_AS_STRING']){
$this->sTmpFile = tempnam(sys_get_temp_dir(), 'jsondata_');
file_put_contents($this->sTmpFile, $sJsonDataContent);
$oCurlFile = curl_file_create($this->sTmpFile);
$aPostFields['json_data'] = $oCurlFile;
} else if ($this->iJsonDataMode === static::ENUM_JSONDATA_AS_FILE) {
}else if ($this->iJsonDataMode === self::MODE['JSONDATA_AS_FILE']){
$aPostFields['json_data'] = $sJsonDataContent;
}
if (utils::IsNotNullOrEmptyString($sCallbackName)) {
$aPostFields['callback'] = $sCallbackName;
}
curl_setopt($ch, CURLOPT_URL, "$this->sUrl/webservices/rest.php");
curl_setopt($ch, CURLOPT_POST, 1);// set post data to true
curl_setopt($ch, CURLOPT_POSTFIELDS, $aPostFields);