diff --git a/.editorconfig b/.editorconfig
index 0d75f75f2..46ae2703a 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -6,14 +6,14 @@ end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = false
-max_line_length = 140
+max_line_length = 300
tab_width = 4
ij_continuation_indent_size = 8
ij_formatter_off_tag = @formatter:off
ij_formatter_on_tag = @formatter:on
ij_formatter_tags_enabled = false
ij_smart_tabs = false
-ij_visual_guides = 80,120
+ij_visual_guides = 300
ij_wrap_on_typing = true
[*.css]
diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php
index 7445a5064..805092902 100644
--- a/application/cmdbabstract.class.inc.php
+++ b/application/cmdbabstract.class.inc.php
@@ -3253,8 +3253,7 @@ HTML;
case 'text':
case 'html':
$data = $oDoc->GetData();
- switch ($oDoc->GetMimeType())
- {
+ switch ($oDoc->GetMimeType()) {
case 'text/xml':
$oPage->add("\n");
break;
diff --git a/application/utils.inc.php b/application/utils.inc.php
index e2d0de654..55030cb3c 100644
--- a/application/utils.inc.php
+++ b/application/utils.inc.php
@@ -2581,4 +2581,12 @@ HTML;
return array_merge($aDefaultConf, $aRichTextConfig);
}
+
+ /**
+ * @return bool : indicate whether we run under a windows environnement or not
+ * @since 2.7.4 : N°3412
+ */
+ public static function IsWindowsEnvironment(){
+ return (substr(PHP_OS,0,3) === 'WIN');
+ }
}
diff --git a/core/cmdbsource.class.inc.php b/core/cmdbsource.class.inc.php
index 16871a549..89f6cbaa8 100644
--- a/core/cmdbsource.class.inc.php
+++ b/core/cmdbsource.class.inc.php
@@ -1082,7 +1082,8 @@ class CMDBSource
}
/**
- * There may have some differences between DB : for example in MySQL 5.7 we have "INT", while in MariaDB >= 10.2 you get "int DEFAULT 'NULL'"
+ * There may have some differences between DB : for example in MySQL 5.7 we have "INT", while in MariaDB >= 10.2 you get "int DEFAULT
+ * 'NULL'"
*
* We still do a case sensitive comparison for enum values !
*
@@ -1093,6 +1094,7 @@ class CMDBSource
* @param string $sDbFieldType
*
* @return bool true if same type and options (case sensitive comparison only for type options), false otherwise
+ * @throws \CoreException
* @since 2.7.0 N°2490
*/
public static function IsSameFieldTypes($sItopGeneratedFieldType, $sDbFieldType)
@@ -1146,18 +1148,55 @@ class CMDBSource
* 1. data type : for example 'VARCHAR'
* 2. type value : for example '255'
* 3. other options : for example ' CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT 0'
+ * @throws \CoreException
*/
private static function GetFieldDataTypeAndOptions($sCompleteFieldType)
{
preg_match('/^([a-zA-Z]+)(\(([^\)]+)\))?( .+)?/', $sCompleteFieldType, $aMatches);
$sDataType = isset($aMatches[1]) ? $aMatches[1] : '';
+
+ if (strcasecmp($sDataType, 'ENUM') === 0){
+ return self::GetEnumOptions($sDataType, $sCompleteFieldType);
+ }
+
$sTypeOptions = isset($aMatches[2]) ? $aMatches[3] : '';
$sOtherOptions = isset($aMatches[4]) ? $aMatches[4] : '';
return array($sDataType, $sTypeOptions, $sOtherOptions);
}
+ /**
+ * @since 2.7.4 N°3065
+ * Handle ENUM options
+ *
+ * @param $sDataType
+ * @param $sCompleteFieldType
+ * Example: ENUM('CSP A','CSP (aaaa) M','NA','OEM(ROC)','OPEN(VL)','RETAIL (Boite)') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
+ *
+ * @return string[] consisting of 3 items :
+ * 1. data type : ENUM or enum here
+ * 2. type value : in-between EUM parenthesis
+ * 3. other options : for example ' CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT 0'
+ * @throws \CoreException
+ */
+ private static function GetEnumOptions($sDataType, $sCompleteFieldType)
+ {
+ $iFirstOpeningParenthesis = strpos($sCompleteFieldType, '(');
+ $iLastEndingParenthesis = strrpos($sCompleteFieldType, ')');
+
+ if ($iFirstOpeningParenthesis === false || $iLastEndingParenthesis === false ){
+ //should never happen as GetFieldDataTypeAndOptions regexp matched.
+ //except if regexp is modiied/broken somehow one day...
+ throw new CoreException("GetEnumOptions issue with $sDataType parsing : " . $sCompleteFieldType);
+ }
+
+ $sTypeOptions = substr($sCompleteFieldType, $iFirstOpeningParenthesis + 1, $iLastEndingParenthesis - 1);
+ $sOtherOptions = substr($sCompleteFieldType, $iLastEndingParenthesis + 1);
+
+ return array($sDataType, $sTypeOptions, $sOtherOptions);
+ }
+
/**
* @param string $sTable
* @param string $sField
diff --git a/core/metamodel.class.php b/core/metamodel.class.php
index a89ff054c..384d53a4b 100644
--- a/core/metamodel.class.php
+++ b/core/metamodel.class.php
@@ -5654,15 +5654,10 @@ abstract class MetaModel
// The field already exists, does it have the relevant properties?
//
- $bToBeChanged = false;
$sActualFieldSpec = CMDBSource::GetFieldSpec($sTable, $sField);
if (!CMDBSource::IsSameFieldTypes($sDBFieldSpec, $sActualFieldSpec))
{
- $bToBeChanged = true;
$aErrors[$sClass][$sAttCode][] = "field '$sField' in table '$sTable' has a wrong type: found $sActualFieldSpec while expecting $sDBFieldSpec";
- }
- if ($bToBeChanged)
- {
$aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` CHANGE `$sField` $sFieldDefinition";
$aAlterTableItems[$sTable][$sField] = "CHANGE `$sField` $sFieldDefinition";
}
diff --git a/datamodels/2.x/itop-portal-base/portal/public/js/portal_form_handler.js b/datamodels/2.x/itop-portal-base/portal/public/js/portal_form_handler.js
index 6fe572daa..e9c3d1eee 100644
--- a/datamodels/2.x/itop-portal-base/portal/public/js/portal_form_handler.js
+++ b/datamodels/2.x/itop-portal-base/portal/public/js/portal_form_handler.js
@@ -33,6 +33,7 @@ $(function()
category: 'redirect',
url: null,
modal: false,
+ timeout_duration: 400,
},
cancel_rule: {
category: 'close',
@@ -56,7 +57,7 @@ $(function()
this.options.submit_rule.url = this.options.submit_url;
if((this.options.cancel_url !== null) && (this.options.cancel_url !== ''))
this.options.cancel_rule.url = this.options.cancel_url;
-
+
this._super();
},
@@ -143,12 +144,14 @@ $(function()
// Determine where we go in case validation is successful
var sRuleType = me.options.submit_rule.category;
var bRedirectInModal = me.options.submit_rule.modal;
+ var iRedirectTimeout = me.options.submit_rule.timeout_duration;
var sRedirectUrl = me.options.submit_rule.url;
// - The validation might want us to be redirect elsewhere
if(oValidation.valid)
{
// Checking if we have to redirect to another page
// Typically this happens when applying a stimulus, we redirect to the transition form
+ // This code let the ajax response override the initial parameters
if(oValidation.redirection !== undefined)
{
var oRedirection = oValidation.redirection;
@@ -160,6 +163,10 @@ $(function()
{
sRedirectUrl = oRedirection.url;
}
+ if(oRedirection.timeout_duration !== undefined)
+ {
+ iRedirectTimeout = oRedirection.timeout_duration;
+ }
sRuleType = 'redirect';
}
}
@@ -241,7 +248,7 @@ $(function()
// Checking if we have to redirect to another page
if(sRuleType === 'redirect')
{
- me._applyRedirectRule(sRedirectUrl, bRedirectInModal);
+ me._applyRedirectRule(sRedirectUrl, bRedirectInModal, iRedirectTimeout);
}
// Close rule only needs to be applied to non modal forms (modal is always closed on submit)
else if(sRuleType === 'close')
@@ -360,10 +367,13 @@ $(function()
{
$('#page_overlay').fadeOut(200);
},
- _applyRedirectRule: function(sRedirectUrl, bRedirectInModal)
+ _applyRedirectRule: function(sRedirectUrl, bRedirectInModal, iRedirectTimeout)
{
var me = this;
+ //optional argument
+ iRedirectTimeout = (typeof iRedirectTimeout !== 'undefined' && iRedirectTimeout != null ) ? iRedirectTimeout : 400;
+
// Always close current modal
if(this.options.is_modal)
{
@@ -391,8 +401,8 @@ $(function()
// Showing loader while redirecting, otherwise user tend to click somewhere in the page.
// Note: We use a timeout because .always() is called right after here and will hide the loader
setTimeout(function(){ me._disableFormBeforeLoading(); }, 50);
- // Redirecting after a few ms so the user can see what happend
- setTimeout(function() { location.href = sRedirectUrl; }, 400);
+ // Redirecting after a few ms so the user can see what happened
+ setTimeout(function() { location.href = sRedirectUrl; }, iRedirectTimeout);
}
}
},
diff --git a/datamodels/2.x/itop-portal-base/portal/src/Controller/UserProfileBrickController.php b/datamodels/2.x/itop-portal-base/portal/src/Controller/UserProfileBrickController.php
index 74f0bfb63..b37eb98fa 100644
--- a/datamodels/2.x/itop-portal-base/portal/src/Controller/UserProfileBrickController.php
+++ b/datamodels/2.x/itop-portal-base/portal/src/Controller/UserProfileBrickController.php
@@ -211,6 +211,7 @@ class UserProfileBrickController extends BrickController
{
$aFormData['validation']['redirection'] = array(
'url' => $oUrlGenerator->generate('p_user_profile_brick'),
+ 'timeout_duration' => 1000, //since there are several ajax request, we use a longer timeout in hope that they will all be finished in time. A promise would have been more reliable, but since this change is made in a minor version, this approach is less error prone.
);
}
}
diff --git a/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php b/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php
index b7df3bbd2..34f473ffa 100644
--- a/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php
+++ b/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php
@@ -1061,6 +1061,28 @@ class ObjectFormManager extends FormManager
$this->CancelAttachments();
}
+ /**
+ * @inheritDoc
+ */
+ public function CheckTransaction($aData)
+ {
+ $isTransactionValid = \utils::IsTransactionValid($this->oForm->GetTransactionId(), false); //The transaction token is kept in order to preserve BC with ajax forms (the second call would fail if the token is deleted). (The GC will take care of cleaning the token for us later on)
+ if (!$isTransactionValid) {
+ if ($this->oObject->IsNew()) {
+ $sError = Dict::S('UI:Error:ObjectAlreadyCreated');
+ } else {
+ $sError = Dict::S('UI:Error:ObjectAlreadyUpdated');
+ }
+
+ $aData['messages']['error'] += [
+ '_main' => [$sError]
+ ];
+ $aData['valid'] = false;
+ }
+
+ return $aData;
+ }
+
/**
* Validates the form and returns an array with the validation status and the messages.
* If the form is valid, creates/updates the object.
@@ -1083,124 +1105,116 @@ class ObjectFormManager extends FormManager
*/
public function OnSubmit($aArgs = null)
{
- $aData = array(
- 'valid' => true,
- 'messages' => array(
- 'success' => array(),
- 'warnings' => array(), // Not used as of today, just to show that the structure is ready for change like this.
- 'error' => array(),
- ),
- );
+ $aData = parent::OnSubmit($aArgs);
+
+ if (! $aData['valid']) {
+ return $aData;
+ }
// Update object and form
$this->OnUpdate($aArgs);
// Check if form valid
- if ($this->oForm->Validate())
- {
- // The try catch is essentially to start a MySQL transaction in order to ensure that all or none objects are persisted when creating an object with links
- try
- {
- $sObjectClass = get_class($this->oObject);
-
- // Starting transaction
- CMDBSource::Query('START TRANSACTION');
- // Forcing allowed writing on the object if necessary. This is used in some particular cases.
- $bAllowWrite = ($sObjectClass === 'Person' && $this->oObject->GetKey() == UserRights::GetContactId());
- if ($bAllowWrite)
- {
- $this->oObject->AllowWrite(true);
- }
-
- // Writing object to DB
- $bActivateTriggers = (!$this->oObject->IsNew() && $this->oObject->IsModified());
- $bWasModified = $this->oObject->IsModified();
- try
- {
- $this->oObject->DBWrite();
- }
- catch (CoreCannotSaveObjectException $e)
- {
- throw new Exception($e->getHtmlMessage());
- }
- // Finalizing images link to object, otherwise it will be cleaned by the GC
- InlineImage::FinalizeInlineImages($this->oObject);
- // Finalizing attachments link to object
- // TODO : This has to be refactored when the function from itop-attachments has been migrated into the core
- if (isset($aArgs['attachmentIds']))
- {
- $this->FinalizeAttachments($aArgs['attachmentIds']);
- }
-
- // Ending transaction with a commit as everything was fine
- CMDBSource::Query('COMMIT');
-
- // Checking if we have to apply a stimulus
- if (isset($aArgs['applyStimulus']))
- {
- $this->oObject->ApplyStimulus($aArgs['applyStimulus']['code']);
- }
- // Activating triggers only on update
- if ($bActivateTriggers)
- {
- $sTriggersQuery = $this->oContainer->getParameter('combodo.portal.instance.conf')['properties']['triggers_query'];
- if ($sTriggersQuery !== null)
- {
- $aParentClasses = MetaModel::EnumParentClasses($sObjectClass, ENUM_PARENT_CLASSES_ALL);
- $oTriggerSet = new DBObjectSet(DBObjectSearch::FromOQL($sTriggersQuery), array(),
- array('parent_classes' => $aParentClasses));
- /** @var \Trigger $oTrigger */
- while ($oTrigger = $oTriggerSet->Fetch())
- {
- try
- {
- $oTrigger->DoActivate($this->oObject->ToArgs('this'));
- }
- catch(Exception $e)
- {
- utils::EnrichRaisedException($oTrigger, $e);
- }
- }
- }
- }
-
- // Resetting caselog fields value, otherwise the value will stay in it after submit.
- $this->oForm->ResetCaseLogFields();
-
- if ($bWasModified)
- {
- //=if (isNew) because $bActivateTriggers = (!$this->oObject->IsNew() && $this->oObject->IsModified())
- if(!$bActivateTriggers)
- {
- $aData['messages']['success'] += array( '_main' => array(Dict::Format('UI:Title:Object_Of_Class_Created', $this->oObject->GetName(),MetaModel::GetName(get_class($this->oObject)))));
- }
- else
- {
- $aData['messages']['success'] += array('_main' => array(Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($this->oObject)), $this->oObject->GetName())));
- }
- }
- }
- catch (Exception $e)
- {
- // End transaction with a rollback as something failed
- CMDBSource::Query('ROLLBACK');
- $aData['valid'] = false;
- $aData['messages']['error'] += array('_main' => array($e->getMessage()));
- IssueLog::Error(__METHOD__.' at line '.__LINE__.' : Rollback during submit ('.$e->getMessage().')');
- }
- finally
- {
- // Removing transaction id
- utils::RemoveTransaction($this->oForm->GetTransactionId());
- }
- }
- else
+ if (! $this->oForm->Validate())
{
// Handle errors
$aData['valid'] = false;
$aData['messages']['error'] += $this->oForm->GetErrorMessages();
+ return $aData;
}
+ // The try catch is essentially to start a MySQL transaction in order to ensure that all or none objects are persisted when creating an object with links
+ try
+ {
+ $sObjectClass = get_class($this->oObject);
+
+ // Starting transaction
+ CMDBSource::Query('START TRANSACTION');
+ // Forcing allowed writing on the object if necessary. This is used in some particular cases.
+ $bAllowWrite = ($sObjectClass === 'Person' && $this->oObject->GetKey() == UserRights::GetContactId());
+ if ($bAllowWrite)
+ {
+ $this->oObject->AllowWrite(true);
+ }
+
+ // Writing object to DB
+ $bActivateTriggers = (!$this->oObject->IsNew() && $this->oObject->IsModified());
+ $bWasModified = $this->oObject->IsModified();
+ try
+ {
+ $this->oObject->DBWrite();
+ }
+ catch (CoreCannotSaveObjectException $e)
+ {
+ throw new Exception($e->getHtmlMessage());
+ }
+ // Finalizing images link to object, otherwise it will be cleaned by the GC
+ InlineImage::FinalizeInlineImages($this->oObject);
+ // Finalizing attachments link to object
+ // TODO : This has to be refactored when the function from itop-attachments has been migrated into the core
+ if (isset($aArgs['attachmentIds']))
+ {
+ $this->FinalizeAttachments($aArgs['attachmentIds']);
+ }
+
+ // Ending transaction with a commit as everything was fine
+ CMDBSource::Query('COMMIT');
+
+ // Checking if we have to apply a stimulus
+ if (isset($aArgs['applyStimulus']))
+ {
+ $this->oObject->ApplyStimulus($aArgs['applyStimulus']['code']);
+ }
+ // Activating triggers only on update
+ if ($bActivateTriggers)
+ {
+ $sTriggersQuery = $this->oContainer->getParameter('combodo.portal.instance.conf')['properties']['triggers_query'];
+ if ($sTriggersQuery !== null)
+ {
+ $aParentClasses = MetaModel::EnumParentClasses($sObjectClass, ENUM_PARENT_CLASSES_ALL);
+ $oTriggerSet = new DBObjectSet(DBObjectSearch::FromOQL($sTriggersQuery), array(),
+ array('parent_classes' => $aParentClasses));
+ /** @var \Trigger $oTrigger */
+ while ($oTrigger = $oTriggerSet->Fetch())
+ {
+ try
+ {
+ $oTrigger->DoActivate($this->oObject->ToArgs('this'));
+ }
+ catch(Exception $e)
+ {
+ utils::EnrichRaisedException($oTrigger, $e);
+ }
+ }
+ }
+ }
+
+ // Resetting caselog fields value, otherwise the value will stay in it after submit.
+ $this->oForm->ResetCaseLogFields();
+
+ if ($bWasModified)
+ {
+ //=if (isNew) because $bActivateTriggers = (!$this->oObject->IsNew() && $this->oObject->IsModified())
+ if(!$bActivateTriggers)
+ {
+ $aData['messages']['success'] += array( '_main' => array(Dict::Format('UI:Title:Object_Of_Class_Created', $this->oObject->GetName(),MetaModel::GetName(get_class($this->oObject)))));
+ }
+ else
+ {
+ $aData['messages']['success'] += array('_main' => array(Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($this->oObject)), $this->oObject->GetName())));
+ }
+ }
+ }
+ catch (Exception $e)
+ {
+ // End transaction with a rollback as something failed
+ CMDBSource::Query('ROLLBACK');
+ $aData['valid'] = false;
+ $aData['messages']['error'] += array('_main' => array($e->getMessage()));
+ IssueLog::Error(__METHOD__.' at line '.__LINE__.' : Rollback during submit ('.$e->getMessage().')');
+ }
+
+
return $aData;
}
diff --git a/datamodels/2.x/itop-portal-base/portal/src/Form/PasswordFormManager.php b/datamodels/2.x/itop-portal-base/portal/src/Form/PasswordFormManager.php
index 98508b172..dd06311a1 100644
--- a/datamodels/2.x/itop-portal-base/portal/src/Form/PasswordFormManager.php
+++ b/datamodels/2.x/itop-portal-base/portal/src/Form/PasswordFormManager.php
@@ -48,6 +48,8 @@ class PasswordFormManager extends FormManager
// Building the form
$oForm = new Form('change_password');
+ $oForm->SetTransactionId(\utils::GetNewTransactionId());
+
// Adding hidden field with form type
$oField = new HiddenField('form_type');
$oField->SetCurrentValue('change_password');
@@ -93,14 +95,11 @@ class PasswordFormManager extends FormManager
*/
public function OnSubmit($aArgs = null)
{
- $aData = array(
- 'valid' => true,
- 'messages' => array(
- 'success' => array(),
- 'warnings' => array(), // Not used as of today, just to show that the structure is ready for change like this.
- 'error' => array(),
- ),
- );
+ $aData = parent::OnSubmit($aArgs);
+
+ if (! $aData['valid']) {
+ return $aData;
+ }
// Update object and form
$this->OnUpdate($aArgs);
diff --git a/datamodels/2.x/itop-portal-base/portal/src/Form/PreferencesFormManager.php b/datamodels/2.x/itop-portal-base/portal/src/Form/PreferencesFormManager.php
index b64388bd2..2b3c6b8a4 100644
--- a/datamodels/2.x/itop-portal-base/portal/src/Form/PreferencesFormManager.php
+++ b/datamodels/2.x/itop-portal-base/portal/src/Form/PreferencesFormManager.php
@@ -49,6 +49,8 @@ class PreferencesFormManager extends FormManager
// Building the form
$oForm = new Form('preferences');
+ $oForm->SetTransactionId(\utils::GetNewTransactionId());
+
// Adding hidden field with form type
$oField = new HiddenField('form_type');
$oField->SetCurrentValue('preferences');
@@ -97,14 +99,11 @@ class PreferencesFormManager extends FormManager
*/
public function OnSubmit($aArgs = null)
{
- $aData = array(
- 'valid' => true,
- 'messages' => array(
- 'success' => array(),
- 'warnings' => array(), // Not used as of today, just to show that the structure is ready for change like this.
- 'error' => array(),
- ),
- );
+ $aData = parent::OnSubmit($aArgs);
+
+ if (! $aData['valid']) {
+ return $aData;
+ }
// Update object and form
$this->OnUpdate($aArgs);
diff --git a/datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/layout.html.twig b/datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/layout.html.twig
index 445ed5a3b..e482db0b6 100644
--- a/datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/layout.html.twig
+++ b/datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/layout.html.twig
@@ -227,16 +227,10 @@
$('#user-profile-wrapper .form_field .help-block > p').remove();
// Submiting contact form through AJAX
- //if($('#{{ oContactForm.id }} .field_set').field_set('hasTouchedFields'))
- //{
- $('#{{ oContactForm.id }}').portal_form_handler('submit', oEvent);
- //}
+ $('#{{ oContactForm.id }}').portal_form_handler('submit', oEvent);
// Submiting preferences form through AJAX
- //if($('#{{ oPreferencesForm.id }} .field_set').field_set('hasTouchedFields'))
- //{
- $('#{{ oPreferencesForm.id }}').portal_form_handler('submit', oEvent);
- //}
+ $('#{{ oPreferencesForm.id }}').portal_form_handler('submit', oEvent);
{% if oPasswordForm is not null %}
// Submiting password form through AJAX
diff --git a/dictionaries/en.dictionary.itop.ui.php b/dictionaries/en.dictionary.itop.ui.php
index f2cfbc463..c83f41631 100644
--- a/dictionaries/en.dictionary.itop.ui.php
+++ b/dictionaries/en.dictionary.itop.ui.php
@@ -473,6 +473,7 @@ Dict::Add('EN US', 'English', 'English', array(
'UI:Error:InvalidDashboard' => 'Error: invalid dashboard',
'UI:Error:MaintenanceMode' => 'Application is currently in maintenance',
'UI:Error:MaintenanceTitle' => 'Maintenance',
+ 'UI:Error:InvalidToken' => 'Error: the requested operation have already been performed (CSRF token not found)',
'UI:GroupBy:Count' => 'Count',
'UI:GroupBy:Count+' => 'Number of elements',
@@ -1567,6 +1568,8 @@ When associated with a trigger, each action is given an "order" number, specifyi
'UI:Search:Criteria:Raw:Filtered' => 'Filtered',
'UI:Search:Criteria:Raw:FilteredOn' => 'Filtered on %1$s',
+
+ 'UI:StateChanged' => 'State changed',
));
//
diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php
index 6bf49e9aa..9dadd97b3 100644
--- a/dictionaries/fr.dictionary.itop.core.php
+++ b/dictionaries/fr.dictionary.itop.core.php
@@ -1053,12 +1053,14 @@ Dict::Add('FR FR', 'French', 'Français', array(
'Class:AsyncTask/Attribute:status+' => '',
'Class:AsyncTask/Attribute:remaining_retries' => 'Essais restants',
'Class:AsyncTask/Attribute:remaining_retries+' => '',
- 'Class:AsyncTask/Attribute:last_error_code' => 'Code d\'erreur',
+ 'Class:AsyncTask/Attribute:last_error_code' => 'Dernier code d\'erreur',
'Class:AsyncTask/Attribute:last_error_code+' => '',
- 'Class:AsyncTask/Attribute:last_error' => 'Error',
+ 'Class:AsyncTask/Attribute:last_error' => 'Dernière erreur',
'Class:AsyncTask/Attribute:last_error+' => '',
- 'Class:AsyncTask/Attribute:last_attempt' => 'Dernier essai',
+ 'Class:AsyncTask/Attribute:last_attempt' => 'Dernière tentative',
'Class:AsyncTask/Attribute:last_attempt+' => '',
+
+
));
// Additional language entries not present in English dict
diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php
index bc3dc5374..33e64de65 100644
--- a/dictionaries/fr.dictionary.itop.ui.php
+++ b/dictionaries/fr.dictionary.itop.ui.php
@@ -454,6 +454,7 @@ Dict::Add('FR FR', 'French', 'Français', array(
'UI:Error:InvalidDashboard' => 'Erreur: Le tableau de bord est invalide',
'UI:Error:MaintenanceMode' => 'L\'application est en maintenance',
'UI:Error:MaintenanceTitle' => 'Maintenance',
+ 'UI:Error:InvalidToken' => 'Erreur: l\'opération a déjà été effectuée (CSRF token not found)',
'UI:GroupBy:Count' => 'Nombre',
'UI:GroupBy:Count+' => 'Nombre d\'éléments',
@@ -1544,6 +1545,8 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
'UI:Search:Criteria:Raw:Filtered' => 'Filtré',
'UI:Search:Criteria:Raw:FilteredOn' => 'Filtré sur %1$s',
+
+ 'UI:StateChanged' => 'Etat modifié',
));
//
diff --git a/pages/UI.php b/pages/UI.php
index c02d1c165..8778bb123 100644
--- a/pages/UI.php
+++ b/pages/UI.php
@@ -1189,7 +1189,7 @@ HTML
$sOrigState = utils::ReadPostedParam('obj_state_orig', '');
if ($sTargetState != $sOrigState)
{
- $aWarnings[] = 'State changed';
+ $aWarnings[] = Dict::S('UI:StateChanged');
}
$oObj->Set($sStateAttCode, $sTargetState);
}
diff --git a/setup/setuputils.class.inc.php b/setup/setuputils.class.inc.php
index f861e3533..46847d70b 100644
--- a/setup/setuputils.class.inc.php
+++ b/setup/setuputils.class.inc.php
@@ -613,7 +613,19 @@ class SetupUtils
// availability of dot / dot.exe
if (empty($sGraphvizPath)) {
$sGraphvizPath = 'dot';
+ } else {
+ clearstatcache();
+ if (!is_file($sGraphvizPath) || !is_executable($sGraphvizPath)) {
+ //N°3412 avoid shell injection
+ return new CheckResult(CheckResult::ERROR,
+ "$sGraphvizPath could not be executed: Please make sure it is installed and in the path");
+ }
+
+ if (!utils::IsWindowsEnvironment()){
+ $sGraphvizPath = escapeshellcmd($sGraphvizPath);
+ }
}
+
$sCommand = "\"$sGraphvizPath\" -V 2>&1";
$aOutput = array();
diff --git a/sources/Form/FormManager.php b/sources/Form/FormManager.php
index 3b34e089a..cb6e15f2d 100644
--- a/sources/Form/FormManager.php
+++ b/sources/Form/FormManager.php
@@ -160,9 +160,45 @@ abstract class FormManager
/**
* @param array|null $aArgs
*
- * @return mixed
+ * @return array
+ *
+ * @since 2.7.4 3.0.0 N°3430
*/
- abstract public function OnSubmit($aArgs = null);
+ public function OnSubmit($aArgs = null)
+ {
+ $aData = array(
+ 'valid' => true,
+ 'messages' => array(
+ 'success' => array(),
+ 'warnings' => array(), // Not used as of today, just to show that the structure is ready for change like this.
+ 'error' => array(),
+ ),
+ );
+
+ $aData = $this->CheckTransaction($aData);
+
+ return $aData;
+ }
+
+ /**
+ * @param array $aData
+ *
+ * @return array
+ *
+ * @since 2.7.4 3.0.0 N°3430
+ */
+ public function CheckTransaction($aData)
+ {
+ $isTransactionValid = \utils::IsTransactionValid($this->oForm->GetTransactionId(), false); //The transaction token is kept in order to preserve BC with ajax forms (the second call would fail if the token is deleted). (The GC will take care of cleaning the token for us later on)
+ if (!$isTransactionValid) {
+ $aData['messages']['error'] += [
+ '_main' => [\Dict::S('UI:Error:InvalidToken')] //This message is generic, if you override this method you should use a more precise message. @see \Combodo\iTop\Portal\Form\ObjectFormManager::CheckTransaction
+ ];
+ $aData['valid'] = false;
+ }
+
+ return $aData;
+ }
/**
* @param array|null $aArgs
diff --git a/test/application/search/CriterionConversionTest.php b/test/application/search/CriterionConversionTest.php
index cd37a1c15..005b9441c 100644
--- a/test/application/search/CriterionConversionTest.php
+++ b/test/application/search/CriterionConversionTest.php
@@ -610,7 +610,8 @@ class CriterionConversionTest extends ItopDataTestCase
*/
function testOqlToForSearchToOqlAltLanguageFR($sOQL, $sExpectedOQL, $aExpectedCriterion)
{
- $this->OqlToSearchToOqlAltLanguage($sOQL, $sExpectedOQL, $aExpectedCriterion, "FR FR");
+ \MetaModel::GetConfig()->Set('date_and_time_format', array('default' => array('date' => 'Y-m-d', 'time' => 'H:i:s', 'date_time' => '$date $time')));
+ $this->OqlToSearchToOqlAltLanguage($sOQL, $sExpectedOQL, $aExpectedCriterion, "FR FR");
}
@@ -630,10 +631,10 @@ class CriterionConversionTest extends ItopDataTestCase
*/
function testOqlToForSearchToOqlAltLanguageEN($sOQL, $sExpectedOQL, $aExpectedCriterion)
{
+ \MetaModel::GetConfig()->Set('date_and_time_format', array('default' => array('date' => 'Y-m-d', 'time' => 'H:i:s', 'date_time' => '$date $time')));
$this->OqlToSearchToOqlAltLanguage($sOQL, $sExpectedOQL, $aExpectedCriterion, "EN US");
}
-
function OqlProviderDates()
{
return array(
diff --git a/test/core/CMDBSourceTest.php b/test/core/CMDBSourceTest.php
index db06932e1..132d71292 100644
--- a/test/core/CMDBSourceTest.php
+++ b/test/core/CMDBSourceTest.php
@@ -35,7 +35,7 @@ class CMDBSourceTest extends ItopTestCase
*/
public function testCompareFieldTypes($bResult, $sItopFieldType, $sDbFieldType)
{
- $this->assertEquals($bResult, CMDBSource::IsSameFieldTypes($sItopFieldType, $sDbFieldType));
+ $this->assertEquals($bResult, CMDBSource::IsSameFieldTypes($sItopFieldType, $sDbFieldType), "$sItopFieldType\n VS\n $sDbFieldType");
}
public function compareFieldTypesProvider()
@@ -106,12 +106,12 @@ class CMDBSourceTest extends ItopTestCase
"ENUM('CSP A','CSP M','NA','OEM(ROC)','OPEN(VL)','RETAIL (Boite)') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci",
"enum('CSP A','CSP M','NA','OEM(ROC)','OPEN(VL)','RETAIL (Boite)') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci",
),
-//FIXME N°3065 before the fix this returns true :(
-// 'ENUM with different values, containing parenthesis' => array(
-// false,
-// "ENUM('value 1 (with parenthesis)','value 2') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci",
-// "enum('value 1 (with parenthesis)','value 3') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci",
-// ),
+ //FIXME N°3065 before the fix this returns true :(
+ 'ENUM with different values, containing parenthesis' => array(
+ false,
+ "ENUM('value 1 (with parenthesis)','value 2') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci",
+ "enum('value 1 (with parenthesis)','value 3') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci",
+ ),
);
}
}
diff --git a/test/phpunit.xml.dist b/test/phpunit.xml.dist
index 2929c7e78..844d229aa 100644
--- a/test/phpunit.xml.dist
+++ b/test/phpunit.xml.dist
@@ -62,6 +62,9 @@
status
+
+ setup/SetupUtilsTest.php
+
integration
diff --git a/test/setup/SetupUtilsTest.php b/test/setup/SetupUtilsTest.php
new file mode 100644
index 000000000..47a996bb7
--- /dev/null
+++ b/test/setup/SetupUtilsTest.php
@@ -0,0 +1,72 @@
+assertEquals($iSeverity, $oCheck->iSeverity);
+ $this->assertContains($sLabel, $oCheck->sLabel);
+ }
+
+ public function CheckGraphvizProvider(){
+ if (substr(PHP_OS,0,3) === 'WIN'){
+ return [];
+ }
+
+ return [
+ "bash injection" => [
+ "touch /tmp/toto",
+ 0,
+ "could not be executed: Please make sure it is installed and in the path",
+ ],
+ "command ok" => [
+ "/usr/bin/whereis",
+ 2,
+ "",
+ ],
+ "empty command => dot by default" => [
+ "",
+ 2,
+ "",
+ ],
+ "command failed" => [
+ "/bin/ls",
+ 1,
+ "dot could not be executed (retcode=2): Please make sure it is installed and in the path",
+ ]
+ ];
+ }
+
+
+}