Merge remote-tracking branch 'origin/support/3.2' into develop

# Conflicts:
#	core/attributedef.class.inc.php
#	setup/extensionsmap.class.inc.php
#	tests/php-unit-tests/composer.lock
This commit is contained in:
Benjamin DALSASS
2026-03-17 08:05:10 +01:00
35 changed files with 555 additions and 63 deletions

View File

@@ -1748,6 +1748,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'security.force_login_when_no_delegated_authentication_endpoints_list' => [
'type' => 'bool',
'description' => 'If true, when no execution policy is defined, the user will be forced to log in (instead of being automatically logged in with the default profile)',
'default' => false,
'value' => false,
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'behind_reverse_proxy' => [
'type' => 'bool',
'description' => 'If true, then proxies custom header (X-Forwarded-*) are taken into account. Use only if the webserver is not publicly accessible (reachable only by the reverse proxy)',

View File

@@ -298,6 +298,46 @@ N°8681 * @param string|null $sHtml The HTML fragment to process
return $sHtml;
}
/**
* Replace <img> tags with a data-img-id attribute by the actual image in base64 representation
* so that the image can be displayed even if the download URL is not accessible (e.g. in unauthenticated approval templates)
*
* @param string $sHtml The HTML fragment to process
*
* @return String The modified HTML
* @since 3.2.3
*/
public static function ReplaceInlineImagesWithBase64Representation(string $sHtml): String
{
return preg_replace_callback(
'/<img\s+[^>]*'.static::DOM_ATTR_ID.'="(\d+)"[^>]*>/i',
function ($matches) {
// Extract inline image ID from the tag
$id = $matches[1];
try {
// Retrieve inline image
$oInline = MetaModel::GetObject(InlineImage::class, $id, true, true);
$oOrmDocument = $oInline->Get('contents');
// Replace src image by the base64 representation
$sInlineImageAsBase64 = base64_encode($oOrmDocument->GetData());
$sDataUri = 'data:'.$oOrmDocument->GetMimeType().';base64,'.$sInlineImageAsBase64;
$sImage = preg_replace('/src=["\'][^"\']+["\']/', 'src="'.$sDataUri.'"', $matches[0]);
// Remove sensitive information (the image ID and secret) from the tag
$sImage = preg_replace('/'.static::DOM_ATTR_ID.'="\d+"\s+'.static::DOM_ATTR_SECRET.'="\w+"/', '', $sImage);
} catch (Exception $e) {
$sImage = '<img src="" alt="'.Dict::S('UI:MissingInlineImage').'">';
}
return $sImage;
},
$sHtml
);
}
/**
* Add an extra attribute data-img-id for images which are based on an actual InlineImage
* so that we can later reconstruct the full "src" URL when needed

View File

@@ -41,6 +41,11 @@ SetupWebPage::AddModule(
'doc.manual_setup' => '',
'doc.more_information' => '',
// Security
'delegated_authentication_endpoints' => [
'ajax.backup.php',
],
// Default settings
//
'settings' => [

View File

@@ -37,6 +37,10 @@ SetupWebPage::AddModule(
// add your sample data XML files here,
],
'delegated_authentication_endpoints' => [
'ajax.php',
],
// Documentation
//
'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any

View File

@@ -1402,6 +1402,7 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', [
'UI:SelectInlineImageToUpload' => 'Vyberte obrázek',
'UI:AvailableInlineImagesLegend' => 'Dostupné obrázky',
'UI:NoInlineImage' => 'Na serveru není dostupný žádný obrázek. Nahrajte nějaký pomocí tlačítka výše.',
'UI:MissingInlineImage' => 'Chybějící obrázek',
'UI:ToggleFullScreen' => 'Přepnout zobrazení',
'UI:Button:ResetImage' => 'Obnovit původní obrázek',
'UI:Button:RemoveImage' => 'Odebrat obrázek',

View File

@@ -1404,6 +1404,7 @@ Ved tilknytningen til en trigger, bliver hver handling tildelt et "rækkefølge"
'UI:SelectInlineImageToUpload' => 'Select the image to upload~~',
'UI:AvailableInlineImagesLegend' => 'Available images~~',
'UI:NoInlineImage' => 'There is no image available on the server. Use the "Browse" button above to select an image from your computer and upload it to the server.~~',
'UI:MissingInlineImage' => 'Manglende billede',
'UI:ToggleFullScreen' => 'Toggle Maximize / Minimize~~',
'UI:Button:ResetImage' => 'Recover the previous image~~',
'UI:Button:RemoveImage' => 'Remove the image~~',

View File

@@ -1401,6 +1401,7 @@ Wenn Aktionen mit Trigger verknüpft sind, bekommt jede Aktion eine Auftragsnumm
'UI:SelectInlineImageToUpload' => 'Wähle das Bild für den Upload aus',
'UI:AvailableInlineImagesLegend' => 'Verfügbare Bilder',
'UI:NoInlineImage' => 'Es sind keine Bilder auf dem Server verfügbar. Nutze den "Durchsuchen" Button oben, um ein Bild vom Computer hochzuladen.',
'UI:MissingInlineImage' => 'Bild fehlt',
'UI:ToggleFullScreen' => 'Maximieren / Minimieren',
'UI:Button:ResetImage' => 'Vorheriges Bild wiederherstellen',
'UI:Button:RemoveImage' => 'Bild löschen',

View File

@@ -1482,6 +1482,7 @@ When associated with a trigger, each action is given an "order" number, specifyi
'UI:SelectInlineImageToUpload' => 'Select the image to upload',
'UI:AvailableInlineImagesLegend' => 'Available images',
'UI:NoInlineImage' => 'There is no image available on the server. Use the "Browse" button above to select an image from your computer and upload it to the server.',
'UI:MissingInlineImage' => 'Missing image',
'UI:ToggleFullScreen' => 'Toggle Maximize / Minimize',
'UI:Button:ResetImage' => 'Recover the previous image',

View File

@@ -1478,6 +1478,7 @@ When associated with a trigger, each action is given an "order" number, specifyi
'UI:SelectInlineImageToUpload' => 'Select the image to upload',
'UI:AvailableInlineImagesLegend' => 'Available images',
'UI:NoInlineImage' => 'There is no image available on the server. Use the "Browse" button above to select an image from your computer and upload it to the server.',
'UI:MissingInlineImage' => 'Missing image',
'UI:ToggleFullScreen' => 'Toggle Maximise / Minimise',
'UI:Button:ResetImage' => 'Recover the previous image',

View File

@@ -1404,6 +1404,7 @@ Cuando se asocien con un disparador, cada acción recibe un número de "orden",
'UI:SelectInlineImageToUpload' => 'Seleccione la imágen a subir',
'UI:AvailableInlineImagesLegend' => 'Imágenes disponibles',
'UI:NoInlineImage' => 'No hay imágenes disponibles en el servidor. Use el botón "Seleccionar archivo" para seleccionar una imágen de su equipo local y subirla al servidor.',
'UI:MissingInlineImage' => 'Imagen faltante',
'UI:ToggleFullScreen' => 'Cambiar Maximizar / Minimizar',
'UI:Button:ResetImage' => 'Recuperar imágen previa',
'UI:Button:RemoveImage' => 'Remover imágen',

View File

@@ -1452,6 +1452,7 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
'UI:SelectInlineImageToUpload' => 'Sélectionnez l\'image à ajouter',
'UI:AvailableInlineImagesLegend' => 'Images disponibles',
'UI:NoInlineImage' => 'Il n\'y a aucune image de disponible sur le serveur. Utilisez le bouton "Parcourir" (ci-dessus) pour sélectionner une image sur votre ordinateur et la télécharger sur le serveur.',
'UI:MissingInlineImage' => 'Image introuvable',
'UI:ToggleFullScreen' => 'Agrandir / Minimiser',
'UI:Button:ResetImage' => 'Récupérer l\'image initiale',
'UI:Button:RemoveImage' => 'Supprimer l\'image',

View File

@@ -1407,6 +1407,7 @@ A művelet eseményindítóhoz rendelésekor kap egy sorszámot , amely meghatá
'UI:SelectInlineImageToUpload' => 'Válasszon egy képet',
'UI:AvailableInlineImagesLegend' => 'Elérhető képek',
'UI:NoInlineImage' => 'A szerveren nincs elérhető kép. Használja a fenti "Tallózás" gombot egy kép kiválasztásához a számítógépéről, és töltse fel a szerverre.',
'UI:MissingInlineImage' => 'Hiányzó kép',
'UI:ToggleFullScreen' => 'Maximalizálás / Minimalizálás',
'UI:Button:ResetImage' => 'Az előző kép visszaállítása',
'UI:Button:RemoveImage' => 'Kép eltávolítása',

View File

@@ -1450,6 +1450,7 @@ Quando è associata a un trigger, a ogni azione è assegnato un numero "ordine",
'UI:SelectInlineImageToUpload' => 'Seleziona l\'immagine da caricare',
'UI:AvailableInlineImagesLegend' => 'Immagini disponibili',
'UI:NoInlineImage' => 'Non ci sono immagini disponibili sul server. Utilizza il pulsante "Sfoglia" sopra per selezionare un\'immagine dal tuo computer e caricarla sul server.',
'UI:MissingInlineImage' => 'Immagine mancante',
'UI:ToggleFullScreen' => 'Attiva/Disattiva a schermo intero',
'UI:Button:ResetImage' => 'Ripristina l\'immagine precedente',
'UI:Button:RemoveImage' => 'Rimuovi l\'immagine',

View File

@@ -1408,6 +1408,7 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
'UI:SelectInlineImageToUpload' => 'Select the image to upload~~',
'UI:AvailableInlineImagesLegend' => 'Available images~~',
'UI:NoInlineImage' => 'There is no image available on the server. Use the "Browse" button above to select an image from your computer and upload it to the server.~~',
'UI:MissingInlineImage' => 'Missing image~~',
'UI:ToggleFullScreen' => 'Toggle Maximize / Minimize~~',
'UI:Button:ResetImage' => 'Recover the previous image~~',
'UI:Button:RemoveImage' => 'Remove the image~~',

View File

@@ -1407,6 +1407,7 @@ Bij die koppeling wordt aan elke actie een volgorde-nummer gegeven. Dit bepaalt
'UI:SelectInlineImageToUpload' => 'Selecteer een afbeelding om te uploaden',
'UI:AvailableInlineImagesLegend' => 'Beschikbare afbeeldingen',
'UI:NoInlineImage' => 'Er is geen afbeelding beschikbaar op de server. Gebruik de "Afbeeldingen doorbladeren..." knop hierboven om een afbeelding te kiezen op je toestel.',
'UI:MissingInlineImage' => 'Ontbrekende afbeelding',
'UI:ToggleFullScreen' => 'Minimaliseren / Maximaliseren',
'UI:Button:ResetImage' => 'Vorige afbeelding herstellen',
'UI:Button:RemoveImage' => 'Afbeelding verwijderen',

View File

@@ -1415,6 +1415,7 @@ W przypadku powiązania z wyzwalaczem, każde działanie otrzymuje numer "porzą
'UI:SelectInlineImageToUpload' => 'Wybierz obraz do przesłania',
'UI:AvailableInlineImagesLegend' => 'Dostępne obrazy',
'UI:NoInlineImage' => 'Na serwerze nie ma obrazu. Użyj przycisku "Przeglądaj" powyżej, aby wybrać obraz ze swojego komputera i przesłać go na serwer.',
'UI:MissingInlineImage' => 'Brakujący obraz',
'UI:ToggleFullScreen' => 'Przełącz Maksymalizuj / Minimalizuj',
'UI:Button:ResetImage' => 'Odzyskaj poprzedni obraz',
'UI:Button:RemoveImage' => 'Usuń obraz',

View File

@@ -1448,6 +1448,7 @@ Quando associada a um gatilho, cada ação recebe um número de "ordem", especif
'UI:SelectInlineImageToUpload' => 'Selecione a imagem para enviar',
'UI:AvailableInlineImagesLegend' => 'Imagens disponíveis',
'UI:NoInlineImage' => 'Não há imagem disponível no servidor. Use o botão "Escolher arquivo" acima para selecionar uma imagem do seu computador e fazer o upload para o servidor',
'UI:MissingInlineImage' => 'Imagem ausente',
'UI:ToggleFullScreen' => 'Alternancia Maximizar / Minimizar',
'UI:Button:ResetImage' => 'Recupere a imagem anterior',
'UI:Button:RemoveImage' => 'Remover a imagem',

View File

@@ -1404,6 +1404,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'UI:SelectInlineImageToUpload' => 'Выберите изображение для загрузки',
'UI:AvailableInlineImagesLegend' => 'Доступные изображения',
'UI:NoInlineImage' => 'На сервере нет доступных изображений. С помощью кнопки "Обзор..." выше выберите изображение на вашем компьютере, чтобы загрузить его на сервер.',
'UI:MissingInlineImage' => 'Отсутствует изображение',
'UI:ToggleFullScreen' => 'Развернуть / Свернуть',
'UI:Button:ResetImage' => 'Восстановить предыдущее изображение',
'UI:Button:RemoveImage' => 'Удалить изображение',

View File

@@ -1405,6 +1405,7 @@ Keď sú priradené spúštačom, každej akcii je dané číslo "príkazu", šp
'UI:SelectInlineImageToUpload' => 'Select the image to upload~~',
'UI:AvailableInlineImagesLegend' => 'Available images~~',
'UI:NoInlineImage' => 'There is no image available on the server. Use the "Browse" button above to select an image from your computer and upload it to the server.~~',
'UI:MissingInlineImage' => 'Missing image~~',
'UI:ToggleFullScreen' => 'Toggle Maximize / Minimize~~',
'UI:Button:ResetImage' => 'Recover the previous image~~',
'UI:Button:RemoveImage' => 'Remove the image~~',

View File

@@ -1408,6 +1408,7 @@ Tetikleme gerçekleştiriğinde işlemler tanımlanan sıra numarası ile gerçe
'UI:SelectInlineImageToUpload' => 'Select the image to upload~~',
'UI:AvailableInlineImagesLegend' => 'Available images~~',
'UI:NoInlineImage' => 'There is no image available on the server. Use the "Browse" button above to select an image from your computer and upload it to the server.~~',
'UI:MissingInlineImage' => 'Missing image~~',
'UI:ToggleFullScreen' => 'Toggle Maximize / Minimize~~',
'UI:Button:ResetImage' => 'Recover the previous image~~',
'UI:Button:RemoveImage' => 'Remove the image~~',

View File

@@ -1405,6 +1405,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
'UI:SelectInlineImageToUpload' => '选择要上传的图片',
'UI:AvailableInlineImagesLegend' => '可用的图片',
'UI:NoInlineImage' => '服务器上没有图片. 使用上面的 "浏览" 按钮, 从您的电脑上选择并上传到服务器.',
'UI:MissingInlineImage' => '缺少图片',
'UI:ToggleFullScreen' => '切换最大化/最小化',
'UI:Button:ResetImage' => '恢复之前的图片',
'UI:Button:RemoveImage' => '移除图片',

View File

@@ -106,10 +106,6 @@ if ($oFilter != null) {
$sPageId = "ui-search-".$oFilter->GetClass();
$sLabel = MetaModel::GetName($oFilter->GetClass());
$oP->SetBreadCrumbEntry($sPageId, $sLabel, '', '', 'fas fa-search', iTopWebPage::ENUM_BREADCRUMB_ENTRY_ICON_TYPE_CSS_CLASSES);
// Menu node
$sFilter = $oFilter->ToOQL();
$oP->add("\n<!-- $sFilter -->\n");
}
$oP->add("</div>\n");
$oP->output();

View File

@@ -34,6 +34,8 @@ try {
require_once(APPROOT.'/application/startup.inc.php');
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
LoginWebPage::DoLoginEx();
IssueLog::Trace('----- Request: '.utils::GetRequestUri(), LogChannels::WEB_REQUEST);
$oPage = new DownloadPage("");
@@ -43,7 +45,6 @@ try {
switch ($operation) {
case 'download_document':
LoginWebPage::DoLoginEx('backoffice', false);
$id = utils::ReadParam('id', '');
$sField = utils::ReadParam('field', '');
if ($sClass == 'Attachment') {
@@ -63,8 +64,6 @@ try {
break;
case 'download_inlineimage':
// No login is required because the "secret" protects us
// Benefit: the inline image can be inserted into any HTML (templating = $this->html(public_log)$)
$id = utils::ReadParam('id', '');
$sSecret = utils::ReadParam('s', '');
$iCacheSec = 31556926; // One year ahead: an inline image cannot change

View File

@@ -1928,6 +1928,17 @@ EOF
$sObjClass = utils::ReadParam('obj_class', '', false, 'class');
$iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer');
// Check user has access to the object before trying to acquire the lock
$oSearch = new DBObjectSearch($sObjClass);
$oSearch->AddCondition(MetaModel::DBGetKey($sObjClass), $iObjKey, '=');
$oSet = new CMDBObjectSet($oSearch);
if (
false === $oSet->CountExceeds(0) ||
UserRights::IsActionAllowed($sObjClass, UR_ACTION_MODIFY, $oSet) !== UR_ALLOWED_YES
) {
throw new SecurityException(Dict::S('UI:ObjectDoesNotExist'));
}
$aResult = iTopOwnershipLock::AcquireLock($sObjClass, $iObjKey);
if (false === $aResult['success']) {
$aLockData = iTopOwnershipLock::IsLocked($sObjClass, $iObjKey);

View File

@@ -87,7 +87,7 @@ if (is_link($sPageEnvFullPath)) {
}
$sTargetPage = CheckPageExists($sPageEnvFullPath, $aPossibleBasePaths);
if ($sTargetPage === false) {
if ($sTargetPage === false || $sModule === 'core' || $sModule === 'dictionaries') {
// Do not recall the page parameters (security takes precedence)
echo "Wrong module, page name or environment...";
exit;
@@ -97,4 +97,35 @@ if ($sTargetPage === false) {
//
// GO!
//
// check module white list
// check conf param
// force login if needed
$aModuleDelegatedAuthenticationEndpointsList = GetModuleDelegatedAuthenticationEndpoints($sModule);
if (is_null($aModuleDelegatedAuthenticationEndpointsList)) {
$bForceLoginWhenNoDelegatedAuthenticationEndpoints = utils::GetConfig()->Get('security.force_login_when_no_delegated_authentication_endpoints_list');
if ($bForceLoginWhenNoDelegatedAuthenticationEndpoints) {
require_once(APPROOT.'/application/startup.inc.php');
LoginWebPage::DoLoginEx();
}
}
if (is_array($aModuleDelegatedAuthenticationEndpointsList) && !in_array($sPage, $aModuleDelegatedAuthenticationEndpointsList)) {
// if module defined a delegated authentication endpoints but not for the current page, we consider that the page is not allowed to be executed without login
require_once(APPROOT.'/application/startup.inc.php');
LoginWebPage::DoLoginEx();
}
if (is_null($aModuleDelegatedAuthenticationEndpointsList) && !UserRights::IsLoggedIn()) {
// check if user is not logged in, if not log a warning in the log file as the page is executed without login, which is not recommended for security reason
IssueLog::Debug("The '$sPage' page is executed without logging in. This call will be blocked in the future and will likely cause unwanted behaviour in the '$sModule' module. Please define a delegated authentication endpoint for the module, as described at https://www.itophub.io/wiki/page?id=latest:customization:new_extension#security.");
}
require_once($sTargetPage);
function GetModuleDelegatedAuthenticationEndpoints(string $sModuleName): ?array
{
$sModuleFile = utils::GetAbsoluteModulePath($sModuleName).'/module.'.$sModuleName.'.php';
require_once APPROOT.'setup/extensionsmap.class.inc.php';
$oExtensionMap = new iTopExtensionsMap();
$aModuleParam = $oExtensionMap->GetModuleInfo($sModuleFile)[2];
return $aModuleParam['delegated_authentication_endpoints'] ?? null;
}

View File

@@ -301,7 +301,7 @@ try {
//
// Read parameters
//
$iDataSourceId = ReadMandatoryParam($oP, 'data_source_id', 'raw_data');
$iDataSourceId = ReadMandatoryParam($oP, 'data_source_id', utils::ENUM_SANITIZATION_FILTER_INTEGER);
$sSynchronize = ReadParam($oP, 'synchronize');
$sSep = ReadParam($oP, 'separator', 'raw_data');
$sQualifier = ReadParam($oP, 'qualifier', 'raw_data');

View File

@@ -79,16 +79,16 @@
},
{
"name": "myclabs/deep-copy",
"version": "1.12.1",
"version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "123267b2c49fbf30d78a7b2d333f6be754b94845"
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845",
"reference": "123267b2c49fbf30d78a7b2d333f6be754b94845",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
@@ -127,7 +127,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.12.1"
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
@@ -135,20 +135,20 @@
"type": "tidelift"
}
],
"time": "2024-11-08T17:47:46+00:00"
"time": "2025-08-01T08:46:24+00:00"
},
{
"name": "nikic/php-parser",
"version": "v5.3.1",
"version": "v5.7.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "8eea230464783aa9671db8eea6f8c6ac5285794b"
"reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b",
"reference": "8eea230464783aa9671db8eea6f8c6ac5285794b",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
"reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
"shasum": ""
},
"require": {
@@ -167,7 +167,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
"dev-master": "5.x-dev"
}
},
"autoload": {
@@ -191,9 +191,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1"
"source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
},
"time": "2024-10-08T18:51:32+00:00"
"time": "2025-12-06T11:56:16+00:00"
},
{
"name": "phar-io/manifest",
@@ -634,16 +634,16 @@
},
{
"name": "phpunit/phpunit",
"version": "9.6.21",
"version": "9.6.34",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa"
"reference": "b36f02317466907a230d3aa1d34467041271ef4a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa",
"reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a",
"reference": "b36f02317466907a230d3aa1d34467041271ef4a",
"shasum": ""
},
"require": {
@@ -654,7 +654,7 @@
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
"myclabs/deep-copy": "^1.12.0",
"myclabs/deep-copy": "^1.13.4",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=7.3",
@@ -665,11 +665,11 @@
"phpunit/php-timer": "^5.0.3",
"sebastian/cli-parser": "^1.0.2",
"sebastian/code-unit": "^1.0.8",
"sebastian/comparator": "^4.0.8",
"sebastian/comparator": "^4.0.10",
"sebastian/diff": "^4.0.6",
"sebastian/environment": "^5.1.5",
"sebastian/exporter": "^4.0.6",
"sebastian/global-state": "^5.0.7",
"sebastian/exporter": "^4.0.8",
"sebastian/global-state": "^5.0.8",
"sebastian/object-enumerator": "^4.0.4",
"sebastian/resource-operations": "^3.0.4",
"sebastian/type": "^3.2.1",
@@ -717,7 +717,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21"
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34"
},
"funding": [
{
@@ -728,12 +728,20 @@
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
"type": "tidelift"
}
],
"time": "2024-09-19T10:50:18+00:00"
"time": "2026-01-27T05:45:00+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -904,16 +912,16 @@
},
{
"name": "sebastian/comparator",
"version": "4.0.8",
"version": "4.0.10",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "fa0f136dd2334583309d32b62544682ee972b51a"
"reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
"reference": "fa0f136dd2334583309d32b62544682ee972b51a",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d",
"reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d",
"shasum": ""
},
"require": {
@@ -966,15 +974,27 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8"
"source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
"type": "tidelift"
}
],
"time": "2022-09-14T12:41:17+00:00"
"time": "2026-01-24T09:22:56+00:00"
},
{
"name": "sebastian/complexity",
@@ -1164,16 +1184,16 @@
},
{
"name": "sebastian/exporter",
"version": "4.0.6",
"version": "4.0.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
"reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
"reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c",
"reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c",
"shasum": ""
},
"require": {
@@ -1229,28 +1249,40 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6"
"source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
"type": "tidelift"
}
],
"time": "2024-03-02T06:33:00+00:00"
"time": "2025-09-24T06:03:27+00:00"
},
{
"name": "sebastian/global-state",
"version": "5.0.7",
"version": "5.0.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
"reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9"
"reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
"reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6",
"reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6",
"shasum": ""
},
"require": {
@@ -1293,15 +1325,27 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/global-state/issues",
"source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7"
"source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/sebastian/global-state",
"type": "tidelift"
}
],
"time": "2024-03-02T06:35:11+00:00"
"time": "2025-08-10T07:10:35+00:00"
},
{
"name": "sebastian/lines-of-code",
@@ -1474,16 +1518,16 @@
},
{
"name": "sebastian/recursion-context",
"version": "4.0.5",
"version": "4.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
"reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1"
"reference": "539c6691e0623af6dc6f9c20384c120f963465a0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
"reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0",
"reference": "539c6691e0623af6dc6f9c20384c120f963465a0",
"shasum": ""
},
"require": {
@@ -1525,15 +1569,27 @@
"homepage": "https://github.com/sebastianbergmann/recursion-context",
"support": {
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
"source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5"
"source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
"type": "tidelift"
}
],
"time": "2023-02-03T06:07:39+00:00"
"time": "2025-08-10T06:57:39+00:00"
},
{
"name": "sebastian/resource-operations",
@@ -1738,16 +1794,16 @@
},
{
"name": "theseer/tokenizer",
"version": "1.2.3",
"version": "1.3.1",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
"reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
"reference": "b7489ce515e168639d17feec34b8847c326b0b3c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
"reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c",
"reference": "b7489ce515e168639d17feec34b8847c326b0b3c",
"shasum": ""
},
"require": {
@@ -1776,7 +1832,7 @@
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": {
"issues": "https://github.com/theseer/tokenizer/issues",
"source": "https://github.com/theseer/tokenizer/tree/1.2.3"
"source": "https://github.com/theseer/tokenizer/tree/1.3.1"
},
"funding": [
{
@@ -1784,7 +1840,7 @@
"type": "github"
}
],
"time": "2024-03-03T12:36:25+00:00"
"time": "2025-11-17T20:03:58+00:00"
}
],
"aliases": [],
@@ -1794,5 +1850,5 @@
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}

View File

@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Combodo\iTop\Test\UnitTest\Application;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Config;
use Exception;
use MetaModel;
class LoginWebPageTest extends ItopDataTestCase
{
public const USE_TRANSACTION = false;
public const PASSWORD = 'a209320P!ù;ralùqpi,pàcqi"nr';
public function setUp(): void
{
parent::setUp();
$sConfigPath = MetaModel::GetConfig()->GetLoadedFile();
$this->oConfig = new Config($sConfigPath);
$this->BackupConfiguration();
$sFolderPath = APPROOT.'env-production/extension-with-delegated-authentication-endpoints-list';
if (file_exists($sFolderPath)) {
throw new Exception("Folder $sFolderPath already exists, please remove it before running the test");
}
mkdir($sFolderPath);
$this->RecurseCopy(__DIR__.'/extension-with-delegated-authentication-endpoints-list', $sFolderPath);
$sFolderPath = APPROOT.'env-production/extension-without-delegated-authentication-endpoints-list';
if (file_exists($sFolderPath)) {
throw new Exception("Folder $sFolderPath already exists, please remove it before running the test");
}
mkdir($sFolderPath);
$this->RecurseCopy(__DIR__.'/extension-without-delegated-authentication-endpoints-list', $sFolderPath);
}
public function tearDown(): void
{
parent::tearDown();
$sFolderPath = APPROOT.'env-production/extension-with-delegated-authentication-endpoints-list';
if (file_exists($sFolderPath)) {
$this->RecurseRmdir($sFolderPath);
} else {
throw new Exception("Folder $sFolderPath does not exist, it should have been created in setUp");
}
$sFolderPath = APPROOT.'env-production/extension-without-delegated-authentication-endpoints-list';
if (file_exists($sFolderPath)) {
$this->RecurseRmdir($sFolderPath);
} else {
throw new Exception("Folder $sFolderPath does not exist, it should have been created in setUp");
}
}
protected function GivenConfigFileAllowedLoginTypes($aAllowedLoginTypes): void
{
@chmod($this->oConfig->GetLoadedFile(), 0770);
$this->oConfig->SetAllowedLoginTypes($aAllowedLoginTypes);
$this->oConfig->WriteToFile($this->oConfig->GetLoadedFile());
@chmod($this->oConfig->GetLoadedFile(), 0444);
}
/**
*
* @throws \Exception
*/
public function testInDelegatedAuthenticationEndpoints()
{
$sPageContent = $this->CallItopUri(
"pages/exec.php?exec_module=extension-with-delegated-authentication-endpoints-list&exec_page=src/Controller/FileInDelegatedAuthenticationEndpointsList.php",
[],
[],
true
);
$this->assertStringNotContainsString('<title>iTop login</title>', $sPageContent, 'File listed in delegated authentication endpoints list (in the module), login should not be requested by exec.');
}
public function testUserCanAccessAnyFile()
{
// generate random login
$sUserLogin = 'user-'.date('YmdHis');
$this->CreateUser($sUserLogin, self::$aURP_Profiles['Service Desk Agent'], self::PASSWORD);
$this->GivenConfigFileAllowedLoginTypes(explode('|', 'form'));
$sPageContent = $this->CallItopUri(
"pages/exec.php?exec_module=extension-with-delegated-authentication-endpoints-list&exec_page=src/Controller/FileNotInDelegatedAuthenticationEndpointsList.php",
[
'auth_user' => $sUserLogin,
'auth_pwd' => self::PASSWORD,
],
[],
true
);
$this->assertStringContainsString('Yo', $sPageContent, 'Logged in user should access any file via exec.php even if the page isn\'t listed in delegated authentication endpoints list');
}
public function testWithoutDelegatedAuthenticationEndpointsListWithForceLoginConf()
{
@chmod($this->oConfig->GetLoadedFile(), 0770);
$this->oConfig->Set('security.force_login_when_no_delegated_authentication_endpoints_list', true, 'AnythingButEmptyOrUnknownValue'); // 3rd param to write file even if show_in_conf_sample is false
$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",
);
$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()
{
$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, 'by default (until N°9343) if no delegated authentication endpoints list is defined, not logged in persons should access pages');
}
public function testNotInDelegatedAuthenticationEndpointsList()
{
$sPageContent = $this->CallItopUri(
"pages/exec.php?exec_module=extension-with-delegated-authentication-endpoints-list&exec_page=src/Controller/FileNotInDelegatedAuthenticationEndpointsList.php",
[],
[],
true
);
$this->assertStringContainsString('<title>iTop login</title>', $sPageContent, 'Since an delegated authentication endpoints list is defined and file isn\'t listed in it, login should be required');
}
/**
* @dataProvider InDelegatedAuthenticationEndpointsWithAdminRequiredProvider
*
* @throws \Exception
*/
public function testInDelegatedAuthenticationEndpointsWithAdminRequired($iProfileId, $bShouldSeeForbiddenAdminPage)
{
// generate random login
$sUserLogin = 'user-'.date('YmdHis');
$this->CreateUser($sUserLogin, $iProfileId, self::PASSWORD);
$this->GivenConfigFileAllowedLoginTypes(explode('|', 'form'));
$sPageContent = $this->CallItopUri(
"pages/exec.php?exec_module=extension-with-delegated-authentication-endpoints-list&exec_page=src/Controller/FileInDelegatedAuthenticationEndpointsListAndAdminRequired.php",
[
'auth_user' => $sUserLogin,
'auth_pwd' => self::PASSWORD,
],
[],
true
);
$bShouldSeeForbiddenAdminPage ?
$this->assertStringContainsString('Access restricted to people having administrator privileges', $sPageContent, 'Should prevent non admin user to access this page') : // in delegated authentication endpoints list (in the module), login should not be required
$this->assertStringContainsString('Yo !', $sPageContent, 'Should execute the file and see its content since user has admin profile');
}
public function InDelegatedAuthenticationEndpointsWithAdminRequiredProvider()
{
return [
'Administrator profile' => [
self::$aURP_Profiles['Administrator'],
'Should see forbidden admin page' => false,
],
'ReadOnly profile' => [
self::$aURP_Profiles['Service Desk Agent'],
'Should see forbidden admin page' => true,
],
];
}
}

