Compare commits

..

6 Commits

29 changed files with 446 additions and 43 deletions

View File

@@ -2075,17 +2075,8 @@ class MenuBlock extends DisplayBlock
$aAllowedProfiles = MetaModel::GetConfig()->Get('concurrent_lock_override_profiles');
$bCanKill = false;
$oUser = UserRights::GetUserObject();
$aUserProfiles = [];
if (!is_null($oUser)) {
$oProfileSet = $oUser->Get('profile_list');
while ($oProfile = $oProfileSet->Fetch()) {
$aUserProfiles[$oProfile->Get('profile')] = true;
}
}
foreach ($aAllowedProfiles as $sProfile) {
if (array_key_exists($sProfile, $aUserProfiles)) {
if (UserRights::HasProfile($sProfile)) {
$bCanKill = true;
break;
}

View File

@@ -1315,8 +1315,8 @@ class utils
}
if (is_null($sAuthUser)) {
$sAuthUser = self::ReadParam('auth_user', '', 'raw_data');
$sAuthPwd = self::ReadParam('auth_pwd', '', 'raw_data');
$sAuthUser = self::ReadParam('auth_user', '', true, 'raw_data');
$sAuthPwd = self::ReadParam('auth_pwd', '', true, 'raw_data');
}
$sParamFile = self::GetParamSourceFile('auth_user');
if (is_null($sParamFile)) {
@@ -1886,6 +1886,9 @@ SQL;
$aHTTPHeaders[] = trim($sHeaderString);
}
// Default options, can be overloaded/extended with the 4th parameter of this method, see above $aCurlOptions
$iCurlAllowedProtocols = self::ConvertProtocolsToCurlOption(
self::GetConfig()->Get('security.post_requests.allowed_protocols')
);
$aOptions = [
CURLOPT_RETURNTRANSFER => true, // return the content of the request
CURLOPT_HEADER => false, // don't return the headers in the output
@@ -1908,6 +1911,9 @@ SQL;
$aAllOptions = $aCurlOptions + $aOptions;
$ch = curl_init($sUrl);
curl_setopt_array($ch, $aAllOptions);
if (!curl_setopt($ch, CURLOPT_PROTOCOLS, $iCurlAllowedProtocols)) {
throw new Exception('Unable to enforce allowed protocols for cURL request');
}
$response = curl_exec($ch);
$iErr = curl_errno($ch);
$sErrMsg = curl_error($ch);
@@ -1929,6 +1935,40 @@ SQL;
return $response;
}
private static function ConvertProtocolsToCurlOption(string $sProtocols): int
{
$aProtocols = explode(',', $sProtocols);
$iCurlProtocols = 0;
foreach ($aProtocols as $sProtocol) {
$iCurlProtocols |= match (strtolower(trim($sProtocol))) {
'dict' => defined('CURLPROTO_DICT') ? CURLPROTO_DICT : 0,
'file' => defined('CURLPROTO_FILE') ? CURLPROTO_FILE : 0,
'ftp' => defined('CURLPROTO_FTP') ? CURLPROTO_FTP : 0,
'ftps' => defined('CURLPROTO_FTPS') ? CURLPROTO_FTPS : 0,
'gopher' => defined('CURLPROTO_GOPHER') ? CURLPROTO_GOPHER : 0,
'http' => defined('CURLPROTO_HTTP') ? CURLPROTO_HTTP : 0,
'https' => defined('CURLPROTO_HTTPS') ? CURLPROTO_HTTPS : 0,
'imap' => defined('CURLPROTO_IMAP') ? CURLPROTO_IMAP : 0,
'imaps' => defined('CURLPROTO_IMAPS') ? CURLPROTO_IMAPS : 0,
'ldap' => defined('CURLPROTO_LDAP') ? CURLPROTO_LDAP : 0,
'ldaps' => defined('CURLPROTO_LDAPS') ? CURLPROTO_LDAPS : 0,
'mqtt' => defined('CURLPROTO_MQTT') ? CURLPROTO_MQTT : 0,
'pop3' => defined('CURLPROTO_POP3') ? CURLPROTO_POP3 : 0,
'pop3s' => defined('CURLPROTO_POP3S') ? CURLPROTO_POP3S : 0,
'rtsp' => defined('CURLPROTO_RTSP') ? CURLPROTO_RTSP : 0,
'scp' => defined('CURLPROTO_SCP') ? CURLPROTO_SCP : 0,
'sftp' => defined('CURLPROTO_SFTP') ? CURLPROTO_SFTP : 0,
'smtp' => defined('CURLPROTO_SMTP') ? CURLPROTO_SMTP : 0,
'smtps' => defined('CURLPROTO_SMTPS') ? CURLPROTO_SMTPS : 0,
'telnet' => defined('CURLPROTO_TELNET') ? CURLPROTO_TELNET : 0,
'tftp' => defined('CURLPROTO_TFTP') ? CURLPROTO_TFTP : 0,
default => throw new Exception("Unsupported protocol: $sProtocol"),
};
}
return $iCurlProtocols;
}
/**
* Get a standard list of character sets
*

View File

@@ -1771,6 +1771,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'security.post_requests.allowed_protocols' => [
'type' => 'string',
'description' => 'List of allowed protocols that will be used for post requests. Allowed values: dict, file, ftp, ftps, gopher, http, https, imap, imaps, ldap, ldaps, mqtt, pop3, pop3s, rtsp, scp, sftp, smtp, smtps, telnet, tftp',
'default' => 'http,https,ftp,ftps,sftp',
'value' => 'http,https,ftp,ftps,sftp',
'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

@@ -58,12 +58,7 @@ $(document).ready(function()
$(this).find('.tooltip.in').tooltip('hide');
// Set the z-index of the modal and its backdrop in case we have several modals opened
// Note: The modal is put above the highest visible one rather than computing its z-index from the number of visible modals, as a modal can still be visible
// while closing, which would put the next one too high (eg. the "Modify" button of a view modal closes it and opens the edit modal at the same time)
let zIndex = 1050;
$('.modal:visible').not(this).each(function () {
zIndex = Math.max(zIndex, (parseInt($(this).css('z-index'), 10) || 1050) + 10);
});
let zIndex = 1050 + (10 * $('.modal:visible').length);
$(this).css('z-index', zIndex);
// Set the z-index of the backdrop later because it is created after the modal
setTimeout(function() {

View File

@@ -742,11 +742,9 @@
{
$oAddon = UserRights::GetModuleInstance();
$aOrgs = $oAddon->GetUserOrgs($oUser,'Organization');
$oSet = $oUser->Get('profile_list');
$aProfiles = $oSet->GetColumnAsArray('profile');
// User is not allowed on the new Organization and has 'Portal user' Profile and is enabled
if ((count($aOrgs) > 0) && !in_array($this->Get('org_id'), $aOrgs) && in_array('Portal user',$aProfiles) && ($oUser->Get('status') === 'enabled'))
if ((count($aOrgs) > 0) && !in_array($this->Get('org_id'), $aOrgs) && UserRights::HasProfile('Portal user', $oUser) && ($oUser->Get('status') === 'enabled'))
{ // Let's block the Person modification,
// replace by $this->AddCheckWarning(...) if you don't want to block the modification
$this->AddCheckIssue(Dict::Format('Class:Person/Error:ChangingOrgDenied', $this->Get('org_id_friendlyname')));

View File

@@ -768,7 +768,7 @@ try {
// N°4129 - Prevent XSS attacks & other script executions
if (utils::GetConfig()->Get('security.disable_inline_documents_sandbox') === false) {
$oPage->add_header('Content-Security-Policy: sandbox;');
$oPage->add_header('Content-Security-Policy: sandbox; default-src \'none\'');
}
ormDocument::DownloadDocument($oPage, $sClass, $id, $sField, 'inline');

View File

@@ -346,12 +346,4 @@ class AjaxPage extends WebPage implements iTabbedPage
{
assert(false);
}
/**
* @inheritDoc
*/
public static function FilterXSS($sHTML)
{
return str_ireplace(['<script', '</script>'], ['<!-- <removed-script', '</removed-script> -->'], $sHTML);
}
}

