Compare commits

...

4 Commits

Author SHA1 Message Date
Benjamin DALSASS
18ed5d8170 N°8612 - add dictionaries entries 2026-03-05 14:52:27 +01:00
Benjamin DALSASS
c970e2ee8f N°8612 - add unitary test 2026-03-05 08:07:51 +01:00
Benjamin DALSASS
609dd78bf7 N°8612 - force authentication for inline image endpoints 2026-03-05 07:19:54 +01:00
Benjamin DALSASS
55d77a6ae0 N°8612 - base64 inline image for unauthenticated context 2026-03-05 07:18:57 +01:00
21 changed files with 95 additions and 4 deletions

View File

@@ -4344,7 +4344,9 @@ class AttributeText extends AttributeString
} else {
$sValue = self::RenderWikiHtml($sValue, true /* wiki only */);
return "<div class=\"HTML ibo-is-html-content\" $sStyle>".InlineImage::FixUrls($sValue).'</div>';
$sImageHtml = UserRights::IsLoggedIn() ? InlineImage::FixUrls($sValue) : InlineImage::ReplaceInlineImagesWithBase64Representation($sValue);
return "<div class=\"HTML ibo-is-html-content\" $sStyle>".$sImageHtml.'</div>';
}
}

View File

@@ -296,6 +296,46 @@ class InlineImage extends DBObject
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+[^>]*data-img-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('/data-img-id="\d+"\s+data-img-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

@@ -1394,6 +1394,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

@@ -1397,6 +1397,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

@@ -1394,6 +1394,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

@@ -1471,6 +1471,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

@@ -1471,6 +1471,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

@@ -1397,6 +1397,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

@@ -1398,6 +1398,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

@@ -1400,6 +1400,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

@@ -1399,6 +1399,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

@@ -1401,6 +1401,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

@@ -1400,6 +1400,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

@@ -1408,6 +1408,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

@@ -1393,6 +1393,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

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

View File

@@ -1398,6 +1398,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

@@ -1401,6 +1401,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

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

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

@@ -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);
}
}