diff --git a/core/config.class.inc.php b/core/config.class.inc.php index f584b1d82..8dbc72ab4 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -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)', diff --git a/core/inlineimage.class.inc.php b/core/inlineimage.class.inc.php index a632045b1..252b425fa 100644 --- a/core/inlineimage.class.inc.php +++ b/core/inlineimage.class.inc.php @@ -298,6 +298,46 @@ N°8681 * @param string|null $sHtml The HTML fragment to process return $sHtml; } + /** + * Replace 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( + '/]*'.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 = ''.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 diff --git a/datamodels/2.x/itop-backup/module.itop-backup.php b/datamodels/2.x/itop-backup/module.itop-backup.php index 35fd85f0b..24c69bdc7 100644 --- a/datamodels/2.x/itop-backup/module.itop-backup.php +++ b/datamodels/2.x/itop-backup/module.itop-backup.php @@ -41,6 +41,11 @@ SetupWebPage::AddModule( 'doc.manual_setup' => '', 'doc.more_information' => '', + // Security + 'delegated_authentication_endpoints' => [ + 'ajax.backup.php', + ], + // Default settings // 'settings' => [ diff --git a/datamodels/2.x/itop-hub-connector/module.itop-hub-connector.php b/datamodels/2.x/itop-hub-connector/module.itop-hub-connector.php index 70521511c..4f6702167 100644 --- a/datamodels/2.x/itop-hub-connector/module.itop-hub-connector.php +++ b/datamodels/2.x/itop-hub-connector/module.itop-hub-connector.php @@ -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 diff --git a/dictionaries/cs.dictionary.itop.ui.php b/dictionaries/cs.dictionary.itop.ui.php index 84141d603..235f38ab2 100755 --- a/dictionaries/cs.dictionary.itop.ui.php +++ b/dictionaries/cs.dictionary.itop.ui.php @@ -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', diff --git a/dictionaries/da.dictionary.itop.ui.php b/dictionaries/da.dictionary.itop.ui.php index 91f1d43cd..b1e85c86e 100644 --- a/dictionaries/da.dictionary.itop.ui.php +++ b/dictionaries/da.dictionary.itop.ui.php @@ -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~~', diff --git a/dictionaries/de.dictionary.itop.ui.php b/dictionaries/de.dictionary.itop.ui.php index 35b75eccf..1f6bff387 100644 --- a/dictionaries/de.dictionary.itop.ui.php +++ b/dictionaries/de.dictionary.itop.ui.php @@ -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', diff --git a/dictionaries/en.dictionary.itop.ui.php b/dictionaries/en.dictionary.itop.ui.php index e13ade60b..1f0e3101a 100644 --- a/dictionaries/en.dictionary.itop.ui.php +++ b/dictionaries/en.dictionary.itop.ui.php @@ -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', diff --git a/dictionaries/en_gb.dictionary.itop.ui.php b/dictionaries/en_gb.dictionary.itop.ui.php index 7407ca13d..231cca9d0 100644 --- a/dictionaries/en_gb.dictionary.itop.ui.php +++ b/dictionaries/en_gb.dictionary.itop.ui.php @@ -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', diff --git a/dictionaries/es_cr.dictionary.itop.ui.php b/dictionaries/es_cr.dictionary.itop.ui.php index 451a59ae2..f692144ec 100644 --- a/dictionaries/es_cr.dictionary.itop.ui.php +++ b/dictionaries/es_cr.dictionary.itop.ui.php @@ -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', diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php index 9fe2da13c..69487d97f 100644 --- a/dictionaries/fr.dictionary.itop.ui.php +++ b/dictionaries/fr.dictionary.itop.ui.php @@ -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', diff --git a/dictionaries/hu.dictionary.itop.ui.php b/dictionaries/hu.dictionary.itop.ui.php index 884ff0c93..af27ebd5c 100755 --- a/dictionaries/hu.dictionary.itop.ui.php +++ b/dictionaries/hu.dictionary.itop.ui.php @@ -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', diff --git a/dictionaries/it.dictionary.itop.ui.php b/dictionaries/it.dictionary.itop.ui.php index 075e089e8..b6f5e6fa0 100644 --- a/dictionaries/it.dictionary.itop.ui.php +++ b/dictionaries/it.dictionary.itop.ui.php @@ -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', diff --git a/dictionaries/ja.dictionary.itop.ui.php b/dictionaries/ja.dictionary.itop.ui.php index 37964ac57..aa603d6e7 100644 --- a/dictionaries/ja.dictionary.itop.ui.php +++ b/dictionaries/ja.dictionary.itop.ui.php @@ -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~~', diff --git a/dictionaries/nl.dictionary.itop.ui.php b/dictionaries/nl.dictionary.itop.ui.php index 848bf5ac1..aef5eb85e 100644 --- a/dictionaries/nl.dictionary.itop.ui.php +++ b/dictionaries/nl.dictionary.itop.ui.php @@ -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', diff --git a/dictionaries/pl.dictionary.itop.ui.php b/dictionaries/pl.dictionary.itop.ui.php index 11ec06a73..cd3489a8c 100644 --- a/dictionaries/pl.dictionary.itop.ui.php +++ b/dictionaries/pl.dictionary.itop.ui.php @@ -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', diff --git a/dictionaries/pt_br.dictionary.itop.ui.php b/dictionaries/pt_br.dictionary.itop.ui.php index 5a7e30a96..529f1a64f 100644 --- a/dictionaries/pt_br.dictionary.itop.ui.php +++ b/dictionaries/pt_br.dictionary.itop.ui.php @@ -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', diff --git a/dictionaries/ru.dictionary.itop.ui.php b/dictionaries/ru.dictionary.itop.ui.php index c39a9ff61..0a024ff48 100644 --- a/dictionaries/ru.dictionary.itop.ui.php +++ b/dictionaries/ru.dictionary.itop.ui.php @@ -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' => 'Удалить изображение', diff --git a/dictionaries/sk.dictionary.itop.ui.php b/dictionaries/sk.dictionary.itop.ui.php index 38778104b..35c8ed924 100644 --- a/dictionaries/sk.dictionary.itop.ui.php +++ b/dictionaries/sk.dictionary.itop.ui.php @@ -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~~', diff --git a/dictionaries/tr.dictionary.itop.ui.php b/dictionaries/tr.dictionary.itop.ui.php index 5ae1c4fe5..b74e3f264 100644 --- a/dictionaries/tr.dictionary.itop.ui.php +++ b/dictionaries/tr.dictionary.itop.ui.php @@ -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~~', diff --git a/dictionaries/zh_cn.dictionary.itop.ui.php b/dictionaries/zh_cn.dictionary.itop.ui.php index ae73e3484..ffd02dd33 100644 --- a/dictionaries/zh_cn.dictionary.itop.ui.php +++ b/dictionaries/zh_cn.dictionary.itop.ui.php @@ -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' => '移除图片', diff --git a/pages/UniversalSearch.php b/pages/UniversalSearch.php index f2d9b2913..97f8cb7e2 100644 --- a/pages/UniversalSearch.php +++ b/pages/UniversalSearch.php @@ -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\n"); } $oP->add("\n"); $oP->output(); diff --git a/pages/ajax.document.php b/pages/ajax.document.php index 3320c298f..69c9d0a03 100644 --- a/pages/ajax.document.php +++ b/pages/ajax.document.php @@ -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 diff --git a/pages/ajax.render.php b/pages/ajax.render.php index 622d641f1..3e8888c86 100644 --- a/pages/ajax.render.php +++ b/pages/ajax.render.php @@ -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); diff --git a/pages/exec.php b/pages/exec.php index 093a5e9ff..a0a10109e 100644 --- a/pages/exec.php +++ b/pages/exec.php @@ -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; +} diff --git a/synchro/synchro_import.php b/synchro/synchro_import.php index 3fca25021..f7763ebaa 100644 --- a/synchro/synchro_import.php +++ b/synchro/synchro_import.php @@ -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'); diff --git a/tests/php-unit-tests/composer.lock b/tests/php-unit-tests/composer.lock index d1ac7cfdc..f0d69db66 100644 --- a/tests/php-unit-tests/composer.lock +++ b/tests/php-unit-tests/composer.lock @@ -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" } diff --git a/tests/php-unit-tests/integration-tests/login-tests/LoginWebPageTest.php b/tests/php-unit-tests/integration-tests/login-tests/LoginWebPageTest.php new file mode 100644 index 000000000..dd35754ec --- /dev/null +++ b/tests/php-unit-tests/integration-tests/login-tests/LoginWebPageTest.php @@ -0,0 +1,177 @@ +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('iTop login', $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('iTop login', $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('iTop login', $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, + ], + ]; + } +} diff --git a/tests/php-unit-tests/integration-tests/login-tests/extension-with-delegated-authentication-endpoints-list/module.extension-with-delegated-authentication-endpoints-list.php b/tests/php-unit-tests/integration-tests/login-tests/extension-with-delegated-authentication-endpoints-list/module.extension-with-delegated-authentication-endpoints-list.php new file mode 100644 index 000000000..34493f366 --- /dev/null +++ b/tests/php-unit-tests/integration-tests/login-tests/extension-with-delegated-authentication-endpoints-list/module.extension-with-delegated-authentication-endpoints-list.php @@ -0,0 +1,51 @@ + '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', + ], + ] +); diff --git a/tests/php-unit-tests/integration-tests/login-tests/extension-with-delegated-authentication-endpoints-list/src/Controller/FileInDelegatedAuthenticationEndpointsList.php b/tests/php-unit-tests/integration-tests/login-tests/extension-with-delegated-authentication-endpoints-list/src/Controller/FileInDelegatedAuthenticationEndpointsList.php new file mode 100644 index 000000000..825c9d411 --- /dev/null +++ b/tests/php-unit-tests/integration-tests/login-tests/extension-with-delegated-authentication-endpoints-list/src/Controller/FileInDelegatedAuthenticationEndpointsList.php @@ -0,0 +1,3 @@ + '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', + ], + ] +); diff --git a/tests/php-unit-tests/integration-tests/login-tests/extension-without-delegated-authentication-endpoints-list/src/Controller/File.php b/tests/php-unit-tests/integration-tests/login-tests/extension-without-delegated-authentication-endpoints-list/src/Controller/File.php new file mode 100644 index 000000000..825c9d411 --- /dev/null +++ b/tests/php-unit-tests/integration-tests/login-tests/extension-without-delegated-authentication-endpoints-list/src/Controller/File.php @@ -0,0 +1,3 @@ +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; + + // expected HTML with base64 representation of the image + $sExpected = << +HTML; + + // test the method + $sResult = InlineImage::ReplaceInlineImagesWithBase64Representation($sHtml); + $this->assertEquals($sExpected, $sResult); + } }