View File

@@ -0,0 +1,51 @@
<?php
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'extension-with-delegated-authentication-endpoints-list/0.0.1',
[
// Identification
//
'label' => 'Templates foundation',
'category' => 'business',
// Setup
//
'dependencies' => [],
'mandatory' => true,
'visible' => false,
'installer' => 'TemplatesBaseInstaller',
// Security
'delegated_authentication_endpoints' => [
'src/Controller/FileInDelegatedAuthenticationEndpointsList.php',
'src/Controller/FileInDelegatedAuthenticationEndpointsListAndAdminRequired.php',
],
// Components
//
'datamodel' => [
'model.templates-base.php',
],
'webservice' => [],
'data.struct' => [// add your 'structure' definition XML files here,
],
'data.sample' => [// add your sample data XML files here,
],
// Documentation
//
'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any
'doc.more_information' => '', // hyperlink to more information, if any
// Default settings
//
'settings' => [
// Select where, in the main UI, the extra data should be displayed:
// tab (dedicated tab)
// properties (right after the properties, but before the log if any)
// none (extra data accessed only by programs)
'view_extra_data' => 'relations',
],
]
);

View File

@@ -0,0 +1,10 @@
<?php
if (UserRights::IsLoggedIn()) {
throw new Exception("User should not be authenticated at this point");
}
require_once(APPROOT.'/application/startup.inc.php');
LoginWebPage::DoLogin(true);
echo 'Yo !';

