diff --git a/application/menunode.class.inc.php b/application/menunode.class.inc.php index eef87d461..9a18deac6 100644 --- a/application/menunode.class.inc.php +++ b/application/menunode.class.inc.php @@ -1423,12 +1423,21 @@ class ShortcutMenuNode extends MenuNode public function GetHyperlink($aExtraParams) { $sContext = $this->oShortcut->Get('context'); - $aContext = unserialize($sContext); - if (isset($aContext['menu'])) { - unset($aContext['menu']); - } - foreach ($aContext as $sArgName => $sArgValue) { - $aExtraParams[$sArgName] = $sArgValue; + try { + $aContext = utils::Unserialize($sContext); + if (isset($aContext['menu'])) { + unset($aContext['menu']); + } + foreach ($aContext as $sArgName => $sArgValue) { + $aExtraParams[$sArgName] = $sArgValue; + } + } catch (Exception $e) { + IssueLog::Warning("User shortcut corrupted, delete the shortcut", LogChannels::CONSOLE, [ + 'shortcut_name' => $this->oShortcut->GetName(), + 'root_cause' => $e->getMessage(), + ]); + // delete the shortcut + $this->oShortcut->DBDelete(); } return parent::GetHyperlink($aExtraParams); } diff --git a/application/utils.inc.php b/application/utils.inc.php index 77683858a..8caba53e6 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -3146,4 +3146,50 @@ TXT return $aTrace; } + + /** + * PHP unserialize encapsulation, allow throwing exception when not allowed object class is detected (for security hardening) + * + * @param string $data data to unserialize + * @param array $aOptions PHP @unserialise options + * @param bool $bThrowNotAllowedObjectClassException flag to throw exception + * + * @return mixed PHP @unserialise return + * @throws Exception + */ + public static function Unserialize(string $data, array $aOptions = ['allowed_classes' => false], bool $bThrowNotAllowedObjectClassException = true): mixed + { + $data = unserialize($data, $aOptions); + + if ($bThrowNotAllowedObjectClassException) { + try { + self::AssertNoIncompleteClassDetected($data); + } catch (Exception $e) { + throw new CoreException('Unserialization failed because an incomplete class was detected.', [], '', $e); + } + } + + return $data; + } + + /** + * Assert that data provided doesn't contain any incomplete class. + * + * @throws Exception + */ + public static function AssertNoIncompleteClassDetected(mixed $data): void + { + if (is_object($data)) { + if ($data instanceof __PHP_Incomplete_Class) { + throw new Exception('__PHP_Incomplete_Class_Name object detected'); + } + foreach (get_object_vars($data) as $property) { + self::AssertNoIncompleteClassDetected($property); + } + } elseif (is_array($data)) { + foreach ($data as $value) { + self::AssertNoIncompleteClassDetected($value); + } + } + } } diff --git a/core/cmdbsource.class.inc.php b/core/cmdbsource.class.inc.php index 8b17a931d..25ccbd7e1 100644 --- a/core/cmdbsource.class.inc.php +++ b/core/cmdbsource.class.inc.php @@ -1579,6 +1579,8 @@ class CMDBSource if (static::GetDBVendor() === static::ENUM_DB_VENDOR_MYSQL) { //Mysql 5.7.0 and upper deprecated --ssl and uses --ssl-mode instead return version_compare(static::GetDBVersion(), '5.7.11', '>='); + } elseif (static::GetDBVendor() === static::ENUM_DB_VENDOR_MARIADB) { + return version_compare(static::GetDBVersion(), '10.2.6', '>='); } return false; } diff --git a/core/userrights.class.inc.php b/core/userrights.class.inc.php index 2007ff4f5..cc980e039 100644 --- a/core/userrights.class.inc.php +++ b/core/userrights.class.inc.php @@ -1182,7 +1182,7 @@ class UserRights return self::$m_oUser->GetKey(); } else { // find the id out of the login string - $oUser = self::FindUser($sLogin); + $oUser = self::FindUser($sLogin, bAllowDisabledUsers: true); if (is_null($oUser)) { return null; } diff --git a/css/backoffice/_shame.scss b/css/backoffice/_shame.scss index 8b6664258..1f6529f29 100644 --- a/css/backoffice/_shame.scss +++ b/css/backoffice/_shame.scss @@ -56,6 +56,8 @@ $ibo-shame--slider--is-round--border-radius: 20px !default; $ibo-shame--slider--is-round--before--border-radius: 7px !default; +$ibo-blockquote--color: $ibo-body-text-color !default; + // N°2847 - Recolor svg illustrations with iTop's primary color .ibo-svg-illustration--container > svg *[fill="#6c63ff"]{ fill: $ibo-svg-illustration--fill; @@ -126,3 +128,11 @@ input:checked + .slider:before { .slider.round:before { border-radius: $ibo-shame--slider--is-round--before--border-radius; } + +/* + Bulma sets blockquote background color through a variable, it affects ckeditor and html display. + This rule is needed harmonize the blockquote text color in both contexts. + */ +.ibo-is-html-content blockquote { + color: $ibo-blockquote--color; +} \ No newline at end of file diff --git a/css/backoffice/components/input/_input-select.scss b/css/backoffice/components/input/_input-select.scss index 7bece46e1..6dd1ccb3b 100644 --- a/css/backoffice/components/input/_input-select.scss +++ b/css/backoffice/components/input/_input-select.scss @@ -203,8 +203,9 @@ $ibo-input-select--autocomplete-item-image--border: 1px solid $ibo-color-grey-60 } // N°7982 Default selectize stylesheet override +// N°9468 Dropdown content needs to be a few pixel shorter than the dropdown itself to avoid double scrollbar .selectize-dropdown-content{ - max-height: $ibo-input-select-selectize--dropdown--max-height; + max-height: calc(#{$ibo-input-select-selectize--dropdown--max-height} - 4px); } .selectize-dropdown.ui-menu .ui-state-active { diff --git a/js/extkeywidget.js b/js/extkeywidget.js index 0fa13781d..7c56a9177 100644 --- a/js/extkeywidget.js +++ b/js/extkeywidget.js @@ -120,6 +120,7 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper this.sFormAttCode = sFormAttCode; var me = this; + const iDropdownContentHeightDifference = 4; this.Init = function () { // make sure that the form is clean @@ -171,7 +172,7 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper // To avoid dropdown to be cut by the container's overflow hidden rule dropdownParent: 'body', onDropdownOpen: function (oDropdownElem) { - me.UpdateDropdownPosition(this.$control, oDropdownElem); + me.UpdateDropdownPosition(this.$control, oDropdownElem, this.$dropdown_content); }, }); let $selectize = $select[0].selectize; // This stores the selectize object to a variable (with name 'selectize') @@ -314,13 +315,14 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper }; /** - * Update the dropdown's position so it always fits in the screen - * - * @param {object} oControlElem jQuery object representing the "control" input (= where the user types) of the external key - * @param {object} oDropdownElem jQuery object representing the results dropdown - * @return {void} - */ - this.UpdateDropdownPosition = function (oControlElem, oDropdownElem) { + * Update the dropdown's position so it always fits in the screen + * + * @param {object} oControlElem jQuery object representing the "control" input (= where the user types) of the external key + * @param {object} oDropdownElem jQuery object representing the results dropdown + * @param {object|undefined} oDropdownContentElem + * @return {void} + */ + this.UpdateDropdownPosition = function (oControlElem, oDropdownElem, oDropdownContentElem) { // First fix width to ensure it's not too long const fControlWidth = oControlElem.outerWidth(); oDropdownElem.css('width', fControlWidth); @@ -328,6 +330,13 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper // Then, fix height / position to ensure it's within the viewport const fWindowHeight = window.innerHeight; + // Clear previously set rule so the comparison is done with dropdown real height + oDropdownElem.css('max-height', ''); + + if(oDropdownContentElem) { + oDropdownContentElem.css('max-height', ''); + } + const fControlTopY = oControlElem.offset().top; const fControlHeight = oControlElem.outerHeight(); @@ -338,14 +347,38 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper if (fDropdownBottomY > fWindowHeight) { // Set dropdown max-height to 1/3 of the screen, this way we are sure the dropdown will fit in either the top / bottom half of the screen - oDropdownElem.css('max-height', '30vh'); + oDropdownElem.css({ + maxHeight: '30vh', + }); fDropdownHeight = oDropdownElem.outerHeight(); - // Position dropdown above input if not enough space on the bottom part of the screen + // N°9468 Dropdown content needs to be a few pixel shorter than the dropdown itself to avoid double scrollbar + if(oDropdownContentElem) { + oDropdownContentElem.css('max-height', `calc(30vh - ${iDropdownContentHeightDifference}px)`); + } + + /* Position dropdown above input if not enough space on the bottom part of the screen + Doesn't seem to work with selectize as an internal plugin "auto_position" refreshes the top position after + this method is called, input set use a custom plugin to avoid fix this issue "plugin_combodo_auto_position" + This would need to take the potential 4px difference (iDropdownContentHeightDifference) into account if this is fixed. + */ if ((fDropdownTopY / fWindowHeight) > 0.6) { - oDropdownElem.css('top', fDropdownTopY - fDropdownHeight - fControlHeight); - } + oDropdownElem.css({ + top: fDropdownTopY - fDropdownHeight - fControlHeight, + borderTop: oDropdownElem.css('border-bottom') + }); + } + else { + oDropdownElem.css({ + borderTop: 'none' + }) + } } + else { + oDropdownElem.css({ + borderTop: 'none' + }) + } }; this.ManageScroll = function () { if ($('#label_'+me.id).scrollParent()[0].tagName != 'HTML') { diff --git a/js/selectize/plugin_combodo_auto_position.js b/js/selectize/plugin_combodo_auto_position.js index 7ec695c14..74d073046 100644 --- a/js/selectize/plugin_combodo_auto_position.js +++ b/js/selectize/plugin_combodo_auto_position.js @@ -19,10 +19,11 @@ Selectize.define("combodo_auto_position", function (aOptions) { // Selectize instance let oSelf = this; + const iDropdownContentHeightDifference = 4; // Plugin options aOptions = $.extend({ - maxDropDownHeight: 200, + maxDropDownHeight: '200px', }, aOptions ); @@ -33,28 +34,47 @@ Selectize.define("combodo_auto_position", function (aOptions) { // Override position dropdown function oSelf.positionDropdown = (function () { return function () { - let iRefHeight = oSelf.$dropdown.outerHeight() < aOptions.maxDropDownHeight ? - oSelf.$dropdown.outerHeight() : aOptions.maxDropDownHeight; + // Clear previously set rules so the comparison is done with dropdown real height + oSelf.$dropdown.css({ + 'max-height': '', + }); - if(oSelf.$control.offset().top + oSelf.$control.outerHeight() + iRefHeight > window.innerHeight){ + oSelf.$dropdown_content.css({ + 'max-height': '', + }); - oSelf.$dropdown.css({ - top: oSelf.$control.offset().top - iRefHeight, - left: oSelf.$control.offset().left, + let iDropdownHeight = oSelf.$dropdown.outerHeight(); + if(oSelf.$control.offset().top + oSelf.$control.outerHeight() + iDropdownHeight > window.innerHeight){ + + // Apply max-height as we are overflowing, that'll allow us to calculate where we should place ourselves later + oSelf.$dropdown.css({ + maxHeight: `${aOptions.maxDropDownHeight}`, + }) + + iDropdownHeight = oSelf.$dropdown.outerHeight(); + + oSelf.$dropdown.css({ + top: oSelf.$control.offset().top - iDropdownHeight + iDropdownContentHeightDifference, // Content will be shorter, so our real height too + left: oSelf.$control.offset().left, width: oSelf.$wrapper.outerWidth(), - 'max-height': `${aOptions.maxDropDownHeight}px`, - 'overflow-y': 'auto', - 'border-top': '1px solid #d0d0d0', + overflowY: 'auto', + borderTop : oSelf.$dropdown.css('border-bottom') }); + + // N°9468 Dropdown content needs to be a few pixel shorter than the dropdown itself to avoid double scrollbar + oSelf.$dropdown_content.css({ + 'max-height': `calc(${aOptions.maxDropDownHeight} - ${iDropdownContentHeightDifference}px)` + }); + } else{ oSelf.$dropdown.css({ top: oSelf.$control.offset().top + oSelf.$control.outerHeight(), left: oSelf.$control.offset().left, width: oSelf.$wrapper.outerWidth(), - 'max-height': `${aOptions.maxDropDownHeight}px`, - 'overflow-y': 'auto' - }); + overflowY: 'auto', + borderTop: 'none' + }); } }; }()); diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 1f900d9b5..a213032b6 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -134,6 +134,7 @@ return array( 'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => $baseDir . '/sources/Application/Helper/CKEditorHelper.php', 'Combodo\\iTop\\Application\\Helper\\ExportHelper' => $baseDir . '/sources/Application/Helper/ExportHelper.php', 'Combodo\\iTop\\Application\\Helper\\FormHelper' => $baseDir . '/sources/Application/Helper/FormHelper.php', + 'Combodo\\iTop\\Application\\Helper\\ImportHelper' => $baseDir . '/sources/Application/Helper/ImportHelper.php', 'Combodo\\iTop\\Application\\Helper\\SearchHelper' => $baseDir . '/sources/Application/Helper/SearchHelper.php', 'Combodo\\iTop\\Application\\Helper\\Session' => $baseDir . '/sources/Application/Helper/Session.php', 'Combodo\\iTop\\Application\\Helper\\WebResourcesHelper' => $baseDir . '/sources/Application/Helper/WebResourcesHelper.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 5422872d6..039c7bee0 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -520,6 +520,7 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/CKEditorHelper.php', 'Combodo\\iTop\\Application\\Helper\\ExportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ExportHelper.php', 'Combodo\\iTop\\Application\\Helper\\FormHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/FormHelper.php', + 'Combodo\\iTop\\Application\\Helper\\ImportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ImportHelper.php', 'Combodo\\iTop\\Application\\Helper\\SearchHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/SearchHelper.php', 'Combodo\\iTop\\Application\\Helper\\Session' => __DIR__ . '/../..' . '/sources/Application/Helper/Session.php', 'Combodo\\iTop\\Application\\Helper\\WebResourcesHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/WebResourcesHelper.php', diff --git a/pages/ajax.csvimport.php b/pages/ajax.csvimport.php index 5e1e19419..185ed5932 100644 --- a/pages/ajax.csvimport.php +++ b/pages/ajax.csvimport.php @@ -5,9 +5,11 @@ * @license http://opensource.org/licenses/AGPL-3.0 */ +use Combodo\iTop\Application\Helper\ImportHelper; use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory; +use Combodo\iTop\Application\UI\Base\Component\Input\Select\Select; use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectOptionUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Input\TextArea; @@ -387,6 +389,14 @@ EOF } break; + case 'display_classes_select': + $oPage = new AjaxPage(""); + $sClassName = utils::ReadPostedParam('class_name', '', utils::ENUM_SANITIZATION_FILTER_CLASS); + $bAdvanced = utils::ReadPostedParam('advanced', 'false'); + $oClassesSelect = ImportHelper::GetClassesSelectUIBlock('class_name', $sClassName, UR_ACTION_BULK_MODIFY, $bAdvanced === 'true'); + $oPage->AddSubBlock($oClassesSelect); + break; + case 'get_csv_template': $sClassName = utils::ReadParam('class_name'); $sFormat = utils::ReadParam('format', 'csv'); diff --git a/pages/csvimport.php b/pages/csvimport.php index b033509b1..e68e8aa04 100644 --- a/pages/csvimport.php +++ b/pages/csvimport.php @@ -5,6 +5,7 @@ * @license http://opensource.org/licenses/AGPL-3.0 */ +use Combodo\iTop\Application\Helper\ImportHelper; use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\CollapsibleSection\CollapsibleSectionUIBlockFactory; @@ -14,7 +15,6 @@ use Combodo\iTop\Application\UI\Base\Component\Form\FormUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Html\Html; use Combodo\iTop\Application\UI\Base\Component\Input\FileSelect\FileSelectUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory; -use Combodo\iTop\Application\UI\Base\Component\Input\Select\Select; use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectOptionUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Input\TextArea; @@ -30,7 +30,6 @@ use Combodo\iTop\Application\WebPage\AjaxPage; use Combodo\iTop\Application\WebPage\ErrorPage; use Combodo\iTop\Application\WebPage\iTopWebPage; use Combodo\iTop\Application\WebPage\WebPage; -use Combodo\iTop\Renderer\BlockRenderer; use Combodo\iTop\Service\Import\CSVImportPageProcessor; try { @@ -52,46 +51,6 @@ try { $oPage = new iTopWebPage(Dict::S('UI:Title:BulkImport')); $oPage->SetBreadCrumbEntry('ui-tool-bulkimport', Dict::S('Menu:CSVImportMenu'), Dict::S('UI:Title:BulkImport+'), '', 'fas fa-file-import', iTopWebPage::ENUM_BREADCRUMB_ENTRY_ICON_TYPE_CSS_CLASSES); - /** - * Helper function to build a select from the list of valid classes for a given action - * - * @param string $sName The name of the select in the HTML form - * @param $sDefaultValue - * @param integer $iWidthPx The width (in pixels) of the drop-down list - * @param integer $iActionCode The ActionCode (from UserRights) to check for authorization for the classes - * - * @return \Combodo\iTop\Application\UI\Base\Component\Input\Select\ - */ - function GetClassesSelectUIBlock(string $sName, $sDefaultValue, int $iActionCode, bool $bAdvanced = false): Select - { - $oSelectBlock = SelectUIBlockFactory::MakeForSelect($sName, 'select_'.$sName); - $oOption = SelectOptionUIBlockFactory::MakeForSelectOption("", Dict::S('UI:CSVImport:ClassesSelectOne'), false); - $oSelectBlock->AddSubBlock($oOption); - $aValidClasses = []; - $aClassCategories = ['bizmodel', 'addon/authentication']; - if ($bAdvanced) { - $aClassCategories[] = 'grant_by_profile'; - } - if (UserRights::IsAdministrator()) { - $aClassCategories[] = 'application'; - } - foreach ($aClassCategories as $sClassCategory) { - foreach (MetaModel::GetClasses($sClassCategory) as $sClassName) { - if ((is_null($iActionCode) || UserRights::IsActionAllowed($sClassName, $iActionCode)) && - (!MetaModel::IsAbstract($sClassName))) { - $sDisplayName = ($bAdvanced) ? MetaModel::GetName($sClassName)." ($sClassName)" : MetaModel::GetName($sClassName); - $aValidClasses[$sDisplayName] = SelectOptionUIBlockFactory::MakeForSelectOption($sClassName, $sDisplayName, ($sClassName == $sDefaultValue)); - } - } - } - ksort($aValidClasses); - foreach ($aValidClasses as $sValue => $oBlock) { - $oSelectBlock->AddSubBlock($oBlock); - } - - return $oSelectBlock; - } - /** * Helper to 'check' an input in an HTML form if the current value equals the value given * @@ -330,7 +289,7 @@ try { $oClassesSelect->AddSubBlock($oDefaultSelect); $aSynchroUpdate = utils::ReadParam('synchro_update', []); } else { - $oClassesSelect = GetClassesSelectUIBlock('class_name', $sClassName, UR_ACTION_BULK_MODIFY, (bool)$bAdvanced); + $oClassesSelect = ImportHelper::GetClassesSelectUIBlock('class_name', $sClassName, UR_ACTION_BULK_MODIFY, (bool)$bAdvanced); } $oPanel = TitleUIBlockFactory::MakeForPage(Dict::S('UI:Title:CSVImportStep3')); $oPage->AddSubBlock($oPanel); @@ -354,11 +313,9 @@ try { $oAdvancedMode->GetInput()->SetIsChecked(($bAdvanced == 1)); $oAdvancedMode->SetBeforeInput(false); $oAdvancedMode->GetInput()->AddCSSClass('ibo-input-checkbox'); + $oAdvancedMode->SetDescription(utils::EscapeHtml(Dict::S('UI:CSVImport:AdvancedMode+'))); $oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oAdvancedMode)); - $oDivAdvancedHelp = UIContentBlockUIBlockFactory::MakeStandard("advanced_help")->AddCSSClass('ibo-is-hidden'); - $oForm->AddSubBlock($oDivAdvancedHelp); - $oDivMapping = UIContentBlockUIBlockFactory::MakeStandard("mapping")->AddCSSClass('mt-5'); $oMessage = AlertUIBlockFactory::MakeForInformation(Dict::S('UI:CSVImport:SelectAClassFirst'))->SetIsClosable(false)->SetIsCollapsible(false); $oDivMapping->AddSubBlock($oMessage); @@ -395,7 +352,7 @@ try { $oPage->add_ready_script( <<add_script( - <<AddTab('tabsTemplate', Dict::S('UI:CSVImport:Tab:Templates')); - $oFieldTemplate = FieldUIBlockFactory::MakeFromObject(Dict::S('UI:CSVImport:PickClassForTemplate'), GetClassesSelectUIBlock('template_class', '', UR_ACTION_BULK_MODIFY)); + $oFieldTemplate = FieldUIBlockFactory::MakeFromObject(Dict::S('UI:CSVImport:PickClassForTemplate'), ImportHelper::GetClassesSelectUIBlock('template_class', '', UR_ACTION_BULK_MODIFY)); $oTabTemplate->AddSubBlock($oFieldTemplate); $oDivTemplate = UIContentBlockUIBlockFactory::MakeStandard("template")->AddCSSClass("ibo-is-visible"); $oTabTemplate->AddSubBlock($oDivTemplate); diff --git a/setup/backup.class.inc.php b/setup/backup.class.inc.php index feb9f84a8..648d89884 100644 --- a/setup/backup.class.inc.php +++ b/setup/backup.class.inc.php @@ -511,7 +511,7 @@ EOF; { $bDbTlsEnabled = $oConfig->Get('db_tls.enabled'); if (!$bDbTlsEnabled) { - return ''; + return CMDBSource::IsSslModeDBVersion() ? ' --skip-ssl' : ''; } $sTlsOptions = ''; // Mysql 5.7.11 and upper deprecated --ssl and uses --ssl-mode instead diff --git a/sources/Application/Helper/ImportHelper.php b/sources/Application/Helper/ImportHelper.php new file mode 100644 index 000000000..c6ffd43df --- /dev/null +++ b/sources/Application/Helper/ImportHelper.php @@ -0,0 +1,65 @@ +AddSubBlock($oOption); + $aValidClasses = []; + $aClassCategories = ['bizmodel', 'addon/authentication']; + if ($bAdvanced) { + $aClassCategories[] = 'grant_by_profile'; + } + if (UserRights::IsAdministrator()) { + $aClassCategories[] = 'application'; + } + foreach ($aClassCategories as $sClassCategory) { + foreach (MetaModel::GetClasses($sClassCategory) as $sClassName) { + if ((is_null($iActionCode) || UserRights::IsActionAllowed($sClassName, $iActionCode)) && + (!MetaModel::IsAbstract($sClassName))) { + $sDisplayName = ($bAdvanced) ? MetaModel::GetName($sClassName)." ($sClassName)" : MetaModel::GetName($sClassName); + $aValidClasses[$sDisplayName] = SelectOptionUIBlockFactory::MakeForSelectOption($sClassName, $sDisplayName, ($sClassName == $sDefaultValue)); + } + } + } + ksort($aValidClasses); + foreach ($aValidClasses as $sValue => $oBlock) { + $oSelectBlock->AddSubBlock($oBlock); + } + + return $oSelectBlock; + } +} diff --git a/sources/Application/UI/Base/Component/DataTable/DataTableSettings.php b/sources/Application/UI/Base/Component/DataTable/DataTableSettings.php index 2a4496b11..03ceb31f1 100644 --- a/sources/Application/UI/Base/Component/DataTable/DataTableSettings.php +++ b/sources/Application/UI/Base/Component/DataTable/DataTableSettings.php @@ -8,8 +8,13 @@ use AttributeFriendlyName; use AttributeLinkedSet; use cmdbAbstract; use cmdbAbstractObject; +use CoreException; use Dict; +use Exception; +use IssueLog; +use LogChannels; use Metamodel; +use utils; /** * Class DataTableSettings @@ -130,7 +135,10 @@ class DataTableSettings */ public function unserialize($sData) { - $aData = unserialize($sData); + $aData = utils::Unserialize($sData); + if (!is_array($aData)) { + throw new CoreException('Wrong data table settings format, expected an array', ['datatable_settings_data' => $aData]); + } $this->iDefaultPageSize = $aData['iDefaultPageSize']; $this->aColumns = $aData['aColumns']; foreach ($this->aClassAliases as $sAlias => $sClass) { @@ -269,7 +277,19 @@ class DataTableSettings return null; } } - $oSettings->unserialize($pref); + + try { + $oSettings->unserialize($pref); + } catch (Exception $e) { + IssueLog::Warning("User table settings corrupted, back to the default values provided by the data model", LogChannels::CONSOLE, [ + 'table_id' => $sTableId, + 'root_cause' => $e->getMessage(), + ]); + // unset the preference + appUserPreferences::UnsetPref($oSettings->GetPrefsKey($sTableId)); + // use the default values provided by the data model + return null; + } return $oSettings; } diff --git a/templates/base/components/input/set/layout.ready.js.twig b/templates/base/components/input/set/layout.ready.js.twig index 60c132bb3..ea07bbc34 100644 --- a/templates/base/components/input/set/layout.ready.js.twig +++ b/templates/base/components/input/set/layout.ready.js.twig @@ -23,7 +23,7 @@ let oWidget{{ oUIBlock.GetId() }} = $('#{{ oUIBlock.GetId() }}').selectize({ }, {# PLUGIN combodo auto position #} 'combodo_auto_position' : { - maxDropDownHeight: 300, {# in px #} + maxDropDownHeight: '30vh', {# same value as external key widget #} }, {# PLUGIN combodo add button #} {% if oUIBlock.HasAddOptionButton() %} diff --git a/tests/php-unit-tests/unitary-tests/application/utilsTest.php b/tests/php-unit-tests/unitary-tests/application/utilsTest.php index b9b109e0a..593883a31 100644 --- a/tests/php-unit-tests/unitary-tests/application/utilsTest.php +++ b/tests/php-unit-tests/unitary-tests/application/utilsTest.php @@ -23,6 +23,7 @@ namespace Combodo\iTop\Test\UnitTest\Application; use Combodo\iTop\Test\UnitTest\ItopTestCase; +use CoreException; use ormDocument; use utils; @@ -983,4 +984,21 @@ INI; unlink($sTmpFileOutsideItop); } + + public function testUnserialize() + { + // data to unserialize containing an object + $sData = 'a:2:{s:6:"string";s:9:"My string";s:6:"object";O:8:"DateTime":3:{s:4:"date";s:26:"2026-04-13 09:09:23.033175";s:13:"timezone_type";i:3;s:8:"timezone";s:16:"Europe/Amsterdam";}}'; + + // allow the DateTime object (no exception triggered) + utils::Unserialize($sData, ['allowed_classes' => ['DateTime']]); + + // flag to avoid throwing an exception + utils::Unserialize($sData, ['allowed_classes' => false], false); + + // flag to require throwing an exception + $this->expectException(CoreException::class); + utils::Unserialize($sData); + + } }