View File

@@ -47,15 +47,9 @@ class DownloadPage extends AjaxPage
header($s_header);
}
if (($this->sContentType == 'text/html') && ($this->sContentDisposition == 'inline')) {
// inline content != attachment && html => filter all scripts for malicious XSS scripts
$sContent = self::FilterXSS($this->sContent);
} else {
$sContent = $this->sContent;
}
$oKpi->ComputeAndReport(get_class($this).' output');
echo $sContent;
$oKpi->ComputeAndReport('Echoing ('.round(strlen($sContent) / 1024).' Kb)');
echo $this->sContent;
$oKpi->ComputeAndReport('Echoing ('.round(strlen($this->sContent) / 1024).' Kb)');
ExecutionKPI::ReportStats();
}
}

View File

@@ -19,9 +19,9 @@
{% endif %}
<span class="ibo-object-details--object-class" data-role="ibo-object-details--object-class">
{% if is_backoffice_menu_enabled('DataModelMenu') %}
<a href="{{ get_absolute_url_app_root() }}pages/schema.php?operation=details_class&class={{ oUIBlock.GetClassName() }}" target="_blank" data-tooltip-content="{{ 'UI:Layout:ObjectDetails:DatamodelSchemaLink:Tooltip' | dict_s }}" data-tooltip-html-enabled="true">{{ oUIBlock.GetClassLabel() }}</a>
<a class="ibo-object-details--object-class--label" href="{{ get_absolute_url_app_root() }}pages/schema.php?operation=details_class&class={{ oUIBlock.GetClassName() }}" target="_blank" data-tooltip-content="{{ 'UI:Layout:ObjectDetails:DatamodelSchemaLink:Tooltip' | dict_s }}" data-tooltip-html-enabled="true">{{ oUIBlock.GetClassLabel() }}</a>
{% else %}
{{ oUIBlock.GetClassLabel() }}
<span class="ibo-object-details--object-class--label">{{ oUIBlock.GetClassLabel() }}</span>
{% endif %}
</span>
{{ parent() }}

