mirror of
https://github.com/Combodo/iTop.git
synced 2026-10-23 00:38:56 +02:00
Compare commits
6 Commits
issue/9239
...
support/3.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da4a810534 | ||
|
|
6865c845f0 | ||
|
|
530c7b2ddd | ||
|
|
692c0723ab | ||
|
|
23619ec6cd | ||
|
|
0ba1bf829c |
@@ -1202,7 +1202,6 @@ class DesignerComboField extends DesignerFormField
|
||||
{
|
||||
$sId = $this->oForm->GetFieldId($this->sCode);
|
||||
$sName = $this->oForm->GetFieldName($this->sCode);
|
||||
$sChecked = $this->defaultValue ? 'checked' : '';
|
||||
$sMandatory = $this->bMandatory ? 'true' : 'false';
|
||||
$sReadOnly = $this->IsReadOnly() ? 'disabled="disabled"' : '';
|
||||
if ($this->IsSorted()) {
|
||||
@@ -1245,8 +1244,7 @@ class DesignerComboField extends DesignerFormField
|
||||
} else {
|
||||
$sSelected = ($sKey == $this->defaultValue) ? 'selected' : '';
|
||||
}
|
||||
// Quick and dirty: display the menu parents as a tree
|
||||
$sHtmlValue = str_replace(' ', ' ', $sDisplayValue);
|
||||
$sHtmlValue = $this->PrepareValueForRendering($sDisplayValue);
|
||||
$sHtml .= "<option value=\"".utils::EscapeHtml($sKey)."\" $sSelected>$sHtmlValue</option>";
|
||||
}
|
||||
$sHtml .= "</select></span>";
|
||||
@@ -1263,6 +1261,14 @@ EOF
|
||||
|
||||
}
|
||||
|
||||
protected function PrepareValueForRendering($sValue = null): ?string
|
||||
{
|
||||
if (is_null($sValue)) {
|
||||
$sValue = $this->defaultValue;
|
||||
}
|
||||
return $sValue;
|
||||
}
|
||||
|
||||
public function ReadParam(&$aValues)
|
||||
{
|
||||
parent::ReadParam($aValues);
|
||||
|
||||
@@ -1747,6 +1747,14 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'security.disable_mandatory_authentication_for_inline_images' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'If true then image access does not require user authentication, provided the image\'s secret is valid. Note that setting this to true gives a less secure access to inline images!',
|
||||
'default' => false,
|
||||
'value' => false,
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'security.disable_joined_classes_filter' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'If true, scope filters aren\'t applied to joined classes or union classes not directly listed in the SELECT clause.',
|
||||
|
||||
@@ -722,6 +722,32 @@
|
||||
</field>
|
||||
</fields>
|
||||
<methods>
|
||||
<method id="IsContactEmailEditAllowed">
|
||||
<static>false</static>
|
||||
<access>public</access>
|
||||
<type>Helper</type>
|
||||
<code><![CDATA[ public function IsContactEmailEditAllowed(): bool
|
||||
{
|
||||
if (!UserRights::IsLoggedIn() || $this->IsNew()
|
||||
|| UserRights::IsAdministrator()
|
||||
|| UserRights::HasProfile('SuperUser')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$oLinkedUsers = new DBObjectSet(
|
||||
DBObjectSearch::FromOQL_AllData('SELECT User WHERE contactid = :contact_id'),
|
||||
[],
|
||||
['contact_id' => $this->GetKey()]
|
||||
);
|
||||
if ($oLinkedUsers->Count() === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$oCurrentContact = UserRights::GetContactObject();
|
||||
return !is_null($oCurrentContact) && ($oCurrentContact->GetKey() == $this->GetKey());
|
||||
}
|
||||
]]></code>
|
||||
</method>
|
||||
<method id="CheckUsersOnUpdate">
|
||||
<static>false</static>
|
||||
<access>public</access>
|
||||
@@ -730,6 +756,9 @@
|
||||
{
|
||||
// This method can block the Person modification, by adding a Check Issue
|
||||
$aChanges = $this->ListChanges();
|
||||
if (array_key_exists('email', $aChanges) && !$this->IsContactEmailEditAllowed()) {
|
||||
$this->AddCheckIssue(Dict::S('UI:Login:Error:AccessRestricted'));
|
||||
}
|
||||
// Current User may not be allowed to see User class, so we can't use $this->Get('user_list')
|
||||
$oSearch = new DBObjectSearch('User');
|
||||
$oSearch->AddCondition('contactid', $this->GetKey(), '=');
|
||||
@@ -799,6 +828,10 @@
|
||||
<type>Overload-DBObject</type>
|
||||
<code><![CDATA[ public function GetAttributeFlags($sAttCode, &$aReasons = array(), $sTargetState = '')
|
||||
{
|
||||
if (($sAttCode === 'email') && !$this->IsContactEmailEditAllowed()) {
|
||||
return parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState) | OPT_ATT_READONLY; // e.g. OPT_ATT_SLAVE
|
||||
}
|
||||
|
||||
if ( ($sAttCode == 'org_id') && (!$this->IsNew()) )
|
||||
{
|
||||
if (MetaModel::GetConfig()->Get('demo_mode'))
|
||||
|
||||
@@ -104,8 +104,8 @@ $(function()
|
||||
$(document).on('click', function(){
|
||||
me._cleanAllMenus();
|
||||
});
|
||||
var oMenu =
|
||||
$('<ul class="menu-icon-select ibo-input-select-icon--menu">');
|
||||
var oMenu = $('<ul class="menu-icon-select ibo-input-select-icon--menu">');
|
||||
this.oButton.data('icon-select-menu', oMenu[0]);
|
||||
for(var i in this.options.items)
|
||||
{
|
||||
var oItem = $('<li class="ibo-input-select-icon--menu--item"><div value="'+i+'"><img class="ibo-input-select-icon--menu--icon" src="'+this.options.items[i].icon+'" style="vertical-align: middle;"/>'+this.options.items[i].label+'<div></li>');
|
||||
|
||||
@@ -64,7 +64,9 @@ try {
|
||||
break;
|
||||
|
||||
case 'download_inlineimage':
|
||||
LoginWebPage::DoLoginEx();
|
||||
if (MetaModel::GetConfig()->Get('security.disable_mandatory_authentication_for_inline_images') === false) {
|
||||
LoginWebPage::DoLoginEx();
|
||||
}
|
||||
$id = utils::ReadParam('id', '');
|
||||
$sSecret = utils::ReadParam('s', '');
|
||||
$iCacheSec = 31556926; // One year ahead: an inline image cannot change
|
||||
|
||||
@@ -78,6 +78,11 @@ XML,
|
||||
</root>
|
||||
HTML
|
||||
],
|
||||
'DesignerComboField should not escape html characters to preserve indentation made with ' => [
|
||||
'\\DesignerComboField',
|
||||
' Foo & Bar',
|
||||
' Foo & Bar',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
188
tests/php-unit-tests/unitary-tests/core/ContactEmailTest.php
Normal file
188
tests/php-unit-tests/unitary-tests/core/ContactEmailTest.php
Normal file
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
// Copyright (c) 2010-2024 Combodo SAS
|
||||
//
|
||||
// This file is part of iTop.
|
||||
//
|
||||
// iTop is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// iTop is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
//
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Core;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use CoreCannotSaveObjectException;
|
||||
use DBObject;
|
||||
use MetaModel;
|
||||
use UserRights;
|
||||
|
||||
/**
|
||||
* @group userRights
|
||||
*/
|
||||
class ContactEmailTest extends ItopDataTestCase
|
||||
{
|
||||
public function testLinkedUserCanUpdateOwnContactEmailViaDbUpdate(): void
|
||||
{
|
||||
$this->CreateTestOrganization();
|
||||
$oContact = $this->CreatePerson(201);
|
||||
$this->CreateAndLoginContactEmailTestUser('contact-email-linked-', 3, $oContact);
|
||||
|
||||
$oContact->Set('email', 'linked-user@example.com');
|
||||
$oContact->DBUpdate();
|
||||
|
||||
$this->assertSame('linked-user@example.com', MetaModel::GetObject('Contact', $oContact->GetKey(), false, true)->Get('email'));
|
||||
}
|
||||
|
||||
public function testAdministratorCanUpdateAnotherContactEmailViaDbUpdate(): void
|
||||
{
|
||||
$this->CreateTestOrganization();
|
||||
$oTargetContact = $this->CreatePerson(202);
|
||||
$this->CreateUser(uniqid('contact-email-admin-target-'), 3, 'A1b!Passw0rd', $oTargetContact->GetKey());
|
||||
$oAdministratorContact = $this->CreatePerson(203);
|
||||
$this->CreateAndLoginContactEmailTestUser('contact-email-admin-', 1, $oAdministratorContact);
|
||||
|
||||
$oTargetContact->Set('email', 'administrator@example.com');
|
||||
$oTargetContact->DBUpdate();
|
||||
|
||||
$this->assertSame('administrator@example.com', MetaModel::GetObject('Contact', $oTargetContact->GetKey(), false, true)->Get('email'));
|
||||
}
|
||||
|
||||
public function testSuperUserCanUpdateAnotherContactEmailViaDbUpdate(): void
|
||||
{
|
||||
$this->CreateTestOrganization();
|
||||
$oTargetContact = $this->CreatePerson(204);
|
||||
$this->CreateUser(uniqid('contact-email-superuser-target-'), 3, 'A1b!Passw0rd', $oTargetContact->GetKey());
|
||||
$oSuperUserContact = $this->CreatePerson(205);
|
||||
$oSuperUserProfile = MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", ['name' => 'SuperUser'], true);
|
||||
$this->assertNotNull($oSuperUserProfile, 'The SuperUser profile must be available for this test');
|
||||
$this->CreateAndLoginContactEmailTestUser('contact-email-superuser-', $oSuperUserProfile->GetKey(), $oSuperUserContact);
|
||||
|
||||
$oTargetContact->Set('email', 'superuser@example.com');
|
||||
$oTargetContact->DBUpdate();
|
||||
|
||||
$this->assertSame('superuser@example.com', MetaModel::GetObject('Contact', $oTargetContact->GetKey(), false, true)->Get('email'));
|
||||
}
|
||||
|
||||
public function testConfigurationManagerCannotUpdateAnotherUsersContactEmail(): void
|
||||
{
|
||||
$this->CreateTestOrganization();
|
||||
$oTargetContact = $this->CreatePerson(206);
|
||||
$this->CreateUser(uniqid('contact-email-target-'), 3, 'A1b!Passw0rd', $oTargetContact->GetKey());
|
||||
$oConfigurationManagerContact = $this->CreatePerson(207);
|
||||
$this->CreateAndLoginContactEmailTestUser('contact-email-config-manager-', 3, $oConfigurationManagerContact);
|
||||
|
||||
try {
|
||||
(new \CoreServices())->ExecOperation('1.3', 'core/update', (object) [
|
||||
'comment' => 'Update another user contact email',
|
||||
'class' => 'Person',
|
||||
'key' => $oTargetContact->GetKey(),
|
||||
'fields' => (object) ['email' => 'forbidden@example.com'],
|
||||
'output_fields' => 'email',
|
||||
]);
|
||||
$this->fail('A configuration manager must not change the email of a contact linked to another user');
|
||||
} catch (CoreCannotSaveObjectException $e) {
|
||||
$this->assertNotEmpty($e->getIssues());
|
||||
}
|
||||
|
||||
$this->assertSame('', MetaModel::GetObject('Contact', $oTargetContact->GetKey(), false, true)->Get('email'));
|
||||
}
|
||||
|
||||
public function testConfigurationManagerCanUpdateContactEmailWhenNoUserIsLinked(): void
|
||||
{
|
||||
$this->CreateTestOrganization();
|
||||
$oTargetContact = $this->CreatePerson(208);
|
||||
$oConfigurationManagerContact = $this->CreatePerson(209);
|
||||
$this->CreateAndLoginContactEmailTestUser('contact-email-config-manager-unlinked-', 3, $oConfigurationManagerContact);
|
||||
|
||||
$oResult = (new \CoreServices())->ExecOperation('1.3', 'core/update', (object) [
|
||||
'comment' => 'Update contact email without linked user',
|
||||
'class' => 'Person',
|
||||
'key' => $oTargetContact->GetKey(),
|
||||
'fields' => (object) ['email' => 'unlinked-contact@example.com'],
|
||||
'output_fields' => 'email',
|
||||
]);
|
||||
|
||||
$this->assertSame(\RestResult::OK, $oResult->code);
|
||||
$this->assertSame('unlinked-contact@example.com', MetaModel::GetObject('Contact', $oTargetContact->GetKey(), false, true)->Get('email'));
|
||||
}
|
||||
|
||||
public function testConfigurationManagerCanUpdateAnotherContactWhenRestRequestIncludesUnchangedEmail(): void
|
||||
{
|
||||
$this->CreateTestOrganization();
|
||||
$oTargetContact = $this->CreatePerson(210);
|
||||
$this->CreateUser(uniqid('contact-rest-target-'), 3, 'A1b!Passw0rd', $oTargetContact->GetKey());
|
||||
$oTargetContact->Set('email', 'existing@example.com');
|
||||
$oTargetContact->DBUpdate();
|
||||
$oConfigurationManagerContact = $this->CreatePerson(211);
|
||||
$this->CreateAndLoginContactEmailTestUser('contact-rest-config-manager-', 3, $oConfigurationManagerContact);
|
||||
|
||||
$oResult = (new \CoreServices())->ExecOperation('1.3', 'core/update', (object) [
|
||||
'comment' => 'Update contact phone',
|
||||
'class' => 'Person',
|
||||
'key' => $oTargetContact->GetKey(),
|
||||
'fields' => (object) [
|
||||
'email' => 'existing@example.com',
|
||||
'phone' => '555-0100',
|
||||
],
|
||||
'output_fields' => 'email, phone',
|
||||
]);
|
||||
|
||||
$this->assertSame(\RestResult::OK, $oResult->code);
|
||||
$oUpdatedContact = MetaModel::GetObject('Person', $oTargetContact->GetKey(), false, true);
|
||||
$this->assertSame('existing@example.com', $oUpdatedContact->Get('email'));
|
||||
$this->assertSame('555-0100', $oUpdatedContact->Get('phone'));
|
||||
}
|
||||
|
||||
public function testConfigurationManagerCanCreatePersonContact(): void
|
||||
{
|
||||
$this->CreateTestOrganization();
|
||||
$oConfigurationManagerContact = $this->CreatePerson(212);
|
||||
$this->CreateAndLoginContactEmailTestUser('contact-create-config-manager-', 3, $oConfigurationManagerContact);
|
||||
|
||||
$oCreatedContact = $this->CreatePerson(213);
|
||||
|
||||
$this->assertGreaterThan(0, $oCreatedContact->GetKey());
|
||||
$this->assertNotNull(MetaModel::GetObject('Person', $oCreatedContact->GetKey(), false, true));
|
||||
}
|
||||
|
||||
public function testContactlessConfigurationManagerCanCreatePersonAndTeamWithEmptyEmail(): void
|
||||
{
|
||||
$oOrganization = $this->CreateTestOrganization();
|
||||
$oUser = $this->CreateContactlessUser(uniqid('contactless-config-manager-'), 3, 'A1b!Passw0rd');
|
||||
$_SESSION = [];
|
||||
UserRights::Login($oUser->Get('login'));
|
||||
$this->assertNull(UserRights::GetContactObject());
|
||||
|
||||
$oPerson = $this->createObject('Person', [
|
||||
'name' => 'Person_without_email',
|
||||
'first_name' => 'Test',
|
||||
'org_id' => $oOrganization->GetKey(),
|
||||
]);
|
||||
$oTeam = $this->createObject('Team', [
|
||||
'name' => 'Team_without_email',
|
||||
'org_id' => $oOrganization->GetKey(),
|
||||
]);
|
||||
|
||||
$this->assertGreaterThan(0, $oPerson->GetKey());
|
||||
$this->assertGreaterThan(0, $oTeam->GetKey());
|
||||
$this->assertSame('', MetaModel::GetObject('Person', $oPerson->GetKey(), false, true)->Get('email'));
|
||||
$this->assertSame('', MetaModel::GetObject('Team', $oTeam->GetKey(), false, true)->Get('email'));
|
||||
}
|
||||
|
||||
private function CreateAndLoginContactEmailTestUser(string $sLoginPrefix, int $iProfileId, DBObject $oContact): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
$oUser = $this->CreateUser(uniqid($sLoginPrefix), $iProfileId, 'A1b!Passw0rd', $oContact->GetKey());
|
||||
UserRights::Login($oUser->Get('login'));
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
<?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'];
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<?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";
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,28 +0,0 @@
|
||||
-----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-----
|
||||
@@ -1,9 +0,0 @@
|
||||
-----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-----
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
that's a file 1
|
||||
@@ -1 +0,0 @@
|
||||
nice content for file 1
|
||||
@@ -1 +0,0 @@
|
||||
nicer content for file 2
|
||||
@@ -1 +0,0 @@
|
||||
another file 1
|
||||
@@ -1,7 +0,0 @@
|
||||
<?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>
|
||||
Binary file not shown.
Reference in New Issue
Block a user