View File

@@ -0,0 +1,45 @@
<?php
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'extension-without-delegated-authentication-endpoints-list/0.0.1',
[
// Identification
//
'label' => 'Templates foundation',
'category' => 'business',
// Setup
//
'dependencies' => [],
'mandatory' => true,
'visible' => false,
'installer' => 'TemplatesBaseInstaller',
// Components
//
'datamodel' => [
'model.templates-base.php',
],
'webservice' => [],
'data.struct' => [// add your 'structure' definition XML files here,
],
'data.sample' => [// add your sample data XML files here,
],
// Documentation
//
'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any
'doc.more_information' => '', // hyperlink to more information, if any
// Default settings
//
'settings' => [
// Select where, in the main UI, the extra data should be displayed:
// tab (dedicated tab)
// properties (right after the properties, but before the log if any)
// none (extra data accessed only by programs)
'view_extra_data' => 'relations',
],
]
);

View File

@@ -9,6 +9,7 @@ namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use InlineImage;
use ormDocument;
class InlineImageTest extends ItopDataTestCase
{
@@ -98,4 +99,36 @@ HTML;
$this->assertStringContainsString(\utils::EscapeHtml(\utils::GetAbsoluteUrlAppRoot().INLINEIMAGE_DOWNLOAD_URL.'123&s=abc'), $sResult);
$this->assertStringContainsString(\utils::EscapeHtml(\utils::GetAbsoluteUrlAppRoot().INLINEIMAGE_DOWNLOAD_URL.'456&s=def'), $sResult);
}
/**
* @covers InlineImage::ReplaceInlineImagesWithBase64Representation
*/
public function testReplaceInlineImagesWithBase64Representation()
{
// create an inline image in the database
$oInlineImage = $this->createObject(InlineImage::class, [
'expire' => (new \DateTime('+1 day'))->format('Y-m-d H:i:s'),
'item_class' => 'UserRequest',
'item_id' => 999,
'item_org_id' => 1,
'contents' => new ormDocument('0x89504E470D0A1A0A0000000D494844520000000E0000000E08060000001F482DD1000000017352474200AECE1CE90000000467414D410000B18F0BFC6105000000097048597300000EC300000EC301C76FA8640000001E49444154384F63782BA3F29F1CCC802E402C1ED588078F6AC483E9AF11008B8BA9C08A7A3F290000000049454E44AE426082', 'image/png', 'square_red.png'),
'secret' => 'a94bff3ea6a872bdbc359a1704cdddb3',
]);
$sInlineImageId = $oInlineImage->GetKey();
$sInlineImageSecret = $oInlineImage->Get('secret');
// HTML with inline image
$sHtml = <<<HTML
<img src="http://host/iTop/pages/ajax.document.php?operation=download_inlineimage&amp;id=$sInlineImageId&amp;s=$sInlineImageSecret" data-img-id="$sInlineImageId" data-img-secret="$sInlineImageSecret" />
HTML;
// expected HTML with base64 representation of the image
$sExpected = <<<HTML
<img src="data:image/png;base64,MHg4OTUwNEU0NzBEMEExQTBBMDAwMDAwMEQ0OTQ4NDQ1MjAwMDAwMDBFMDAwMDAwMEUwODA2MDAwMDAwMUY0ODJERDEwMDAwMDAwMTczNTI0NzQyMDBBRUNFMUNFOTAwMDAwMDA0Njc0MTRENDEwMDAwQjE4RjBCRkM2MTA1MDAwMDAwMDk3MDQ4NTk3MzAwMDAwRUMzMDAwMDBFQzMwMUM3NkZBODY0MDAwMDAwMUU0OTQ0NDE1NDM4NEY2Mzc4MkJBM0YyOUYxQ0NDODAyRTQwMkMxRUQ1ODgwNzhGNkFDNDgzRTlBRjExMDA4QjhCQTlDMDhBN0EzRjI5MDAwMDAwMDA0OTQ1NEU0NEFFNDI2MDgy" />
HTML;
// test the method
$sResult = InlineImage::ReplaceInlineImagesWithBase64Representation($sHtml);
$this->assertEquals($sExpected, $sResult);
}
}