Compare commits

...

43 Commits

Author SHA1 Message Date
odain
433a6f1c3c N°6171 - Password Expiration: can expire mode has no effect on user who have never changed their password - finalize work 2023-05-04 16:03:27 +02:00
odain
075e2ec94a N°6171 - Password Expiration: can expire mode has no effect on user who have never changed their password - fix test 2023-05-04 15:08:40 +02:00
odain
e5c1a01c69 N°6171 - Password Expiration: can expire mode has no effect on user who have never changed their password - core change 2023-05-04 14:38:28 +02:00
odain
692a8b978f N°6171 - Password Expiration: can expire mode has no effect on user who have never changed their password - test first 2023-05-04 13:52:45 +02:00
odain
a0ecd59568 N°6171 - Password Expiration: can expire mode has no effect on user who have never changed their password - test first 2023-05-04 11:51:56 +02:00
Stephen Abello
eebc61385d N°6009 - Fix restore backup button not working when JS dependencies are present 2023-05-02 09:24:54 +02:00
Pierre Goiffon
3c15186685 Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-04-26 17:12:08 +02:00
Pierre Goiffon
fa038ded3d N°6254 ItopDataTestCase::CreateUserRequest : fix new argument default value
Was creating error Too few arguments passed
2023-04-26 16:42:27 +02:00
Pierre Goiffon
e7ea1b831c N°6254 ItopDataTestCase::CreateUserRequest : now pass fields values as array
More versatile way of doing things !
2023-04-26 16:22:26 +02:00
Molkobain
f15ac75f8f Merge remote-tracking branch 'origin/support/3.0.3' into support/3.0 2023-04-26 11:25:04 +02:00
Molkobain
e28dbebbd5 Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	application/displayblock.class.inc.php
#	dictionaries/en.dictionary.itop.ui.php
#	dictionaries/fr.dictionary.itop.ui.php
2023-04-25 21:56:42 +02:00
Molkobain
4aff65f98b N°6217 - Add accessiblity meta data for title on "Power menu" 2023-04-25 21:51:32 +02:00
Molkobain
8aba578cfa Merge remote-tracking branch 'origin/support/3.0.3' into support/3.0 2023-04-25 21:05:42 +02:00
acognet
7e7f8577e8 Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	application/displayblock.class.inc.php
2023-04-25 15:14:22 +02:00
acognet
3c94974d9d N°541 - Dashlets: Improve readability when to much labels (pie chart) or too long labels (bar chart) 2023-04-25 12:09:11 +02:00
acognet
d6e5069dd5 N°541 - Dashlets: Improve readability when to much labels (pie chart) or too long labels (bar chart) 2023-04-24 14:26:33 +02:00
Stephen Abello
f839638e0b N°6188 - Creation cancellation in pop-up while in edition of parent object wrongfully returns to object list 2023-04-21 16:12:37 +02:00
Pierre Goiffon
740ff8c649 💡 DeprecatedCallsLog phpdoc 2023-04-21 14:58:00 +02:00
Molkobain
cfe227e0c7 N°6216 - Fix line-height being too big in the attachments table 2023-04-20 15:28:20 +02:00
Molkobain
ed79c8f099 Merge remote-tracking branch 'origin/support/3.0.3' into support/3.0 2023-04-20 12:53:34 +02:00
Molkobain
4560f751d1 Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	application/displayblock.class.inc.php
#	application/itopwebpage.class.inc.php
#	dictionaries/en.dictionary.itop.ui.php
#	dictionaries/fr.dictionary.itop.ui.php
2023-04-20 12:53:12 +02:00
Molkobain
fbd72b2783 N°6217 - Add accessiblity meta data for title on "Power menu" 2023-04-20 11:03:43 +02:00
Molkobain
778118cfb4 N°6204 - REST API: Add unit test for callback parameter 2023-04-18 22:34:11 +02:00
Molkobain
096ed9a63a N°6204 - Improve REST API unit test readability 2023-04-18 22:14:39 +02:00
Molkobain
06eb79d4f4 N°6204 - Fix REST/JSON API crash when using JSON-P and iBackofficeDictXXX interfaces 2023-04-18 14:42:36 +02:00
Anne-Catherine
4e95ca3c7b N°541 - Dashlets: Improve readability when to much labels (pie chart) or too long labels (bar chart) (#452)
* N°541 - Dashlets: Improve readability when to much labels (pie chart) or too long labels (bar chart)
2023-04-13 11:23:20 +02:00
Pierre Goiffon
4c626d0782 Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	core/log.class.inc.php
2023-04-12 10:22:34 +02:00
Pierre Goiffon
1114ed9562 N°6099 DeadLockLog : improve documentation and use existing constants (#441) 2023-04-12 10:21:34 +02:00
Pierre Goiffon
1ddfaf0b61 N°6100 ObjectFormManager::OnSubmit : better log for DBWrite exceptions (#353)
Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2023-04-11 18:09:45 +02:00
Pierre Goiffon
c6fb03547f Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-04-11 17:55:26 +02:00
Pierre Goiffon
34368fe795 N°6173 \HTMLSanitizer::Sanitize : Fix handling only svg_sanitizer (#450) 2023-04-11 17:52:41 +02:00
Pierre Goiffon
db46298cb8 Merge branch 'support/3.0.3' into support/3.0 2023-04-11 09:19:56 +02:00
Eric Espie
424e7b37d7 Merge branch 'support/3.0.3' into support/3.0 2023-04-07 09:44:38 +02:00
Eric Espie
8ffddeff01 N°6085 - UNION is not supported in UserRightsProfile::GetSelectFilter 2023-04-05 17:42:26 +02:00
Eric Espie
21d37fb237 N°6085 - UNION is not supported in UserRightsProfile::GetSelectFilter 2023-04-05 17:07:23 +02:00
Molkobain
fca4006811 N°6085 - UNION is not supported in UserRightsProfile::GetSelectFilter 2023-04-05 15:58:31 +02:00
Molkobain
c3b00939dd N°6140 - Add HTML metadata on custom fields to be aligned with regular fields 2023-03-31 17:58:09 +02:00
Molkobain
75df33f606 N°6139 - Add HTML metadata on activity panel to be aligned with regular fields 2023-03-30 18:39:09 +02:00
Molkobain
78d8829d65 N°6131 - Improve robustness of tooltips helper when no DOM element passed to CombodoTooltip::InitTooltipFromMarkup() 2023-03-27 18:04:01 +02:00
Pierre Goiffon
307edd3f7a Fix AttributeDefinitionTest parse error in PHP 7.2 2023-03-21 09:03:54 +01:00
Molkobain
6bf906a72f Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	js/dashboard.js
2023-03-17 15:44:23 +01:00
Molkobain
0f016d7511 N°6112 - Dashboard: Improve robustness by trimming dashlet ID returned by server 2023-03-17 15:37:57 +01:00
Pierre Goiffon
d782987f50 README : fix requirements link 2023-03-14 17:08:55 +01:00
28 changed files with 381 additions and 100 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]
- [Software requirements][4]
- [iTop 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:upgrading_itop
[4]: https://www.itophub.io/wiki/page?id=latest:install:requirements
[5]: https://www.itophub.io/wiki
[6]: https://store.itophub.io/en_US/

View File

@@ -3054,7 +3054,16 @@ 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();
$oPage->add_ready_script("$('#form_{$this->m_iFormId} button.cancel').on('click', function() { BackToDetails('$sClass', $iKey, '$sDefaultUrl', $sJSToken)} );");
$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);
$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><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 aria-label="'.Dict::S('UI:Menu:Toolkit').'"><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,6 +1611,7 @@ JS
$iTotalCount = 0;
$aURLs = array();
foreach ($aRes as $iRow => $aRow) {
$sValue = $aRow['grouped_by_1'];
$sHtmlValue = $oGroupByExp->MakeValueLabel($this->m_oFilter, $sValue, $sValue);
@@ -1621,6 +1622,7 @@ JS
'value' => (float)$aRow[$sFctVar],
);
// Build the search for this subset
$oSubsetSearch = $this->m_oFilter->DeepClone();
$oCondition = new BinaryExpression($oGroupByExp, '=', new ScalarExpression($sValue));
@@ -1637,9 +1639,13 @@ JS
switch ($sChartType) {
case 'bars':
$aNames = array();
$iMaxNbCharsInLabel = 0;
$aNames = [];
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);
@@ -1647,21 +1653,31 @@ JS
$oBlock->sId = $sId;
$oBlock->sJSURLs = $sJSURLs;
$oBlock->sURLForRefresh = str_replace("'", "\'", $sUrl);
$oBlock->iMaxNbCharsInLabel = $iMaxNbCharsInLabel;
break;
case 'pie':
$aColumns = array();
$aNames = array();
$aColumns = [];
$aNames = [];
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,7 +211,23 @@ class UILinksWidgetDirect
$oObj = DBObject::MakeDefaultInstance($sRealClass);
$aPrefillParam = array('source_obj' => $oSourceObj);
$oObj->PrefillForm('creation_from_editinplace', $aPrefillParam);
cmdbAbstractObject::DisplayCreationForm($oPage, $sRealClass, $oObj, array(), array('formPrefix' => $this->sInputid, 'noRelations' => true, 'fieldsFlags' => $aFieldFlags));
$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);
}
else
{

View File

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

View File

@@ -961,7 +961,9 @@ class ToolsLog extends LogAPI
/**
* @see \CMDBSource::LogDeadLock()
* @since 2.7.1
* @since 2.7.1 PR #139
*
* @link https://dev.mysql.com/doc/refman/5.7/en/innodb-deadlocks.html
*/
class DeadLockLog extends LogAPI
{
@@ -986,10 +988,10 @@ class DeadLockLog extends LogAPI
{
switch ($iMysqlErrorNo)
{
case 1205:
case CMDBSource::MYSQL_ERRNO_WAIT_TIMEOUT:
return self::CHANNEL_WAIT_TIMEOUT;
break;
case 1213:
case CMDBSource::MYSQL_ERRNO_DEADLOCK:
return self::CHANNEL_DEADLOCK_FOUND;
break;
default:
@@ -1019,7 +1021,14 @@ class DeadLockLog extends LogAPI
/**
* @since 3.0.0 N°3731
* 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
*/
class DeprecatedCallsLog extends LogAPI
{

View File

@@ -6,10 +6,6 @@
$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;
@@ -33,10 +29,7 @@ $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 {
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;
vertical-align: middle;
}
.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,19 +197,39 @@ 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())
{
@@ -294,7 +314,7 @@ class UserLocal extends UserInternal
{
$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,6 +137,9 @@ 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())
@@ -149,12 +152,7 @@ try
$sTokenFile = APPROOT.'/data/restore.'.$sToken.'.tok';
file_put_contents($sTokenFile, $sFile);
$oPage->add_ready_script(
<<<JS
$("#restore_token").val('$sToken');
JS
);
$oPage->SetData(['token' => $sToken]);
$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
$('#backup_errors').append(data);
$('#restore_token').val(data.token);
var oParams = {};
oParams.operation = 'restore_exec';

View File

@@ -40,6 +40,7 @@ use Dict;
use DOMDocument;
use DOMXPath;
use Exception;
use ExceptionLog;
use InlineImage;
use IssueLog;
use MetaModel;
@@ -1142,6 +1143,7 @@ class ObjectFormManager extends FormManager
$sObjectClass = get_class($this->oObject);
$bExceptionLogged = false;
try {
// modification flags
$bIsNew = $this->oObject->IsNew();
@@ -1163,6 +1165,14 @@ 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'));
}
@@ -1222,11 +1232,12 @@ class ObjectFormManager extends FormManager
}
}
}
catch (Exception $e)
{
catch (Exception $e) {
$aData['valid'] = false;
$aData['messages']['error'] += array('_main' => array($e->getMessage()));
IssueLog::Error(__METHOD__.' at line '.__LINE__.' : '.$e->getMessage());
if (false === $bExceptionLogged) {
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);
me.add_dashlet_prepare(options, data.trim());
});
},
add_dashlet_ajax: function (options, sDashletId) {

View File

@@ -794,8 +794,12 @@ const CombodoTooltip = {
InitTooltipFromMarkup: function (oElem, bForce = false) {
const oOptions = {};
// First, check if the tooltip isn't already instantiated
if ((oElem.attr('data-tooltip-instantiated') === 'true') && (bForce === false)) {
// 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)) {
return false;
}
else if((oElem.attr('data-tooltip-instantiated') === 'true') && (bForce === true) && (oElem[0]._tippy !== undefined)){

View File

@@ -59,8 +59,43 @@ class ConsoleSimpleFieldRenderer extends FieldRenderer
else
{
$oBlock = FieldUIBlockFactory::MakeStandard($this->oField->GetLabel());
$oBlock->AddDataAttribute("input-id",$this->oField->GetGlobalId());
$oBlock->AddDataAttribute("input-type",$sFieldClass);
$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;
}
}
switch ($sFieldClass)
{
case 'Combodo\\iTop\\Form\\Field\\DateTimeField':

View File

@@ -5,6 +5,7 @@
*/
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;
@@ -105,6 +106,15 @@ 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,6 +599,18 @@ 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,7 +28,9 @@ class BlockChartAjaxBars extends UIBlock
public $sId;
/** @var string */
public $sJSURLs;
/** @var string */
public $sURLForRefresh;
/** @var int */
public $iMaxNbCharsInLabel;
}

View File

@@ -28,6 +28,8 @@ 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') {
if ($this->sContentType != 'text/plain' && $this->sContentType != 'application/json' && $this->sContentType != 'application/javascript') {
/** @var \iBackofficeDictEntriesExtension $oExtensionInstance */
foreach (MetaModel::EnumPlugins('iBackofficeDictEntriesExtension') as $oExtensionInstance) {
foreach ($oExtensionInstance->GetDictEntries() as $sDictEntry) {

View File

@@ -1,5 +1,11 @@
{# @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,6 +1,12 @@
{# @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: {
@@ -9,7 +15,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
},
@@ -19,17 +25,19 @@ 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,6 +6,9 @@
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,25 +294,31 @@ class ItopDataTestCase extends ItopTestCase
* Create a UserRequest in database
*
* @param int $iNum
* @param int $iTimeSpent
* @param int $iOrgId
* @param int $iCallerId
* @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)
*
* @return \UserRequest
* @throws Exception
* @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
*/
protected function CreateUserRequest($iNum, $iTimeSpent = 0, $iOrgId = 0, $iCallerId = 0)
{
/** @var \UserRequest $oTicket */
$oTicket = $this->createObject('UserRequest', array(
protected function CreateUserRequest($iNum, $aUserRequestCustomParams = []) {
$aUserRequestDefaultParams = [
'ref' => 'Ticket_'.$iNum,
'title' => 'BUG 1161_'.$iNum,
//'request_type' => 'incident',
'description' => 'Add aggregate functions',
'time_spent' => $iTimeSpent,
'caller_id' => $iCallerId,
'org_id' => ($iOrgId == 0 ? $this->getTestOrgId() : $iOrgId),
));
'org_id' => $this->getTestOrgId(),
];
$aUserRequestParams = array_merge($aUserRequestDefaultParams, $aUserRequestCustomParams);
/** @var \UserRequest $oTicket */
$oTicket = $this->createObject('UserRequest', $aUserRequestParams);
$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", 1213));
throw new MySQLException("FAKE DEADLOCK", [], new Exception("FAKE DEADLOCK", CMDBSource::MYSQL_ERRNO_DEADLOCK));
}
}
}

View File

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

View File

@@ -8,7 +8,13 @@
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;
/**
@@ -34,23 +40,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', array('login' => 'john'));
/** @var \ormLinkSet $oProfileSet */
$oUserLocal = MetaModel::NewObject(UserLocal::class, array('login' => 'john'));
/** @var ormLinkSet $oProfileSet */
$oProfileSet = $oUserLocal->Get('profile_list');
$oProfileSet->AddItem(
\MetaModel::NewObject('URP_UserProfile', array('profileid' => 1))
MetaModel::NewObject(URP_UserProfile::class, array('profileid' => 1))
);
$aValidatorCollection = array();
@@ -242,9 +248,10 @@ class UserLocalTest extends ItopDataTestCase
*/
public function testPasswordRenewal($sBefore, $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));
$sDateFormat = AttributeDate::GetInternalFormat();
$oBefore = is_null($sBefore) ? null : date($sDateFormat, strtotime($sBefore));
$oNow = date($sDateFormat);
$oExpectedAfter = is_null($sExpectedAfter) ? null : date($sDateFormat, strtotime($sExpectedAfter));
$aUserLocalValues = array('login' => 'john');
if (!is_null($oBefore))
@@ -253,15 +260,14 @@ class UserLocalTest extends ItopDataTestCase
}
/** @var UserLocal $oUserLocal */
$oUserLocal = \MetaModel::NewObject('UserLocal', $aUserLocalValues);
/** @var \ormLinkSet $oProfileSet */
$oUserLocal = MetaModel::NewObject(UserLocal::class, $aUserLocalValues);
/** @var ormLinkSet $oProfileSet */
$oProfileSet = $oUserLocal->Get('profile_list');
$oProfileSet->AddItem(
\MetaModel::NewObject('URP_UserProfile', array('profileid' => 1))
MetaModel::NewObject(URP_UserProfile::class, array('profileid' => 1))
);
$this->assertEquals($oBefore, $oUserLocal->Get('password_renewed_date'));
//INSERT
@@ -270,17 +276,19 @@ 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();
@@ -304,5 +312,85 @@ 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,6 +4,8 @@ namespace Combodo\iTop\Test\UnitTest\Webservices;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Exception;
use MetaModel;
use utils;
/**
@@ -19,11 +21,13 @@ class RestTest extends ItopDataTestCase
{
const USE_TRANSACTION = false;
const MODE = [ 'JSONDATA_AS_STRING' => 0, 'JSONDATA_AS_FILE' => 1 , 'NO_JSONDATA' => 2 ];
const ENUM_JSONDATA_AS_STRING = 0;
const ENUM_JSONDATA_AS_FILE = 1;
const ENUM_JSONDATA_NONE = 2;
private $sTmpFile = "";
/** @var int $iJsonDataMode */
private $sJsonDataMode;
private $iJsonDataMode = self::ENUM_JSONDATA_AS_STRING;
private $sUrl;
private $sLogin;
private $sPassword = "Iuytrez9876543ç_è-(";
@@ -42,10 +46,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);
@@ -53,6 +57,28 @@ 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
@@ -67,7 +93,7 @@ class RestTest extends ItopDataTestCase
$aJson = json_decode($sOuputJson, true);
$this->assertNotNull($aJson, "Cannot decode returned JSON : $sOuputJson");
if ($this->iJsonDataMode === self::MODE['NO_JSONDATA']){
if ($this->iJsonDataMode === static::ENUM_JSONDATA_NONE){
$this->assertStringContainsString("3", "".$aJson['code'], $sOuputJson);
$this->assertStringContainsString("Error: Missing parameter 'json_data'", "".$aJson['message'], $sOuputJson);
return;
@@ -121,7 +147,7 @@ JSON;
$sOuputJson = $this->CreateTicketViaApi($description);
$aJson = json_decode($sOuputJson, true);
if ($this->iJsonDataMode === self::MODE['NO_JSONDATA']){
if ($this->iJsonDataMode === static::ENUM_JSONDATA_NONE){
$this->assertStringContainsString("3", "".$aJson['code'], $sOuputJson);
$this->assertStringContainsString("Error: Missing parameter 'json_data'", "".$aJson['message'], $sOuputJson);
return;
@@ -160,7 +186,7 @@ JSON;
$sOuputJson = $this->CreateTicketViaApi($description);
$aJson = json_decode($sOuputJson, true);
if ($this->iJsonDataMode === self::MODE['NO_JSONDATA']){
if ($this->iJsonDataMode === static::ENUM_JSONDATA_NONE){
$this->assertStringContainsString("3", "".$aJson['code'], $sOuputJson);
$this->assertStringContainsString("Error: Missing parameter 'json_data'", "".$aJson['message'], $sOuputJson);
return;
@@ -198,9 +224,9 @@ JSON;
public function BasicProvider(){
return [
'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']]
'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]
];
}
@@ -246,7 +272,7 @@ JSON;
}
private function CallRestApi($sJsonDataContent){
private function CallRestApi(string $sJsonDataContent, string $sCallbackName = null){
$ch = curl_init();
$aPostFields = [
'version' => '1.3',
@@ -254,16 +280,20 @@ JSON;
'auth_pwd' => $this->sPassword,
];
if ($this->iJsonDataMode === self::MODE['JSONDATA_AS_STRING']){
if ($this->iJsonDataMode === static::ENUM_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 === self::MODE['JSONDATA_AS_FILE']){
} else if ($this->iJsonDataMode === static::ENUM_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);