View File

@@ -72,6 +72,30 @@ class utilsTest extends ItopTestCase
];
}
/**
* @dataProvider DoPostRequestAllowedProtocolsProvider
*/
public function testDoPostRequestRespectsAllowedProtocolsConfig(string $sAllowedProtocols, bool $bFileProtocolAllowed): void
{
// Config::Set changes the in-memory setting only; the test framework reloads config after this test class.
utils::GetConfig()->Set('security.post_requests.allowed_protocols', $sAllowedProtocols);
if (!$bFileProtocolAllowed) {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Problem opening URL');
} else {
$this->expectNotToPerformAssertions();
}
utils::DoPostRequest('file:///tmp', ['payload' => 'test']);
}
public function DoPostRequestAllowedProtocolsProvider(): array
{
return [
'file protocol is allowed' => ['file,http,https', true],
'file protocol is not allowed' => ['http,https', false],
];
}
/**
* @dataProvider realPathDataProvider
* @covers utils::RealPath()

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Combodo\iTop\Test\UnitTest\Module\ItopCoreUpdate;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
class CoreUpdateAvailabilityTest extends ItopDataTestCase
{
public const USE_TRANSACTION = false;
private const AUTHENTICATION_PASSWORD = 'test-password-123';
protected function setUp(): void
{
parent::setUp();
$this->BackupConfiguration();
$this->AddLoginModeAndSaveConfiguration('url');
}
public function testFunctionalAdministratorCanAccessUpgradeWhenEnabled(): void
{
$this->SetUpgradeEnabled(true);
$sMenuPage = $this->CallAsFunctionalAdministrator();
$this->assertSame(200, $this->GetLastHttpStatus());
$this->assertStringContainsString('data-menu-id="iTopUpdate"', $sMenuPage);
$this->CallAsFunctionalAdministrator('core_update.select_update_file', ['maintenance' => 'true']);
$this->assertSame(200, $this->GetLastHttpStatus());
}
public function testUpgradeMenuIsHiddenWhenDisabled(): void
{
$this->SetUpgradeEnabled(false);
$sMenuPage = $this->CallAsFunctionalAdministrator();
$this->assertSame(200, $this->GetLastHttpStatus());
$this->assertStringNotContainsString('data-menu-id="iTopUpdate"', $sMenuPage);
}
public function testDirectUpgradePageAccessIsDeniedWhenDisabled(): void
{
$this->SetUpgradeEnabled(false);
$this->CallAsFunctionalAdministrator('core_update.select_update_file', ['maintenance' => 'true']);
$this->assertSame(403, $this->GetLastHttpStatus());
}
public function testUpgradeAjaxEndpointIsDeniedWhenDisabled(): void
{
$this->SetUpgradeEnabled(false);
$this->CallAsFunctionalAdministrator('core_update_ajax.can_update_core');
$this->assertSame(403, $this->GetLastHttpStatus());
}
private function SetUpgradeEnabled(bool $bEnabled): void
{
// Expected config parameter; it is not currently declared by itop-core-update.
$this->oiTopConfig->Set('itop-core-update.enabled', $bEnabled);
$this->SaveItopConfFile();
}
private function CallAsFunctionalAdministrator(?string $sRoute = null, array $aParams = []): string
{
$sLogin = 'core-update-admin-'.bin2hex(random_bytes(6));
$this->CreateContactlessUser(
$sLogin,
self::$aURP_Profiles['Administrator'],
self::AUTHENTICATION_PASSWORD
);
$aQuery = $aParams;
if (null !== $sRoute) {
$aQuery['route'] = $sRoute;
}
$aQuery['auth_user'] = $sLogin;
$aQuery['auth_pwd'] = self::AUTHENTICATION_PASSWORD;
$sUri = 'pages/UI.php';
if ([] !== $aQuery) {
$sUri .= '?'.http_build_query($aQuery);
}
return $this->CallItopUri($sUri, [], [CURLOPT_POST => 0]);
}
private function GetLastHttpStatus(): int
{
$this->assertNotNull($this->aLastCurlGetInfo);
$this->assertArrayHasKey('http_code', $this->aLastCurlGetInfo);
return $this->aLastCurlGetInfo['http_code'];
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Combodo\iTop\Test\UnitTest\Module\ItopCoreUpdate;
use Combodo\iTop\CoreUpdate\Service\CoreUpdater;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
class SignatureValidationTest extends ItopTestCase
{
private const RESOURCES_DIRECTORY = __DIR__.'/resources/';
private string $sWorkDirectory = '';
private string $sPublicKey;
private string $sDestinationDirectory;
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('datamodels/2.x/itop-core-update/src/Service/CoreUpdater.php');
$this->sWorkDirectory = __DIR__.'/.signature-test-'.bin2hex(random_bytes(8));
mkdir($this->sWorkDirectory, 0700, true);
$this->sDestinationDirectory = $this->sWorkDirectory.'/installation';
mkdir($this->sDestinationDirectory, 0700);
$this->sPublicKey = file_get_contents(self::RESOURCES_DIRECTORY.'dummy-public.pem');
file_put_contents($this->sDestinationDirectory.'/installation-sentinel.txt', 'unchanged');
}
protected function tearDown(): void
{
static::RecurseRmdir($this->sWorkDirectory);
parent::tearDown();
}
public function testValidSignedPackageIsAccepted(): void
{
// Expected API: verify with an injected test public key before extracting to the supplied destination.
CoreUpdater::ValidateAndExtractDownloadedFile(
self::RESOURCES_DIRECTORY.'dummy-signed.zip',
$this->sPublicKey,
$this->sDestinationDirectory
);
$this->assertSame(
file_get_contents(self::RESOURCES_DIRECTORY.'package/file1'),
file_get_contents($this->sDestinationDirectory.'/file1')
);
}
public function testPackageIsRejectedBeforeExtractionWhenManifestWasAlteredAfterSigning(): void
{
$this->AssertRejectedBeforeExtraction(self::RESOURCES_DIRECTORY.'dummy-altered-manifest.zip');
}
public function testPackageIsRejectedBeforeExtractionWhenCoveredFileWasAltered(): void
{
$this->AssertRejectedBeforeExtraction(self::RESOURCES_DIRECTORY.'dummy-altered-content.zip');
}
public function testPackageWithInvalidSignatureIsRejectedBeforeExtraction(): void
{
$this->AssertRejectedBeforeExtraction(self::RESOURCES_DIRECTORY.'dummy-badly-signed.zip');
}
public function testPackageWithoutSignatureIsRejectedBeforeExtraction(): void
{
$this->AssertRejectedBeforeExtraction(self::RESOURCES_DIRECTORY.'dummy-unsigned.zip');
}
public function testPackageWithoutManifestIsRejectedBeforeExtraction(): void
{
$this->AssertRejectedBeforeExtraction(self::RESOURCES_DIRECTORY.'dummy-missing-manifest.zip');
}
private function AssertRejectedBeforeExtraction(string $sArchivePath): void
{
$aInstallationBefore = $this->SnapshotDirectory($this->sDestinationDirectory);
$bRejected = false;
try {
CoreUpdater::ValidateAndExtractDownloadedFile(
$sArchivePath,
$this->sPublicKey,
$this->sDestinationDirectory
);
} catch (\UnexpectedValueException $e) {
$bRejected = true;
}
$this->assertTrue($bRejected, 'The invalid update package was not rejected.');
$this->assertSame($aInstallationBefore, $this->SnapshotDirectory($this->sDestinationDirectory));
$this->assertFileDoesNotExist($this->sDestinationDirectory.'/file1');
}
private function SnapshotDirectory(string $sDirectory): array
{
$aSnapshot = [];
$oIterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($sDirectory, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($oIterator as $oFile) {
if ($oFile->isFile()) {
$sRelativePath = substr($oFile->getPathname(), strlen($sDirectory) + 1);
$aSnapshot[$sRelativePath] = hash_file('sha256', $oFile->getPathname());
}
}
ksort($aSnapshot);
return $aSnapshot;
}
}

View File

@@ -0,0 +1,52 @@
<?php
// Usage: php build-package.php <content-dir> <private-key.pem> <output.zip>
[$script, $contentDir, $privateKeyPath, $outputZip] = $argv;
$contentDir = rtrim(realpath($contentDir), '/');
$manifestName = 'manifest.xml';
$signatureName = 'manifest.xml.sig'; // TODO: use the name the module expects
// 1. List files (excluding manifest and signature), sorted for a stable output
$files = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($contentDir, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
$relativePath = substr($file->getPathname(), strlen($contentDir) + 1);
if (in_array($relativePath, [$manifestName, $signatureName], true)) {
continue;
}
$files[] = $relativePath;
}
sort($files);
// 2. Build manifest.xml in your format
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<files>\n";
foreach ($files as $relativePath) {
$fullPath = "$contentDir/$relativePath";
$xml .= sprintf(
"<file><path>%s</path><md5>%s</md5><size>%d</size></file>\n",
htmlspecialchars($relativePath, ENT_XML1),
md5_file($fullPath),
filesize($fullPath)
);
}
$xml .= "</files>\n";
file_put_contents("$contentDir/$manifestName", $xml);
// 3. Sign the manifest
$privateKey = openssl_pkey_get_private(file_get_contents($privateKeyPath));
openssl_sign($xml, $signature, $privateKey, OPENSSL_ALGO_SHA256);
file_put_contents("$contentDir/$signatureName", $signature); // or base64_encode($signature)
// 4. Zip everything
$zip = new ZipArchive();
$zip->open($outputZip, ZipArchive::CREATE | ZipArchive::OVERWRITE);
foreach (array_merge($files, [$manifestName, $signatureName]) as $relativePath) {
$zip->addFile("$contentDir/$relativePath", $relativePath);
}
$zip->close();
echo "Built $outputZip with ".count($files)." files\n";

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDYyavpa+/92vWV
vm8VnqthJJ6J6cWZbeD/amvWQg3754Az66Gr4Lf0mTD+7iWbKzjcqANyDLOug3+J
Hpxaj0ogx04lBi+MqFZZfYydD1S/xjrNwF4UnE0KfSPF5ZqgDjhJqhtevWlo7lxq
KoI6902bmv1XVP8we+aUANQeCI0OI3VeMcIVZ76yEz7BDyrQ1t7Dj7H44gsR4MFQ
P4K5QAHJ6+zW36K8/oiIzasvQ5bTVgVyx4UIlBHbw5YRpNdieX+NUFHQnMDUiZ0y
1beJHujF24LUrmqAL95S3fADurxEsuDtTUow27z0ZLJ4vxmsGaGKYZT/9ZYJoHdu
ux91Xdf9AgMBAAECggEAKqsO5gNzmfBVs+JmdMflDVqqxZQ4aylgRoVIskQQAouf
BRkUmgLB0iewqAxxZGgOage7nd/4IZoKKQ/YUE6xlWAE9m6QP0wGqth2K5eP46r9
ee+p1L6qvyzANLwrQeNqmr0bY/s92wRHNjIlrbttD/picKTvETjhujGrjjm6oZRm
MwOvCJ+/NLOHQbSfmlVhEFQTwH+efCiEvegQYBeLDgBEx/K8K5W4KxbUSnbk0oAM
TFEqmuGpSr11uN25W1I2OEKF0Ow2heEv9n9tDgdg5JbiX67HeSpGbrKs7bD6MpAh
mrTEHKv6EC6q9RZEtJRnIHFRqMQd1HDzlGSdjebPFQKBgQD3+bG0522x0tU6qmXo
gPZMS9iQlCDFZ3q899y3Peu3+ThZAG3MFj1pmBbl2whjuC65EF6cW//O04jqGl6b
jNUwUgqcJMywes9JDQ2WWWQp81JAfyQAJ8gL5PHazi/ffK1B03Z9y47dQNy2UIAl
4BvhWS2CBYeoPsdYWFbnpHLgFwKBgQDfzZwNzv7qgkREuVNRTAcBxcnnVMVrndXp
3JFFDu3lt/JL+B0rxESwLkcgcEA3EUrY8y5Iq7NbnS6doVmZeCxi3sXbY/fEkyEy
PHVmAUzHChxcsupZrOWoIxQzNbP8kJB22v9ocKaV1FfBVkkJ2WzWYjcl+DscKZ6X
/EoZ4JbhCwKBgGhMiLcrRBMwN2lmdFiXrYCnwh4Ff8cn4BMNObOFAtktpMqiiIv3
qrr4MmFZgCzZMiW6weCIPY8ToBm/H53TXXPmffnw7HsqHHeAWv+q95tBcY4ZK944
3UZLdIJwiOg6O3FdbRA6Bwv+lRbFwzCrE270IFT42HcmeJgrB9qEa6DfAoGAJP9G
dvnrJjVWt82zOlNJLT/XOsjGwh2biUePNE1dgoZAJdLkb6HIgR+FXpVv6CqRpv+8
fQL1aYW4eRJ9T2OQYt/U8Ym7IeH4sOdRuQUp/hVrzt884oo0hPEXm8ea83uqhht5
8Xf98fLidvGerWQmG+KsnzhD/UncD+ADF/gKuTUCgYAWM5ulgrgdo/ceum1JzMsh
qRDoFN1ktXE8oZtoOvtWAECEzMk51QiuxCYikyE5jqg2Vys3LBZXqTvuyPzL/HDJ
oB734PY8hoI1BYrJ0dKjQCPfImX2VMJQZ+Yk/21UaBwOIdoGDC1NVLNBlvjvjEEv
IWp2vMPzX4CDAee+86uMjg==
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Mmr6Wvv/dr1lb5vFZ6r
YSSeienFmW3g/2pr1kIN++eAM+uhq+C39Jkw/u4lmys43KgDcgyzroN/iR6cWo9K
IMdOJQYvjKhWWX2MnQ9Uv8Y6zcBeFJxNCn0jxeWaoA44SaobXr1paO5caiqCOvdN
m5r9V1T/MHvmlADUHgiNDiN1XjHCFWe+shM+wQ8q0Nbew4+x+OILEeDBUD+CuUAB
yevs1t+ivP6IiM2rL0OW01YFcseFCJQR28OWEaTXYnl/jVBR0JzA1ImdMtW3iR7o
xduC1K5qgC/eUt3wA7q8RLLg7U1KMNu89GSyeL8ZrBmhimGU//WWCaB3brsfdV3X
/QIDAQAB
-----END PUBLIC KEY-----

View File

@@ -0,0 +1 @@
nicer content for file 2

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<files>
<file><path>file1</path><md5>e7d0f0aa46a74bd08e0f0f268a19812b</md5><size>15</size></file>
<file><path>folder1/file1</path><md5>f0d7b0cc26724909f6cb17f55040e054</md5><size>23</size></file>
<file><path>folder1/file2</path><md5>1696bef05836b4787a86a6497781e161</md5><size>24</size></file>
<file><path>folder2/file1</path><md5>d9966d42983e134e3f5249a5f2be66f7</md5><size>14</size></file>
</files>

View File

@@ -4,6 +4,7 @@ namespace Combodo\iTop\Test\UnitTest\Pages;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Dict;
use ormDocument;
use UserLocal;
use UserRequest;
@@ -132,6 +133,47 @@ class AjaxRenderTest extends ItopDataTestCase
return $this->AcquireLockAsUser(self::$sLogin, self::$iTicketId);
}
public function testDisplayDocumentHasSandboxWhenConfigurationDoesNotDisableIt(): void
{
$sLogin = uniqid('AjaxRenderTest');
$this->CreateContactlessUser($sLogin, self::$aURP_Profiles['Administrator'], self::AUTHENTICATION_PASSWORD);
$iDocumentId = $this->GivenObjectInDB('DocumentFile', [
'name' => 'AjaxRenderTest_'.uniqid(),
'org_id' => $this->getTestOrgId(),
'file' => new ormDocument('<svg xmlns="http://www.w3.org/2000/svg"></svg>', 'image/svg+xml', 'test.svg'),
]);
$this->oiTopConfig->Set('security.disable_inline_documents_sandbox', false);
$this->SaveItopConfFile();
$sResponseHeaders = $this->GetDocumentResponseHeaders($sLogin, $iDocumentId);
$this->assertStringContainsString(
'sandbox',
$sResponseHeaders
);
}
private function GetDocumentResponseHeaders(string $sLogin, int $iDocumentId): string
{
$sResponse = $this->CallItopUri(
'pages/ajax.render.php?'.http_build_query([
'operation' => 'display_document',
'class' => 'DocumentFile',
'id' => $iDocumentId,
'field' => 'file',
'auth_user' => $sLogin,
'auth_pwd' => self::AUTHENTICATION_PASSWORD,
]),
[],
[
CURLOPT_HTTPHEADER => ['X-Combodo-Ajax:1'],
CURLOPT_HEADER => true,
CURLOPT_POST => 0,
]
);
return substr($sResponse, 0, $this->aLastCurlGetInfo['header_size']);
}
// Helper method to create a user with a specific profile
private function CreateUserWithProfile(int $iProfileId): UserLocal
{

View File

@@ -116,7 +116,7 @@ class DataSynchroTest extends ItopDataTestCase
* @throws \MySQLException
* @throws \OQLException
*/
public function RunDataSynchroTest($aUserLoginUsecase)
public function RunDataSynchroTest($aUserLoginUsecase, array $aAdditionalImportParams = [])
{
$sDescription = $aUserLoginUsecase['desc'];
$sTargetClass = $aUserLoginUsecase['target_class'];
@@ -273,13 +273,13 @@ class DataSynchroTest extends ItopDataTestCase
$sCSVTmpFile = tempnam(sys_get_temp_dir(), "CSV");
file_put_contents($sCSVTmpFile, $sCsvData);
$aParams = [
$aParams = array_merge($aAdditionalImportParams, [
'csvfile' => $sCSVTmpFile,
'data_source_id' => $iDataSourceId,
'separator' => ';',
'simulate' => 0,
'output' => 'details',
];
]);
list($iRetCode, $aOutputLines) = static::ExecSynchroImport($aParams, $bSynchroByHttp);
unlink($sCSVTmpFile);
@@ -390,6 +390,11 @@ class DataSynchroTest extends ItopDataTestCase
$this->RunDataSynchroTest($this->GetNominalUsecaseData());
}
public function testDataSynchroByCliWithMaxChunkSize()
{
$this->RunDataSynchroTest($this->GetNominalUsecaseData(), ['max_chunk_size' => 1 ]);
}
public function testDataSynchroByHttp()
{
$aUserLoginUsecase = $this->GetNominalUsecaseData();