Compare commits

...

10 Commits

Author SHA1 Message Date
Molkobain
0a4bbfe5a9 🙈 Ensure data/.compilation-symlinks is not commited is deleted 2026-10-01 16:29:57 +02:00
Stephen Abello
31a4e1a0a0 N°10120 - Filtered classes in subqueries produce invalid SQL (#1059)
* N°10120 - Filtered classes in subqueries produce invalid SQL

* Avoid SQL cache to be shared between ids only and full columns queries
2026-10-01 10:44:33 +02:00
Stephen Abello
de74103118 Fix unit test cause by security changes in #1055 2026-10-01 09:35:55 +02:00
Stephen Abello
a2a00cb582 N°10075 - Update configuration parameters default value (#1055) 2026-09-29 10:02:57 +02:00
jf-cbd
4d699f79e8 Add unit test for N°9740 2026-09-28 11:30:54 +02:00
Stephen Abello
873cea1a76 N°9557 - Small improvement to shortcut preferences (#1049)
* N°9557 - Small improvement to shortcut preferences

* Apply code review suggestions
2026-09-25 10:28:05 +02:00
jf-cbd
adc2596b4a Issue/9740 fix attachment removal (#1052)
(cherry picked from commit af972313d7)
2026-09-18 15:40:52 +02:00
Molkobain
2ac4229ee0 N°10070 - Loss of DroidSansFallback font which impacts exports in east asian character sets (#1048)
* N°10070 - Loss of DroidSansFallback font which impacts exports in east asian character sets

* N°10070 - Add unit test
2026-09-15 10:03:52 +02:00
Stephen Abello
4a6af15237 N°10041 - Fullscreen CKeditor in a modal can't take the focus (#1043) 2026-09-14 09:52:21 +02:00
Stephen Abello
6c1206658e N°9916 - Fix menu notification indicator position on Firefox 2026-09-04 10:22:19 +02:00
22 changed files with 3704 additions and 843 deletions

1
.gitignore vendored
View File

@@ -33,7 +33,6 @@ tests/*/vendor/*
!/data/index.php
!/data/web.config
!/data/exclude.txt
!/data/.compilation-symlinks
# iTop extensions
/extensions/**

View File

@@ -61,7 +61,7 @@ foreach ($aTcpdfFontsDirContent as $sTcpdfFontResourceName) {
* 2) Then adding the DroidSansFallback font (useful for CJK data for example)
*/
echo $sCurrentScriptFileName.": ---2) Copying font files to TCPDF ($sTcPdfFontsFolder)...\n";
$aFontFilesToCopy = glob(__DIR__.'\droidsansfallback.*');
$aFontFilesToCopy = glob(__DIR__.DIRECTORY_SEPARATOR.'droidsansfallback.*');
foreach ($aFontFilesToCopy as $sFontFileToCopy) {
$sFontFileName = basename($sFontFileToCopy);
echo $sCurrentScriptFileName.': copying '.$sFontFileName."\n";

View File

@@ -109,10 +109,18 @@ $('#shortcut_rename_dlg').dialog({
modal: true,
title: '$sDialogTitle',
buttons: [
{ text: "$sOkButtonLabel", click: ShortcutRenameOK},
{ text: "$sCancelButtonLabel", click: function() {
$(this).dialog( "close" ); $(this).remove();
} },
{
text: "$sCancelButtonLabel",
click: function() {
$(this).dialog( "close" ); $(this).remove();
},
'class': 'ibo-button ibo-is-alternative ibo-is-neutral action cancel'
},
{
text: "$sOkButtonLabel",
click: ShortcutRenameOK,
'class': 'ibo-is-regular ibo-is-primary'
},
],
close: function() { $(this).remove(); }
});

View File

@@ -1750,8 +1750,8 @@ class Config
'security.disable_joined_classes_filter' => [
'type' => 'bool',
'description' => 'If true, scope filters aren\'t applied to joined classes or union classes not directly listed in the SELECT clause.',
'default' => true,
'value' => true,
'default' => false,
'value' => false,
'source_of_value' => '',
'show_in_conf_sample' => false,
],
@@ -1766,8 +1766,8 @@ class Config
'security.disable_exec_forced_login_for_all_enpoints' => [
'type' => 'bool',
'description' => 'If true, when no delegated authentication module is defined, no login will be forced on modules exec endpoints',
'default' => true,
'value' => true,
'default' => false,
'value' => false,
'source_of_value' => '',
'show_in_conf_sample' => false,
],

View File

@@ -1704,7 +1704,7 @@ class DBObjectSearch extends DBSearch
* @return array|mixed|\SQLObjectQuery|null
* @throws \CoreException
*/
public function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null)
public function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null, $bSelectOnlyIds = false)
{
// Hide objects that are not visible to the current user
//
@@ -1775,6 +1775,8 @@ class DBObjectSearch extends DBSearch
$aContextData['aSelectExpr'] = $aSelectExpr;
$sRawId .= $bGetCount;
$aContextData['bGetCount'] = $bGetCount;
$sRawId .= 'ids:'.($bSelectOnlyIds ? '1' : '0');
$aContextData['bSelectOnlyIds'] = $bSelectOnlyIds;
if (is_array($aSelectedClasses)) {
$sRawId .= implode(',', $aSelectedClasses); // Unions may alter the list of selected columns
}
@@ -1828,7 +1830,7 @@ class DBObjectSearch extends DBSearch
if (!isset($oSQLQuery)) {
$oKPI = new ExecutionKPI();
$oSQLObjectQueryBuilder = new SQLObjectQueryBuilder($oSearch);
$oSQLQuery = $oSQLObjectQueryBuilder->BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr);
$oSQLQuery = $oSQLObjectQueryBuilder->BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr, $bSelectOnlyIds);
$oKPI->ComputeStats('BuildSQLQueryStruct', $sOqlQuery);
if (self::$m_bQueryCacheEnabled) {
@@ -1938,7 +1940,7 @@ class DBObjectSearch extends DBSearch
$oSearch = $this;
$aClassesToFilter = $this->GetSelectedClasses();
// Opt-in for joined classes filtering, otherwise only filter the selected class(es)
// Joined classes filtering can be disabled through the configuration.
if (MetaModel::GetConfig()->Get('security.disable_joined_classes_filter') === false) {
$aClassesToFilter = $this->GetJoinedClasses();
}

View File

@@ -995,7 +995,7 @@ abstract class DBSearch
* @internal
*
*/
public function MakeSelectQuery($aOrderBy = [], $aArgs = [], $aAttToLoad = null, $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false, $bBeautifulSQL = true)
public function MakeSelectQuery($aOrderBy = [], $aArgs = [], $aAttToLoad = null, $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false, $bBeautifulSQL = true, $bSelectOnlyIds = false)
{
// Check the order by specification, and prefix with the class alias
// and make sure that the ordering columns are going to be selected
@@ -1036,7 +1036,7 @@ abstract class DBSearch
}
}
$oSQLQuery = $this->GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount);
$oSQLQuery = $this->GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, null, null, $bSelectOnlyIds);
if ($this->m_bNoContextParameters) {
// Only internal parameters
@@ -1120,7 +1120,7 @@ abstract class DBSearch
* @internal
*
*/
protected function GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $aGroupByExpr = null, $aSelectExpr = null)
protected function GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $aGroupByExpr = null, $aSelectExpr = null, $bSelectOnlyIds = false)
{
$oSearch = $this->ApplyDataFilters();
@@ -1142,7 +1142,7 @@ abstract class DBSearch
}
}
$oSQLQuery = $oSearch->GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr, null, $aSelectExpr);
$oSQLQuery = $oSearch->GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr, null, $aSelectExpr, $bSelectOnlyIds);
$oSQLQuery->SetSourceOQL($oSearch->ToOQL());
// Join to an additional table, if required...
@@ -1177,7 +1177,8 @@ abstract class DBSearch
$bGetCount,
$aGroupByExpr = null,
$aSelectedClasses = null,
$aSelectExpr = null
$aSelectExpr = null,
$bSelectOnlyIds = false
);
/**

View File

@@ -527,10 +527,10 @@ class DBUnionSearch extends DBSearch
throw new Exception('MakeUpdateQuery is not implemented for the unions!');
}
public function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null)
public function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null, $bSelectOnlyIds = false)
{
if (count($this->aSearches) == 1) {
return $this->aSearches[0]->GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr, $aSelectedClasses, $aSelectExpr);
return $this->aSearches[0]->GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr, $aSelectedClasses, $aSelectExpr, $bSelectOnlyIds);
}
$aSQLQueries = [];
@@ -610,7 +610,7 @@ class DBUnionSearch extends DBSearch
}
}
}
$oSubQuery = $oSearch->GetSQLQueryStructure($aQueryAttToLoad, false, $aQueryGroupByExpr, $aSearchSelectedClasses, $aQuerySelectExpr);
$oSubQuery = $oSearch->GetSQLQueryStructure($aQueryAttToLoad, false, $aQueryGroupByExpr, $aSearchSelectedClasses, $aQuerySelectExpr, $bSelectOnlyIds);
if (count($aSearchAliases) > 1) {
// Necessary to make sure that selected columns will match throughout all the queries
// (default order of selected fields depending on the order of JOINS)
@@ -683,7 +683,7 @@ class DBUnionSearch extends DBSearch
return $this;
}
// Opt-in for joined classes filtering, otherwise fallback on DBSearch filtering
// Joined classes filtering can be disabled through the configuration.
if (MetaModel::GetConfig()->Get('security.disable_joined_classes_filter') === true) {
return parent::ApplyDataFilters();
}

File diff suppressed because it is too large Load Diff

View File

@@ -29,13 +29,14 @@ class SQLObjectQueryBuilder
* @param array $aGroupByExpr
* @param array $aSelectedClasses
* @param array $aSelectExpr
* @param bool $bSelectOnlyIds
*
* @return null|SQLObjectQuery
* @throws \CoreException
*/
public function BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null)
public function BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null, $bSelectOnlyIds = false)
{
if ($bGetCount || !is_null($aGroupByExpr)) {
if ($bGetCount || !is_null($aGroupByExpr) || $bSelectOnlyIds) {
// Avoid adding all the fields for counts or "group by" requests
$aAttToLoad = [];
foreach ($this->oDBObjetSearch->GetSelectedClasses() as $sClassAlias => $sClass) {

View File

@@ -791,8 +791,9 @@ $ibo-navigation-menu--user-info--height--is-expanded: 100% !default;
color: $ibo-navigation-menu--user--text-color;
}
.ibo-navigation-menu--user-notifications--toggler--icon {
padding-left: $ibo-navigation-menu--user-notifications--toggler--icon--padding-left;
.ibo-navigation-menu--user-notifications--toggler--icon {
position: relative;
padding-left: $ibo-navigation-menu--user-notifications--toggler--icon--padding-left;
}
}

View File

@@ -56,7 +56,7 @@ function RenderAttachments(AjaxPage $oPage, $iTransactionId)
try {
require_once APPROOT.'/application/startup.inc.php';
require_once APPROOT.'/application/loginwebpage.class.inc.php';
LoginWebPage::DoLoginEx(null /* any portal */, false);
LoginWebPage::DoLogin(); // No user portal should access this endpoint.
$oPage = new AjaxPage("");
@@ -105,15 +105,6 @@ try {
$oPage->SetData($aResult);
break;
case 'remove':
$iAttachmentId = utils::ReadParam('att_id', '');
$oSearch = DBObjectSearch::FromOQL("SELECT Attachment WHERE id = :id");
$oSet = new DBObjectSet($oSearch, [], ['id' => $iAttachmentId]);
while ($oAttachment = $oSet->Fetch()) {
$oAttachment->DBDelete();
}
break;
case 'refresh_attachments_render':
$sTempId = utils::ReadParam('temp_id', '', false, 'transaction_id');
RenderAttachments($oPage, $sTempId);

View File

@@ -0,0 +1,19 @@
/*
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
// Overload of the default dialog widget
$.widget('ui.dialog', $.ui.dialog, {
_allowInteraction: function (oEvent) {
const oTarget = $(oEvent.target);
// If we interact with a CKEditor instance in fullscreen mode, we need to allow it
// We could check if the current instance is in the dialog, but it's easier to always allow it in fullscreen
if (oTarget.closest('.ck.ck-fullscreen__main-wrapper, .ck-body-wrapper').length > 0) {
return true;
}
// If that's not a specific case, fall back to the default behavior
return this._super(oEvent);
}
});

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1239,13 +1239,32 @@ JS
$oSearch = new DBObjectSearch('Shortcut');
$aShortcuts = utils::ReadMultipleSelection($oSearch);
$iShortcut = $aShortcuts[0];
$oShortcut = MetaModel::GetObject('Shortcut', $iShortcut);
$oShortcutSearch = new DBObjectSearch('Shortcut');
$oShortcutSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
$oShortcutSearch->AddCondition('id', $iShortcut, '=');
$oShortcutSet = new CMDBObjectSet($oShortcutSearch);
$oShortcut = $oShortcutSet->Fetch();
if ($oShortcut === null) {
throw new SecurityException(Dict::S('UI:ObjectDoesNotExist'));
}
$oShortcut->StartRenameDialog($oPage);
break;
case 'shortcut_rename_go':
$iShortcut = utils::ReadParam('id', 0);
$oShortcut = MetaModel::GetObject('Shortcut', $iShortcut);
$oShortcutSearch = new DBObjectSearch('Shortcut');
$oShortcutSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
$oShortcutSearch->AddCondition('id', $iShortcut, '=');
$oShortcutSet = new CMDBObjectSet($oShortcutSearch);
$oShortcut = $oShortcutSet->Fetch();
if ($oShortcut === null) {
throw new SecurityException(Dict::S('UI:ObjectDoesNotExist'));
}
$sName = utils::ReadParam('attr_name', '', false, 'raw_data');
if (strlen($sName) > 0) {

View File

@@ -243,7 +243,7 @@ JS
);
$oShortcutsToolBar->AddSubBlock($oShortcutsRenameButton);
// - Delete button
$oShortcutsDeleteButton = ButtonUIBlockFactory::MakeForSecondaryAction(
$oShortcutsDeleteButton = ButtonUIBlockFactory::MakeForDestructiveAction(
Dict::S('UI:Button:Delete'),
null,
null,

View File

@@ -209,6 +209,7 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
$this->LinkScriptFromAppRoot('js/pages/backoffice/keyboard-shortcuts.js');
// Used throughout the app.
$this->LinkScriptFromAppRoot('js/pages/backoffice/dialog.js');
$this->LinkScriptFromAppRoot('js/pages/backoffice/toolbox.js');
$this->LinkScriptFromAppRoot('js/pages/backoffice/on-ready.js');

View File

@@ -111,7 +111,7 @@ class LoginWebPageTest extends ItopDataTestCase
$this->assertStringContainsString('<title>iTop login</title>', $sPageContent, 'if itop is configured to force login when no there is no delegated authentication endpoints list, then login should be required.');
}
public function testWithoutDelegatedAuthenticationEndpointsListWithDefaultConfiguration()
public function testWithoutDelegatedAuthenticationEndpointsListRequiresLoginByDefault()
{
$sPageContent = $this->CallItopUri(
"pages/exec.php?exec_module=extension-without-delegated-authentication-endpoints-list&exec_page=src/Controller/File.php",
@@ -120,7 +120,24 @@ class LoginWebPageTest extends ItopDataTestCase
true
);
$this->assertStringContainsString('Yo', $sPageContent, 'by default (until N°9343) if no delegated authentication endpoints list is defined, not logged in persons should access pages');
$this->assertStringContainsString('<title>iTop login</title>', $sPageContent, 'by default, login should be required when no delegated authentication endpoints list is defined');
}
public function testWithoutDelegatedAuthenticationEndpointsListWithCompatibilityOptOut()
{
@chmod($this->oConfig->GetLoadedFile(), 0770);
$this->oConfig->Set('security.disable_exec_forced_login_for_all_enpoints', true, 'AnythingButEmptyOrUnknownValue');
$this->oConfig->WriteToFile();
@chmod($this->oConfig->GetLoadedFile(), 0444);
$sPageContent = $this->CallItopUri(
"pages/exec.php?exec_module=extension-without-delegated-authentication-endpoints-list&exec_page=src/Controller/File.php",
[],
[],
true
);
$this->assertStringContainsString('Yo !', $sPageContent, 'the compatibility opt-out should allow anonymous access when no delegated authentication endpoints list is defined');
}
public function testNotInDelegatedAuthenticationEndpointsList()

View File

@@ -0,0 +1,38 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\DotMake\Dependencies\Composer\Tcpdf;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
/**
* @coversNothing
*/
class TcpdfUpdateFontsTest extends ItopTestCase
{
public function testThatDroidSansFallbackFilesAreCopiedToTcpdfFontsFolderAfterLibraryUpdate(): void
{
$sSourcePattern = APPROOT
.'.make'.DIRECTORY_SEPARATOR.'dependencies'.DIRECTORY_SEPARATOR.'composer'.DIRECTORY_SEPARATOR.'tcpdf'.DIRECTORY_SEPARATOR.'droidsansfallback.*';
$aSourceFiles = glob($sSourcePattern);
$this->assertIsArray($aSourceFiles, 'Unable to read source TCPDF custom font files.');
$this->assertNotEmpty($aSourceFiles, 'No source files found for pattern droidsansfallback.*');
foreach ($aSourceFiles as $sSourceFilePath) {
$sFontFileName = basename($sSourceFilePath);
$sDestinationFilePath = APPROOT
.'lib'.DIRECTORY_SEPARATOR.'tecnickcom'.DIRECTORY_SEPARATOR.'tcpdf'.DIRECTORY_SEPARATOR.'fonts'.DIRECTORY_SEPARATOR.$sFontFileName;
$this->assertFileExists($sDestinationFilePath, "Missing copied font file: {$sFontFileName}");
$this->assertSame(
hash_file('sha256', $sSourceFilePath),
hash_file('sha256', $sDestinationFilePath),
"Copied font file content mismatch: {$sFontFileName}"
);
}
}
}

View File

@@ -419,6 +419,7 @@ class UserLocalTest extends ItopDataTestCase
$this->assertInstanceOf(ormLinkSet::class, $oProfilesSet);
$this->assertEquals(0, $oProfilesSet->Count());
MetaModel::GetConfig()->Set('security.hide_administrators', false);
MetaModel::GetConfig()->Set('security.disable_joined_classes_filter', true);
$oProfilesSet = $this->GetAdminUserProfileList();
$this->assertIsObject($oProfilesSet);
$this->assertInstanceOf(ormLinkSet::class, $oProfilesSet);

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Combodo\iTop\Test\UnitTest\Module\ItopAttachment;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
class AttachmentAjaxEndpointTest extends ItopDataTestCase
{
public const USE_TRANSACTION = false;
private const AUTHENTICATION_PASSWORD = 'tagada-Secret,007';
protected function setUp(): void
{
parent::setUp();
$this->BackupConfiguration();
$this->AddLoginModeAndSaveConfiguration('url');
}
/**
* @dataProvider AjaxEndpointAccessProvider
*/
public function testAjaxEndpointAccess(string $sProfile, int $iExpectedHttpCode): void
{
$sLogin = 'user-'.uniqid();
$this->CreateUser($sLogin, self::$aURP_Profiles[$sProfile], self::AUTHENTICATION_PASSWORD);
$iHttpCode = $this->CallAttachmentEndpointAs($sLogin);
$this->assertSame($iExpectedHttpCode, $iHttpCode);
}
public function AjaxEndpointAccessProvider(): array
{
return [
'console user' => ['Service Desk Agent', 200],
'portal user' => ['Portal user', 302], // redirect to portal
];
}
private function CallAttachmentEndpointAs(string $sLogin): int
{
$this->CallItopUri(
'env-production/itop-attachments/ajax.itop-attachment.php?operation=add&'.http_build_query([
'auth_user' => $sLogin,
'auth_pwd' => self::AUTHENTICATION_PASSWORD,
]),
[],
[
CURLOPT_HTTPHEADER => ['X-Combodo-Ajax:1'],
CURLOPT_POST => 0,
],
true
);
return $this->aLastCurlGetInfo['http_code'];
}
}