Compare commits

...

90 Commits

Author SHA1 Message Date
jf-cbd
056dce4d78 Merge remote-tracking branch 'origin/support/2.7' into support/3.1
# Conflicts:
#	webservices/rest.php
2025-03-06 16:12:15 +01:00
jf-cbd
063bb9680e N°8231 - Better variable fallback 2025-03-06 16:09:51 +01:00
jf-cbd
1142bf327c Fix tests 2025-03-06 14:54:18 +01:00
jf-cbd
278496eaf6 Update tests for 3.1 2025-03-06 14:40:39 +01:00
jf-cbd
77ba0b398f Fix merge conflict 2025-03-06 12:11:20 +01:00
jf-cbd
04ca7bf603 Merge remote-tracking branch 'origin/support/2.7' into support/3.1
# Conflicts:
#	core/restservices.class.inc.php
#	datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php
#	tests/php-unit-tests/unitary-tests/core/Delta/delta_test_sanitize_output.xml
#	tests/php-unit-tests/unitary-tests/core/RestServicesSanitizeOutputTest.php
#	tests/php-unit-tests/unitary-tests/core/RestServicesTest.php
#	webservices/rest.php
2025-03-06 12:10:00 +01:00
jf-cbd
8f8ac46f55 N°8215 - When PHP warning are enabled, Global Request doesn't work 2025-03-06 11:59:08 +01:00
denis.flaven@combodo.com
07b904ee1b N°8231 - making rest api logs more readable 2025-03-06 11:59:08 +01:00
jf-cbd
be8d348b25 N°8231 - Update tests 2025-03-03 16:49:27 +01:00
jf-cbd
6c7a98fe3d Merge remote-tracking branch 'origin/support/2.7' into support/3.1 2025-03-03 16:38:32 +01:00
denis.flaven@combodo.com
ec2203229b N°8231 - making rest api log more readable 2025-03-03 16:23:25 +01:00
jf-cbd
da4457f5b4 Merge remote-tracking branch 'origin/support/2.7' into support/3.1 2025-03-03 14:36:50 +01:00
denis.flaven@combodo.com
97848cea4f N°8231 - making rest api log more readable 2025-03-03 11:38:52 +01:00
jf-cbd
94d6eca0c1 N°8215 - When PHP warning are enabled, Global Request doesn't work 2025-02-21 13:20:27 +01:00
jf-cbd
355da8ec0a Merge remote-tracking branch 'origin/support/2.7' into support/3.1
# Conflicts:
#	datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php
2025-02-21 13:18:19 +01:00
jf-cbd
5f006c45db N°8215 - When PHP warning are enabled, Global Request doesn't work 2025-02-21 13:16:07 +01:00
denis.flaven@combodo.com
46aaeb4301 Merge branch 'support/2.7' into support/3.1 2025-02-07 10:24:18 +01:00
denis.flaven@combodo.com
affed69999 Version number bump. 2025-02-07 10:09:48 +01:00
denis.flaven@combodo.com
64a216e0f6 Merge branch 'support/2.7' into support/3.1 2025-01-31 17:13:09 +01:00
denis.flaven@combodo.com
d5754fc568 N°8135 - Bump datamodel version. 2025-01-31 17:04:56 +01:00
jf-cbd
ccb1ca9d79 Merge remote-tracking branch 'origin/support/2.7' into support/3.1 2025-01-28 10:32:18 +01:00
jf-cbd
44290db312 N°8134 - Portal user profile is broken, regression from 7776 2025-01-28 10:23:44 +01:00
Eric Espie
025af923ea N°8131 - Issue on DBlinkchange event when object is delete 2025-01-24 14:23:50 +01:00
Eric Espie
858b12abaa N°8131 - Issue on DBlinkchange event when object is delete 2025-01-24 11:59:51 +01:00
Eric Espie
a7bc4bd411 Add new measure points for KPI logger 2025-01-22 16:34:31 +01:00
jf-cbd
a07f66c061 Merge remote-tracking branch 'origin/support/2.7' into support/3.1 2025-01-21 16:48:33 +01:00
jf-cbd
c49ceae75e Fix HandleForm call 2025-01-21 16:46:15 +01:00
jf-cbd
4da975cb64 Merge remote-tracking branch 'origin/support/2.7' into support/3.1 2025-01-21 12:15:06 +01:00
jf-cbd
8980f627e9 Fix format 2025-01-21 12:09:06 +01:00
jf-cbd
ec61b52238 N°7776 remove twig from ajax calls, 3.1 edition 2025-01-20 16:02:26 +01:00
jf-cbd
072596a53b Merge remote-tracking branch 'origin/support/2.7' into support/3.1
# Conflicts:
#	datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php
2025-01-20 15:53:34 +01:00
jf-cbd
160bfd714b N°7776 remove twig from ajax calls 2025-01-20 15:41:22 +01:00
jf-cbd
1c5cb1547f Merge remote-tracking branch 'origin/support/2.7' into support/3.1 2025-01-16 17:15:12 +01:00
jf-cbd
8d58372074 Update unattended installation script documentation 2025-01-16 17:13:26 +01:00
odain
5780f26817 N°7810: fix merge 2024-12-27 11:13:31 +01:00
odain
343f3286b8 Merge branch 'support/2.7' into support/3.1 2024-12-27 09:08:47 +01:00
Eric Espie
37fc1a5723 N°7810 - security hardening 2024-12-27 09:04:28 +01:00
jf-cbd
1fa50f695d Security hardening 2024-12-16 10:47:06 +01:00
jf-cbd
692cf4f635 Merge branch 'support/2.7' into support/3.1 2024-12-16 10:27:00 +01:00
jf-cbd
95aa444ee6 Security hardening 2024-12-13 16:48:13 +01:00
jf-cbd
f5de808c7c Security hardening (#685)
* security hardening
2024-12-13 15:09:18 +01:00
jf-cbd
cbb4281a37 N°7980 - security hardening 2024-11-29 16:40:34 +01:00
Benjamin Dalsass
06dcae1dd1 Merge remote-tracking branch 'origin/support/2.7' into support/3.1 2024-11-27 09:50:57 +01:00
Benjamin Dalsass
e03033ce52 N°7219 - Fatal error following dashboard modification when dashboard title contains an é 2024-11-27 09:40:22 +01:00
Karel Vlk
c70d62a51e 🐛 N°7916 SF#2274 EmailLaminas.php: Keep charset with part header in multipart email (#672)
* 🐛 N°2274 EmailLaminas.php: Keep charset with part header in multipart email

* Add a unit test

---------

Co-authored-by: Stephen Abello <stephen.abello@combodo.com>
2024-11-08 09:38:57 +01:00
jf-cbd
0d5ff261fe Merge remote-tracking branch 'origin/support/2.7' into support/3.1 2024-11-07 14:51:32 +01:00
jf-cbd
374b35f78a 🚀 Fix GitHub action 2024-11-07 14:50:46 +01:00
Stephen Abello
9371bc6d7b N°7925 Fix incorrectly formatted In-Reply-To email header 2024-11-04 14:38:31 +01:00
Karel Vlk
58e964fb8c 🐛 N°7917 SF#2272 EmailLaminas.php: Fix Message-ID format (#671)
* 🐛 N°2272 EmailLaminas.php: Fix Message-ID format

* EmailLaminas.php: Add MessageId import

as suggested by @steffunky in PR 671
2024-11-04 14:37:15 +01:00
jf-cbd
e8ecc85828 Merge remote-tracking branch 'origin/support/2.7' into support/3.1 2024-10-22 16:09:02 +02:00
jf-cbd
04bd8cc5ce 🚀 Update GitHub actions to improve PR classification 2024-10-22 16:07:47 +02:00
Eric Espie
88756a443a Fix event listeners display when the listener is a static method 2024-10-14 09:25:05 +02:00
denis.flaven@combodo.com
052e2a1a42 🔖 Prepare 3.1.2 version 2024-10-03 12:14:32 +02:00
Eric Espie
eb1ecff7d8 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2024-09-26 17:37:43 +02:00
Eric Espie
8141723869 Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	approot.inc.php
#	css/css-variables.scss
#	datamodels/2.x/authent-cas/module.authent-cas.php
#	datamodels/2.x/authent-external/module.authent-external.php
#	datamodels/2.x/authent-ldap/module.authent-ldap.php
#	datamodels/2.x/authent-local/module.authent-local.php
#	datamodels/2.x/combodo-db-tools/module.combodo-db-tools.php
#	datamodels/2.x/itop-attachments/module.itop-attachments.php
#	datamodels/2.x/itop-backup/module.itop-backup.php
#	datamodels/2.x/itop-bridge-virtualization-storage/module.itop-bridge-virtualization-storage.php
#	datamodels/2.x/itop-change-mgmt-itil/module.itop-change-mgmt-itil.php
#	datamodels/2.x/itop-change-mgmt/module.itop-change-mgmt.php
#	datamodels/2.x/itop-config-mgmt/module.itop-config-mgmt.php
#	datamodels/2.x/itop-config/module.itop-config.php
#	datamodels/2.x/itop-core-update/module.itop-core-update.php
#	datamodels/2.x/itop-datacenter-mgmt/module.itop-datacenter-mgmt.php
#	datamodels/2.x/itop-endusers-devices/module.itop-endusers-devices.php
#	datamodels/2.x/itop-files-information/module.itop-files-information.php
#	datamodels/2.x/itop-full-itil/module.itop-full-itil.php
#	datamodels/2.x/itop-hub-connector/module.itop-hub-connector.php
#	datamodels/2.x/itop-incident-mgmt-itil/module.itop-incident-mgmt-itil.php
#	datamodels/2.x/itop-knownerror-mgmt/module.itop-knownerror-mgmt.php
#	datamodels/2.x/itop-oauth-client/module.itop-oauth-client.php
#	datamodels/2.x/itop-portal-base/module.itop-portal-base.php
#	datamodels/2.x/itop-portal/module.itop-portal.php
#	datamodels/2.x/itop-problem-mgmt/module.itop-problem-mgmt.php
#	datamodels/2.x/itop-profiles-itil/module.itop-profiles-itil.php
#	datamodels/2.x/itop-request-mgmt-itil/module.itop-request-mgmt-itil.php
#	datamodels/2.x/itop-request-mgmt/module.itop-request-mgmt.php
#	datamodels/2.x/itop-service-mgmt-provider/module.itop-service-mgmt-provider.php
#	datamodels/2.x/itop-service-mgmt/module.itop-service-mgmt.php
#	datamodels/2.x/itop-sla-computation/module.itop-sla-computation.php
#	datamodels/2.x/itop-storage-mgmt/module.itop-storage-mgmt.php
#	datamodels/2.x/itop-tickets/module.itop-tickets.php
#	datamodels/2.x/itop-virtualization-mgmt/module.itop-virtualization-mgmt.php
#	datamodels/2.x/itop-welcome-itil/module.itop-welcome-itil.php
#	datamodels/2.x/version.xml
2024-09-26 17:37:07 +02:00
denis.flaven@combodo.com
8cb701bda3 🔖 Prepare 2.7.11 version 2024-09-26 16:53:24 +02:00
jf-cbd
1b29746806 Rename github token 2024-09-23 17:14:41 +02:00
jf-cbd
fb9c317256 Add an action in the workflow to automatically add pull requests to the Combodo PRs dashboard 2024-09-23 14:43:33 +02:00
Molkobain
8c704951e1 N°7730 - Rename constant as it is actually for ID and class selectors, not only IDs 2024-08-19 18:41:32 +02:00
Eric Espie
24c23628d6 N°7730 - code hardening 2024-08-19 15:15:22 +02:00
jf-cbd
141c22ff67 Fix selectize bug when multiple selectize fields exist on the same page 2024-07-17 14:17:09 +02:00
Thomas Casteleyn
1e3c425e81 N°7645 - PHP 8.1: Fix usage of strpos() & str_replace() with null value when compiling empty dictionary (#600)
* fix(compiler): provide empty string instead of null value

* Apply review suggestions
2024-07-10 10:07:12 +02:00
Benjamin Dalsass
b2e3fb6354 N°7024 - Fix opening an object with abstract class indirect linked set in Portal 2024-07-05 09:10:19 +02:00
jf-cbd
45ce68e16e Merge remote-tracking branch 'refs/remotes/origin/support/3.0' into support/3.1 2024-07-04 13:55:11 +02:00
jf-cbd
1aef576403 N°7604 - Security hardening 2024-07-04 13:52:19 +02:00
jf-cbd
bfd9be8280 Merge remote-tracking branch 'refs/remotes/origin/support/3.0' into support/3.1 2024-07-04 10:59:57 +02:00
jf-cbd
96e1388dde N°7603 - Security hardening + UI blocks examples updated 2024-07-04 10:56:08 +02:00
Timothee
86b48b8980 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2024-07-03 17:00:22 +02:00
Timothee
69c8791fc5 Fix merge conflit resolution d3b9965283 2024-07-03 16:48:08 +02:00
Eric Espie
5fd8678a3a N°7619 - Object deletion not cascaded to legacy extensions 2024-07-02 10:38:25 +02:00
Eric Espie
86df9ac035 N°7619 - Object deletion not cascaded to legacy extensions 2024-07-02 10:37:36 +02:00
Molkobain
3f997b416f N°7313 - Revert "fix" as it breaks grouping on ext. keys, OQL functions and more
We keep the enhancements to the test framework though.
2024-06-28 14:54:51 +02:00
Eric Espie
cceb6809e7 Fix CI 2024-06-24 14:20:33 +02:00
Eric Espie
48b559472e Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts:
#	tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php
2024-06-24 14:02:04 +02:00
Eric Espie
cddc452693 Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2024-06-24 13:55:29 +02:00
Eric Espie
0904a21e3f Cleanup ItopTestCase 2024-06-24 11:50:37 +02:00
Timothee
66230199f8 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2024-06-21 12:41:17 +02:00
Timothee
1f1a2b660f N°7581 Improve error message readability during object creation/modification in the portal (regression introduced with N°7545) 2024-06-21 12:36:52 +02:00
Molkobain
4f36369ea1 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2024-06-21 11:32:21 +02:00
Molkobain
33a906f11a Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2024-06-21 11:29:18 +02:00
Molkobain
82d11eeb47 N°7127 - Upgrade handlebars.js to v4.7.8 2024-06-21 11:19:39 +02:00
Eric Espie
b8bf66031b Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2024-06-20 11:07:57 +02:00
Eric Espie
2596a150bf Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2024-06-20 11:07:36 +02:00
Eric Espie
142d6c8993 N°7533 - Detect and warns on Galera clusters 2024-06-20 11:06:57 +02:00
Timothee
d70e5dff45 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2024-06-17 16:55:26 +02:00
Timothee
c4fc0ed982 Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2024-06-17 16:51:30 +02:00
Timothee
320922a13d N°7545 Correctly display error message 2024-06-17 16:49:33 +02:00
Eric Espie
f96d28a0a8 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2024-06-12 16:48:39 +02:00
Eric Espie
d3b9965283 Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	core/cmdbsource.class.inc.php
2024-06-12 16:48:06 +02:00
Eric Espie
f03d731b1d N°7533 - Prevent installation of iTop on Galera clusters 2024-06-12 16:14:23 +02:00
91 changed files with 1614 additions and 766 deletions

43
.github/workflows/action.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Add PRs to Combodo PRs Dashboard
on:
pull_request_target:
types:
- opened
jobs:
add-to-project:
name: Add PR to Combodo Project
runs-on: ubuntu-latest
steps:
- name: Check if author is a member of the organization
id: check-membership
run: |
ORG="Combodo"
AUTHOR=$(jq -r .pull_request.user.login "$GITHUB_EVENT_PATH")
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}" \
"https://api.github.com/orgs/$ORG/members/$AUTHOR")
if [ "$RESPONSE" == "404" ]; then
echo "project_url=https://github.com/orgs/Combodo/projects/5" >> $GITHUB_ENV
echo "is_member=false" >> $GITHUB_ENV
else
echo "project_url=https://github.com/orgs/Combodo/projects/4" >> $GITHUB_ENV
echo "is_member=true" >> $GITHUB_ENV
fi
- name: Add internal tag if member
if: env.is_member == 'true'
run: |
curl -X POST -H "Authorization: token ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/Combodo/iTop/issues/${{ github.event.pull_request.number }}/labels \
-d '{"labels":["internal"]}'
env:
is_member: ${{ env.is_member }}
- name: Add PR to the appropriate project
uses: actions/add-to-project@v1.0.2
with:
project-url: ${{ env.project_url }}
github-token: ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}

View File

@@ -1715,6 +1715,11 @@ interface iRestServiceProvider
public function ExecOperation($sVersion, $sVerb, $aParams);
}
interface iRestInputSanitizer
{
public function SanitizeJsonInput(string $sJsonInput): string;
}
/**
* Minimal REST response structure. Derive this structure to add response data and error codes.
*
@@ -1806,6 +1811,14 @@ class RestResult
* @api
*/
public $message;
/**
* Sanitize the content of this result to hide sensitive information
*/
public function SanitizeContent()
{
// The default implementation does nothing
}
}
/**

View File

@@ -1115,7 +1115,9 @@ HTML
// Note: DisplayBareHeader is called before adding $oObjectDetails to the page, so it can inject HTML before it through $oPage.
/** @var \iTopWebPage $oPage */
$oKPI = new ExecutionKPI();
$aHeadersBlocks = $this->DisplayBareHeader($oPage, $bEditMode);
$oKPI->ComputeStatsForExtension($this, 'DisplayBareHeader');
if (false === empty($aHeadersBlocks['subtitle'])) {
$oObjectDetails->AddSubTitleBlocks($aHeadersBlocks['subtitle']);
}
@@ -1128,8 +1130,12 @@ HTML
$oPage->AddTabContainer(OBJECT_PROPERTIES_TAB, '', $oObjectDetails);
$oPage->SetCurrentTabContainer(OBJECT_PROPERTIES_TAB);
$oPage->SetCurrentTab('UI:PropertiesTab');
$oKPI = new ExecutionKPI();
$this->DisplayBareProperties($oPage, $bEditMode);
$oKPI->ComputeStatsForExtension($this, 'DisplayBareProperties');
$oKPI = new ExecutionKPI();
$this->DisplayBareRelations($oPage, $bEditMode);
$oKPI->ComputeStatsForExtension($this, 'DisplayBareRelations');
// Note: Adding the JS snippet which enables the image upload should have been done directly by the ActivityPanel which would have kept the independance principle
@@ -4583,6 +4589,8 @@ HTML;
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$sExtensionClass = get_class($oExtensionInstance);
$this->LogCRUDDebug(__METHOD__, "Calling $sExtensionClass::OnDBInsert()");
$oKPI = new ExecutionKPI();
$oExtensionInstance->OnDBInsert($oNewObj, self::GetCurrentChange());
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBInsert');
@@ -4664,7 +4672,22 @@ HTML;
return $oDeletionPlan;
}
protected function PostDeleteActions(): void
final protected function PreDeleteActions(): void
{
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$sExtensionClass = get_class($oExtensionInstance);
$this->LogCRUDDebug(__METHOD__, "Calling $sExtensionClass::OnDBDelete()");
$oKPI = new ExecutionKPI();
$oExtensionInstance->OnDBDelete($this, self::GetCurrentChange());
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBDelete');
}
parent::PreDeleteActions();
}
final protected function PostDeleteActions(): void
{
parent::PostDeleteActions();
}
@@ -4678,6 +4701,8 @@ HTML;
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$sExtensionClass = get_class($oExtensionInstance);
$this->LogCRUDDebug(__METHOD__, "Calling $sExtensionClass::OnDBDelete()");
$oKPI = new ExecutionKPI();
$oExtensionInstance->OnDBDelete($this, self::GetCurrentChange());
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBDelete');
@@ -4699,6 +4724,7 @@ HTML;
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$sExtensionClass = get_class($oExtensionInstance);
$this->LogCRUDDebug(__METHOD__, "Calling $sExtensionClass::OnIsModified()");
$oKPI = new ExecutionKPI();
$bIsModified = $oExtensionInstance->OnIsModified($this);
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnIsModified');
@@ -4758,6 +4784,8 @@ HTML;
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$sExtensionClass = get_class($oExtensionInstance);
$this->LogCRUDDebug(__METHOD__, "Calling $sExtensionClass::OnCheckToWrite()");
$oKPI = new ExecutionKPI();
$aNewIssues = $oExtensionInstance->OnCheckToWrite($this);
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnCheckToWrite');
@@ -4808,6 +4836,8 @@ HTML;
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$sExtensionClass = get_class($oExtensionInstance);
$this->LogCRUDDebug(__METHOD__, "Calling $sExtensionClass::OnCheckToDelete()");
$oKPI = new ExecutionKPI();
$aNewIssues = $oExtensionInstance->OnCheckToDelete($this);
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnCheckToDelete');
@@ -6076,7 +6106,9 @@ JS
// We want to avoid launching the listener twice, first here, and secondly after saving the Ticket in the listener
// By disabling the event to be fired, we can remove the current object from the attribute !
$oObject = MetaModel::GetObject($sClass, $sId, false);
self::FireEventDbLinksChangedForObject($oObject);
if (!is_null($oObject)) {
self::FireEventDbLinksChangedForObject($oObject);
}
self::RemoveObjectAwaitingEventDbLinksChanged($sClass, $sId);
}
@@ -6084,13 +6116,11 @@ JS
{
self::SetEventDBLinksChangedBlocked(true);
// N°6408 The object can have been deleted
if (!is_null($oObject)) {
$oObject->FireEvent(EVENT_DB_LINKS_CHANGED);
$oObject->FireEvent(EVENT_DB_LINKS_CHANGED);
// Update the object if needed
if (count($oObject->ListChanges()) !== 0) {
$oObject->DBUpdate();
}
// Update the object if needed
if (count($oObject->ListChanges()) !== 0) {
$oObject->DBUpdate();
}
cmdbAbstractObject::SetEventDBLinksChangedBlocked(false);
}

View File

@@ -1264,12 +1264,12 @@ EOF
$sOkButtonLabel = Dict::S('UI:Button:Save');
$sCancelButtonLabel = Dict::S('UI:Button:Cancel');
$sId = utils::HtmlEntities($this->sId);
$sLayoutClass = utils::HtmlEntities($this->sLayoutClass);
$sId = json_encode($this->sId);
$sLayoutClass = json_encode($this->sLayoutClass);
$sAutoReload = $this->bAutoReload ? 'true' : 'false';
$sAutoReloadSec = (string) $this->iAutoReloadSec;
$sTitle = utils::HtmlEntities($this->sTitle);
$sFile = utils::HtmlEntities($this->GetDefinitionFile());
$sTitle = json_encode($this->sTitle);
$sFile = json_encode($this->GetDefinitionFile());
$sUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
$sReloadURL = $this->GetReloadURL();
@@ -1325,15 +1325,15 @@ $('#dashboard_editor').dialog({
});
$('#dashboard_editor .ui-layout-center').runtimedashboard({
dashboard_id: '$sId',
layout_class: '$sLayoutClass',
title: '$sTitle',
dashboard_id: $sId,
layout_class: $sLayoutClass,
title: $sTitle,
auto_reload: $sAutoReload,
auto_reload_sec: $sAutoReloadSec,
submit_to: '$sUrl',
submit_parameters: {operation: 'save_dashboard', file: '$sFile', extra_params: $sJSExtraParams, reload_url: '$sReloadURL'},
submit_parameters: {operation: 'save_dashboard', file: $sFile, extra_params: $sJSExtraParams, reload_url: '$sReloadURL'},
render_to: '$sUrl',
render_parameters: {operation: 'render_dashboard', file: '$sFile', extra_params: $sJSExtraParams, reload_url: '$sReloadURL'},
render_parameters: {operation: 'render_dashboard', file: $sFile, extra_params: $sJSExtraParams, reload_url: '$sReloadURL'},
new_dashlet_parameters: {operation: 'new_dashlet'}
});

View File

@@ -704,7 +704,7 @@ class DisplayBlock
if ($bDoSearch)
{
// Keep the table_id identifying this table if we're performing a search
$sTableId = utils::ReadParam('_table_id_', null, false, 'raw_data');
$sTableId = utils::ReadParam('_table_id_', null, false, utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
if ($sTableId != null)
{
$aExtraParams['table_id'] = $sTableId;
@@ -1684,19 +1684,16 @@ JS
$aRes = CMDBSource::QueryToArray($sSql);
$oContext = new ApplicationContext();
$sContextParam = $oContext->GetForLink();
/** @var AttributeDefinition $oGroupByAttDef */
$oGroupByAttDef = $aGroupBy["grouped_by_1"]->GetAttDef();
$iTotalCount = 0;
$aURLs = array();
foreach ($aRes as $iRow => $aRow) {
$sValue = $aRow['grouped_by_1'];
$sPlainTextValue = $oGroupByAttDef->GetValueLabel($sValue);
$sHtmlValue = utils::EscapeHtml($sPlainTextValue);
$sHtmlValue = $oGroupByExp->MakeValueLabel($this->m_oFilter, $sValue, $sValue);
$iTotalCount += $aRow['_itop_count_'];
$aValues[] = array(
'label' => $sPlainTextValue,
'label' => html_entity_decode(strip_tags($sHtmlValue), ENT_QUOTES, 'UTF-8'),
'label_html' => $sHtmlValue,
'value' => (float)$aRow[$sFctVar],
);

View File

@@ -60,6 +60,24 @@ class CoreCannotSaveObjectException extends CoreException
return $sContent;
}
public function getTextMessage()
{
$sTitle = Dict::S('UI:Error:SaveFailed');
$sContent = utils::HtmlEntities($sTitle);
if (count($this->aIssues) == 1) {
$sIssue = reset($this->aIssues);
$sContent .= utils::HtmlEntities($sIssue);
} else {
foreach ($this->aIssues as $sError) {
$sContent .= " ".utils::HtmlEntities($sError).", ";
}
}
return $sContent;
}
public function getIssues()
{
return $this->aIssues;

View File

@@ -109,6 +109,11 @@ class utils
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER = 'element_identifier';
/**
* @var string For XML / HTML node id/class selector
* @since 3.1.2 3.2.1
*/
public const ENUM_SANITIZATION_FILTER_ELEMENT_SELECTOR = 'element_selector';
/**
* @var string For variables names
* @since 3.0.0
@@ -489,8 +494,17 @@ class utils
}
break;
// For XML / HTML node identifiers
case static::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER:
$retValue = preg_replace('/[^a-zA-Z0-9_-]/', '', $value);
$retValue = filter_var($retValue, FILTER_VALIDATE_REGEXP,
['options' => ['regexp' => '/^[A-Za-z0-9][A-Za-z0-9_-]*$/']]);
break;
// For XML / HTML node id selector
case static::ENUM_SANITIZATION_FILTER_ELEMENT_SELECTOR:
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP,
['options' => ['regexp' => '/^[#\.][A-Za-z0-9][A-Za-z0-9_-]*$/']]);
break;
case static::ENUM_SANITIZATION_FILTER_VARIABLE_NAME:

View File

@@ -23,7 +23,7 @@ define('ITOP_DESIGN_LATEST_VERSION', '3.1');
* @used-by utils::GetItopVersionWikiSyntax()
* @used-by iTopModulesPhpVersionIntegrationTest
*/
define('ITOP_CORE_VERSION', '3.1.1');
define('ITOP_CORE_VERSION', '3.1.3');
/**
* @var string

View File

@@ -136,7 +136,7 @@ abstract class AttributeDefinition
protected $aCSSClasses;
public function GetType()
public function GetType()
{
return Dict::S('Core:'.get_class($this));
}
@@ -4164,7 +4164,7 @@ class AttributeFinalClass extends AttributeString
*/
class AttributePassword extends AttributeString implements iAttributeNoGroupBy
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
/**
* Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329)
@@ -4241,7 +4241,7 @@ class AttributePassword extends AttributeString implements iAttributeNoGroupBy
*/
class AttributeEncryptedString extends AttributeString implements iAttributeNoGroupBy
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
static $sKey = null; // Encryption key used for all encrypted fields
static $sLibrary = null; // Encryption library used for all encrypted fields
@@ -9973,7 +9973,7 @@ class AttributeSubItem extends AttributeDefinition
*/
class AttributeOneWayPassword extends AttributeDefinition implements iAttributeNoGroupBy
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
/**
* Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329)

View File

@@ -1169,8 +1169,8 @@ class CMDBSource
*/
public static function IsSameFieldTypes($sItopGeneratedFieldType, $sDbFieldType)
{
list($sItopFieldDataType, $sItopFieldTypeOptions, $sItopFieldOtherOptions) = static::GetFieldDataTypeAndOptions($sItopGeneratedFieldType);
list($sDbFieldDataType, $sDbFieldTypeOptions, $sDbFieldOtherOptions) = static::GetFieldDataTypeAndOptions($sDbFieldType);
[$sItopFieldDataType, $sItopFieldTypeOptions, $sItopFieldOtherOptions] = static::GetFieldDataTypeAndOptions($sItopGeneratedFieldType);
[$sDbFieldDataType, $sDbFieldTypeOptions, $sDbFieldOtherOptions] = static::GetFieldDataTypeAndOptions($sDbFieldType);
if (strcasecmp($sItopFieldDataType, $sDbFieldDataType) !== 0)
{
@@ -1603,7 +1603,19 @@ class CMDBSource
return false;
}
/**
public static function GetClusterNb()
{
$result = 0;
$sSql = "SHOW STATUS LIKE 'wsrep_cluster_size';";
$aRows = self::QueryToArray($sSql);
if (count($aRows) > 0)
{
$result = $aRows[0]['Value'];
}
return intval($result);
}
/**
* @see https://dev.mysql.com/doc/refman/5.7/en/charset-database.html
* @return string query to upgrade database charset and collation if needed, null if not
* @throws \MySQLException

View File

@@ -766,6 +766,42 @@ abstract class DBObject implements iDisplay
$this->Set($sAttCode, $sValue);
}
/**
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
* @throws \OQLException
* @throws \ReflectionException
*/
protected function PreDeleteActions(): void
{
$this->SetReadOnly('No modification allowed before delete');
$this->FireEventAboutToDelete();
$oKPI = new ExecutionKPI();
$this->OnDelete();
$oKPI->ComputeStatsForExtension($this, 'OnDelete');
// Activate any existing trigger
$sClass = get_class($this);
$aParams = array('class_list' => MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL));
$oSet = new DBObjectSet(DBObjectSearch::FromOQL('SELECT TriggerOnObjectDelete AS t WHERE t.target_class IN (:class_list)'), array(),
$aParams);
while ($oTrigger = $oSet->Fetch()) {
/** @var \TriggerOnObjectDelete $oTrigger */
try {
$oKPI = new ExecutionKPI();
$oTrigger->DoActivate($this->ToArgs('this'));
}
catch (Exception $e) {
$oTrigger->LogException($e, $this);
utils::EnrichRaisedException($oTrigger, $e);
}
finally {
$oKPI->ComputeStatsForExtension($this, 'TriggerOnObjectDelete');
}
}
}
/**
* @return void
* @throws \ReflectionException
@@ -4090,16 +4126,17 @@ abstract class DBObject implements iDisplay
CMDBSource::DeleteFrom($sDeleteSQL);
}
/**
* @internal
*
* @throws ArchivedObjectException
* @throws CoreException
* @throws CoreUnexpectedValue
* @throws MySQLException
* @throws MySQLHasGoneAwayException
* @throws OQLException
*/
/**
* @internal
*
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
* @throws \OQLException
* @throws \Random\RandomException
* @throws \ReflectionException
*/
protected function DBDeleteSingleObject()
{
$this->LogCRUDEnter(__METHOD__);
@@ -4110,29 +4147,7 @@ abstract class DBObject implements iDisplay
return;
}
$this->SetReadOnly("No modification allowed before delete");
$this->FireEventAboutToDelete();
$oKPI = new ExecutionKPI();
$this->OnDelete();
$oKPI->ComputeStatsForExtension($this, 'OnDelete');
// Activate any existing trigger
$sClass = get_class($this);
$aParams = array('class_list' => MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL));
$oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectDelete AS t WHERE t.target_class IN (:class_list)"), array(),
$aParams);
while ($oTrigger = $oSet->Fetch())
{
/** @var \TriggerOnObjectDelete $oTrigger */
try
{
$oTrigger->DoActivate($this->ToArgs('this'));
}
catch(Exception $e) {
$oTrigger->LogException($e, $this);
utils::EnrichRaisedException($oTrigger, $e);
}
}
$this->PreDeleteActions();
$this->RecordObjDeletion($this->m_iKey); // May cause a reload for storing history information

View File

@@ -145,7 +145,7 @@ class EMail implements iEMail
*/
public function SetInReplyTo(string $sMessageId)
{
$this->AddToHeader('In-Reply-To', $sMessageId);
$this->oMailer->SetInReplyTo($sMessageId);
}
public function SetBody($sBody, $sMimeType = 'text/html', $sCustomStyles = null)

View File

@@ -6852,6 +6852,9 @@ abstract class MetaModel
/**
* Instantiate an object already persisted to the Database.
*
* Note that LinkedSet attributes are not loaded.
* DBObject::Reload() will be called when getting a LinkedSet attribute
*
* @api
* @see MetaModel::GetObjectWithArchive to get object even if it's archived
* @see utils::PushArchiveMode() to enable search on archived objects

View File

@@ -44,6 +44,8 @@ class ObjectResult
* @var string
* @api
*/
use SanitizeTrait;
public $message;
/**
* @var mixed|null
@@ -156,6 +158,19 @@ class ObjectResult
{
$this->fields[$sAttCode] = $this->MakeResultValue($oObject, $sAttCode, $bExtendedOutput);
}
public function SanitizeContent()
{
foreach($this->fields as $sFieldAttCode => $fieldValue)
{
try {
$oAttDef = MetaModel::GetAttributeDef($this->class, $sFieldAttCode);
} catch (Exception $e) { // for special cases like ID
continue;
}
$this->SanitizeFieldIfSensitive($this->fields, $sFieldAttCode, $fieldValue, $oAttDef);
}
}
}
@@ -221,6 +236,16 @@ class RestResultWithObjects extends RestResult
$sObjKey = get_class($oObject).'::'.$oObject->GetKey();
$this->objects[$sObjKey] = $oObjRes;
}
public function SanitizeContent()
{
parent::SanitizeContent();
foreach($this->objects as $sObjKey => $oObjRes)
{
$oObjRes->SanitizeContent();
}
}
}
/**
@@ -308,9 +333,10 @@ class RestDelete
*
* @package Core
*/
class CoreServices implements iRestServiceProvider
class CoreServices implements iRestServiceProvider, iRestInputSanitizer
{
/**
use SanitizeTrait;
/**
* Enumerate services delivered by this class
*
* @param string $sVersion The version (e.g. 1.0) supported by the services
@@ -528,18 +554,18 @@ class CoreServices implements iRestServiceProvider
}
else
{
if (!$bExtendedOutput && RestUtils::GetOptionalParam($aParams, 'output_fields', '*') != '*')
if (!$bExtendedOutput && RestUtils::GetOptionalParam($aParams, 'output_fields', '*') != '*')
{
$aFields = $aShowFields[$sClass];
//Id is not a valid attribute to optimize
if (in_array('id', $aFields))
if (in_array('id', $aFields))
{
unset($aFields[array_search('id', $aFields)]);
}
$aAttToLoad = array($oObjectSet->GetClassAlias() => $aFields);
$oObjectSet->OptimizeColumnLoad($aAttToLoad);
}
while ($oObject = $oObjectSet->Fetch())
{
$oResult->AddObject(0, '', $oObject, $aShowFields, $bExtendedOutput);
@@ -737,6 +763,33 @@ class CoreServices implements iRestServiceProvider
return $oResult;
}
public function SanitizeJsonInput(string $sJsonInput): string
{
$sSanitizedJsonInput = $sJsonInput;
$aJsonData = json_decode($sSanitizedJsonInput, true);
$sOperation = $aJsonData['operation'];
switch ($sOperation) {
case 'core/check_credentials':
if (isset($aJsonData['password'])) {
$aJsonData['password'] = '*****';
}
break;
case 'core/update':
case 'core/create':
default :
$sClass = $aJsonData['class'];
if (isset($aJsonData['fields'])) {
foreach ($aJsonData['fields'] as $sFieldAttCode => $fieldValue) {
$oAttDef = MetaModel::GetAttributeDef($sClass, $sFieldAttCode);
$this->SanitizeFieldIfSensitive($aJsonData['fields'], $sFieldAttCode, $fieldValue, $oAttDef);
}
}
break;
}
return json_encode($aJsonData, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
/**
* Helper for object deletion
*/
@@ -875,3 +928,50 @@ class CoreServices implements iRestServiceProvider
return $iLimit * max(0, $iPage - 1);
}
}
trait SanitizeTrait
{
/**
* Sanitize a field if it is sensitive.
*
* @param array $fields The fields array
* @param string $sFieldAttCode The attribute code
* @param mixed $oAttDef The attribute definition
* @throws Exception
*/
private function SanitizeFieldIfSensitive(array &$fields, string $sFieldAttCode, $fieldValue, $oAttDef): void
{
// for simple attribute
if ($oAttDef instanceof iAttributeNoGroupBy) // iAttributeNoGroupBy is equivalent to sensitive attribute
{
$fields[$sFieldAttCode] = '*****';
return;
}
// for 1-n / n-n relation
if ($oAttDef instanceof AttributeLinkedSet) {
foreach ($fieldValue as $i => $aLnkValues) {
foreach ($aLnkValues as $sLnkAttCode => $sLnkValue) {
$oLnkAttDef = MetaModel::GetAttributeDef($oAttDef->GetLinkedClass(), $sLnkAttCode);
if ($oLnkAttDef instanceof iAttributeNoGroupBy) { // 1-n relation
$fields[$sFieldAttCode][$i][$sLnkAttCode] = '*****';
}
elseif ($oAttDef instanceof AttributeLinkedSetIndirect && $oLnkAttDef instanceof AttributeExternalField) { // for n-n relation
$oExtKeyAttDef = MetaModel::GetAttributeDef($oLnkAttDef->GetTargetClass(), $oLnkAttDef->GetExtAttCode());
if ($oExtKeyAttDef instanceof iAttributeNoGroupBy) {
$fields[$sFieldAttCode][$i][$sLnkAttCode] = '*****';
}
}
}
}
return;
}
// for external attribute
if ($oAttDef instanceof AttributeExternalField) {
$oExtKeyAttDef = MetaModel::GetAttributeDef($oAttDef->GetTargetClass(), $oAttDef->GetExtAttCode());
if ($oExtKeyAttDef instanceof iAttributeNoGroupBy) {
$fields[$sFieldAttCode] = '*****';
}
}
}
}

View File

@@ -5,7 +5,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'authent-cas/3.1.1',
'authent-cas/3.1.3',
array(
// Identification
//

View File

@@ -27,7 +27,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'authent-external/3.1.1',
'authent-external/3.1.3',
array(
// Identification
//

View File

@@ -9,7 +9,7 @@ if (function_exists('ldap_connect'))
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'authent-ldap/3.1.1',
'authent-ldap/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'authent-local/3.1.1',
'authent-local/3.1.3',
array(
// Identification
//

View File

@@ -5,7 +5,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'combodo-backoffice-darkmoon-theme/3.1.1',
'combodo-backoffice-darkmoon-theme/3.1.3',
array(
// Identification
//

View File

@@ -24,7 +24,7 @@
/** @noinspection PhpUnhandledExceptionInspection */
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'combodo-db-tools/3.1.1',
'combodo-db-tools/3.1.3',
array(
// Identification
//

View File

@@ -19,7 +19,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-attachments/3.1.1',
'itop-attachments/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-backup/3.1.1',
'itop-backup/3.1.3',
array(
// Identification
//

View File

@@ -5,7 +5,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-bridge-cmdb-services/3.1.1',
'itop-bridge-cmdb-services/3.1.3',
array(
// Identification
//

View File

@@ -5,7 +5,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-bridge-cmdb-ticket/3.1.1',
'itop-bridge-cmdb-ticket/3.1.3',
array(
// Identification
//

View File

@@ -5,7 +5,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-bridge-datacenter-mgmt-services/3.1.1',
'itop-bridge-datacenter-mgmt-services/3.1.3',
array(
// Identification
//

View File

@@ -5,7 +5,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-bridge-endusers-devices-services/3.1.1',
'itop-bridge-endusers-devices-services/3.1.3',
array(
// Identification
//

View File

@@ -5,7 +5,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-bridge-storage-mgmt-services/3.1.1',
'itop-bridge-storage-mgmt-services/3.1.3',
array(
// Identification
//

View File

@@ -5,7 +5,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-bridge-virtualization-mgmt-services/3.1.1',
'itop-bridge-virtualization-mgmt-services/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-bridge-virtualization-storage/3.1.1',
'itop-bridge-virtualization-storage/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-change-mgmt-itil/3.1.1',
'itop-change-mgmt-itil/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-change-mgmt/3.1.1',
'itop-change-mgmt/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-config-mgmt/3.1.1',
'itop-config-mgmt/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-config/3.1.1',
'itop-config/3.1.3',
array(
// Identification
//

View File

@@ -24,7 +24,7 @@
/** @noinspection PhpUnhandledExceptionInspection */
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-core-update/3.1.1',
'itop-core-update/3.1.3',
[
// Identification
//

View File

@@ -18,7 +18,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-datacenter-mgmt/3.1.1',
'itop-datacenter-mgmt/3.1.3',
array(
// Identification
//

View File

@@ -25,7 +25,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-endusers-devices/3.1.1',
'itop-endusers-devices/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-faq-light/3.1.1',
'itop-faq-light/3.1.3',
array(
// Identification
//

View File

@@ -24,7 +24,7 @@
/** @noinspection PhpUnhandledExceptionInspection */
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-files-information/3.1.1',
'itop-files-information/3.1.3',
array(
// Identification
//

View File

@@ -6,7 +6,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-full-itil/3.1.1',
'itop-full-itil/3.1.3',
array(
// Identification
//

View File

@@ -5,7 +5,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-hub-connector/3.1.1',
'itop-hub-connector/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-incident-mgmt-itil/3.1.1',
'itop-incident-mgmt-itil/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-knownerror-mgmt/3.1.1',
'itop-knownerror-mgmt/3.1.3',
array(
// Identification
//

View File

@@ -5,7 +5,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-oauth-client/3.1.1',
'itop-oauth-client/3.1.3',
array(
// Identification
//

View File

@@ -20,7 +20,7 @@
/** @noinspection PhpUnhandledExceptionInspection */
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-portal-base/3.1.1', array(
'itop-portal-base/3.1.3', array(
// Identification
'label' => 'Portal Development Library',
'category' => 'Portal',

View File

@@ -15,6 +15,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with iTop. If not, see <http://www.gnu.org/licenses/>
p_user_profile_brick_edit_person:
path: '/user/edit_person'
defaults:
_controller: 'Combodo\iTop\Portal\Controller\UserProfileBrickController::EditPerson'
p_user_profile_brick:
path: '/user/{sBrickId}'
defaults:

File diff suppressed because one or more lines are too long

View File

@@ -1334,6 +1334,11 @@ class ObjectController extends BrickController
$bIgnoreSilos = $oScopeValidator->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass);
$aParams = array('objects_id' => $aObjectIds);
$oSearch = DBObjectSearch::FromOQL("SELECT $sObjectClass WHERE id IN (:objects_id)");
if (!$oScopeValidator->AddScopeToQuery($oSearch, $sObjectClass)
) {
IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to read ' . $sObjectClass . ' object.');
throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
}
if ($bIgnoreSilos === true) {
$oSearch->AllowAllData();
}
@@ -1389,6 +1394,10 @@ class ObjectController extends BrickController
$aObjectAttCodes = $oRequestManipulator->ReadParam('aObjectAttCodes', array(), FILTER_UNSAFE_RAW);
$aLinkAttCodes = $oRequestManipulator->ReadParam('aLinkAttCodes', array(), FILTER_UNSAFE_RAW);
$sDateTimePickerWidgetParent = $oRequestManipulator->ReadParam('sDateTimePickerWidgetParent', array(), FILTER_UNSAFE_RAW);
if (!MetaModel::IsLinkClass($sLinkClass)) {
IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' asked for wrong lnk class '.$sLinkClass);
throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
}
if (empty($sObjectClass) || empty($aObjectIds) || empty($aObjectAttCodes)) {
IssueLog::Info(__METHOD__.' at line '.__LINE__.' : sObjectClass, aObjectIds and aObjectAttCodes expected, "'.$sObjectClass.'", "'.implode('/',
@@ -1400,7 +1409,12 @@ class ObjectController extends BrickController
$bIgnoreSilos = $oScopeValidator->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass);
$aParams = array('objects_id' => $aObjectIds);
$oSearch = DBObjectSearch::FromOQL("SELECT $sObjectClass WHERE id IN (:objects_id)");
if ($bIgnoreSilos === true)
if (!$oScopeValidator->AddScopeToQuery($oSearch, $sObjectClass)
) {
IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to read ' . $sObjectClass . ' object.');
throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
}
if ($bIgnoreSilos === true)
{
$oSearch->AllowAllData();
}
@@ -1418,10 +1432,35 @@ class ObjectController extends BrickController
// Prepare link data
$aObjectData = $this->PrepareObjectInformation($oObject, $aObjectAttCodes);
// New link object (needed for renderers)
$oNewLink = new $sLinkClass();
$aAttCodes = MetaModel::GetAttributesList($sLinkClass, ['AttributeExternalKey']);
$sAttCodeToObject = '';
foreach ($aAttCodes as $sAttCode) {
$oAttDef = MetaModel::GetAttributeDef($sLinkClass, $sAttCode);
/** @var \AttributeExternalKey $oAttDef */
if ($oAttDef->GetTargetClass() === $sObjectClass) {
$sAttCodeToObject = $sAttCode;
}
}
if ($sAttCodeToObject === '') {
IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' asked for incoherent lnk class '.$sLinkClass.' with object class '.$sObjectClass);
throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
}
$oNewLink = MetaModel::NewObject($sLinkClass, [
$sAttCodeToObject => $oObject->GetKey(), // so later placeholders in filters will be applied on external keys on the same link
]);
foreach ($aLinkAttCodes as $sAttCode) {
$oAttDef = MetaModel::GetAttributeDef($sLinkClass, $sAttCode);
/** @var \Combodo\iTop\Form\Field\SelectObjectField $oField */
$oField = $oAttDef->MakeFormField($oNewLink);
if ($oAttDef::GetFormFieldClass() === '\\Combodo\\iTop\\Form\\Field\\SelectObjectField') {
$oFieldSearch = $oField->GetSearch();
$sFieldClass = $oFieldSearch->GetClass();
if ($oScopeValidator->AddScopeToQuery($oFieldSearch, $sFieldClass)){
$oField->SetSearch($oFieldSearch);
} else {
$oField->SetSearch(DBObjectSearch::FromOQL("SELECT $sFieldClass WHERE 1=0"));
}
}
// Prevent datetimepicker popup to be truncated
if ($oField instanceof DateTimeField) {
$oField->SetDateTimePickerWidgetParent($sDateTimePickerWidgetParent);

View File

@@ -35,7 +35,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use UserRights;
use utils;
use Dict;
/**
* Class UserProfileBrickController
*
@@ -66,34 +66,9 @@ class UserProfileBrickController extends BrickController
$oRequestManipulator = $this->get('request_manipulator');
/** @var \Combodo\iTop\Portal\Helper\ObjectFormHandlerHelper $ObjectFormHandler */
$ObjectFormHandler = $this->get('object_form_handler');
/** @var \Combodo\iTop\Portal\Brick\BrickCollection $oBrickCollection */
$oBrickCollection = $this->get('brick_collection');
$oBrick = $this->GetBrick($sBrickId);
// If the brick id was not specified, we get the first one registered that is an instance of UserProfileBrick as default
if ($sBrickId === null)
{
/** @var \Combodo\iTop\Portal\Brick\PortalBrick $oTmpBrick */
foreach ($oBrickCollection->GetBricks() as $oTmpBrick)
{
if ($oTmpBrick instanceof UserProfileBrick)
{
$oBrick = $oTmpBrick;
}
}
// We make sure a UserProfileBrick was found
if (!isset($oBrick) || $oBrick === null)
{
$oBrick = new UserProfileBrick();
//throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, 'UserProfileBrick : Brick could not be loaded as there was no UserProfileBrick loaded in the application.');
}
}
else
{
$oBrick = $oBrickCollection->GetBrickById($sBrickId);
}
$aData = array();
$aData = array();
// Setting form mode regarding the demo mode parameter
$bDemoMode = MetaModel::GetConfig()->Get('demo_mode');
@@ -130,11 +105,12 @@ class UserProfileBrickController extends BrickController
$oCurContact = UserRights::GetContactObject();
$sCurContactClass = get_class($oCurContact);
$sCurContactId = $oCurContact->GetKey();
$aForm = $oBrick->GetForm();
$aForm['submit_endpoint'] = $this->generateUrl('p_user_profile_brick_edit_person', ['sBrickId' => $sBrickId]);
// Preparing forms
$aData['forms']['contact'] = $ObjectFormHandler->HandleForm($oRequest, $sFormMode, $sCurContactClass, $sCurContactId,
$oBrick->GetForm());
$aData['forms']['preferences'] = $this->HandlePreferencesForm($oRequest, $sFormMode);
$aData['forms']['contact'] = $ObjectFormHandler->HandleForm($oRequest, $sFormMode, $sCurContactClass, $sCurContactId,
$aForm);
$aData['forms']['preferences'] = $this->HandlePreferencesForm($oRequest, $sFormMode);
// - If user can change password, we display the form
$aData['forms']['password'] = (UserRights::CanChangePassword()) ? $this->HandlePasswordForm($oRequest, $sFormMode) : null;
@@ -150,6 +126,35 @@ class UserProfileBrickController extends BrickController
return $oResponse;
}
public function EditPerson(Request $oRequest)
{
/** @var \Combodo\iTop\Portal\Helper\ObjectFormHandlerHelper $oObjectFormHandler */
$oObjectFormHandler = $this->get('object_form_handler');
/** @var \Combodo\iTop\Portal\Helper\SecurityHelper $oSecurityHelper */
$oSecurityHelper = $this->get('security_helper');
$oCurContact = UserRights::GetContactObject();
$sObjectClass = get_class($oCurContact);
$sObjectId = $oCurContact->GetKey();
// Checking security layers
// Warning : This is a dirty quick fix to allow editing its own contact information
$bAllowWrite = ($sObjectClass === 'Person' && $sObjectId == UserRights::GetContactId());
if (!$oSecurityHelper->IsActionAllowed(UR_ACTION_MODIFY, $sObjectClass, $sObjectId) && !$bAllowWrite) {
IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to modify ' . $sObjectClass . '::' . $sObjectId . ' object.');
throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
}
$aForm = $this->GetBrick()->GetForm();
$aForm['submit_endpoint'] = $this->generateUrl('p_user_profile_brick_edit_person');
$aData = ['sMode' => 'edit'];
$aData['form'] = $oObjectFormHandler->HandleForm($oRequest, $aData['sMode'], $sObjectClass, $sObjectId, $aForm);
return new JsonResponse($aData);
}
/**
* @param \Symfony\Component\HttpFoundation\Request $oRequest
* @param string $sFormMode
@@ -381,7 +386,7 @@ class UserProfileBrickController extends BrickController
'sObjectField' => $sPictureAttCode,
'cache' => 86400,
's' => $oOrmDoc->GetSignature(),
]);
]);
$aFormData['validation'] = array(
'valid' => true,
'messages' => array(),
@@ -394,4 +399,34 @@ class UserProfileBrickController extends BrickController
return $aFormData;
}
/**
* @param $sBrickId
* @return \Combodo\iTop\Portal\Brick\PortalBrick|UserProfileBrick
* @throws \Combodo\iTop\Portal\Brick\BrickNotFoundException
*/
public function GetBrick($sBrickId = null)
{
/** @var \Combodo\iTop\Portal\Brick\BrickCollection $oBrickCollection */
$oBrickCollection = $this->get('brick_collection');
// If the brick id was not specified, we get the first one registered that is an instance of UserProfileBrick as default
if ($sBrickId === null) {
/** @var \Combodo\iTop\Portal\Brick\PortalBrick $oTmpBrick */
foreach ($oBrickCollection->GetBricks() as $oTmpBrick) {
if ($oTmpBrick instanceof UserProfileBrick) {
$oBrick = $oTmpBrick;
}
}
// We make sure a UserProfileBrick was found
if (!isset($oBrick) || $oBrick === null) {
$oBrick = new UserProfileBrick();
//throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, 'UserProfileBrick : Brick could not be loaded as there was no UserProfileBrick loaded in the application.');
}
} else {
$oBrick = $oBrickCollection->GetBrickById($sBrickId);
}
return $oBrick;
}
}

View File

@@ -242,13 +242,17 @@ class ObjectFormHandlerHelper
case static::ENUM_MODE_CREATE:
case static::ENUM_MODE_EDIT:
case static::ENUM_MODE_VIEW:
$sFormEndpoint = $this->oUrlGenerator->generate(
'p_object_'.$sMode,
array(
'sObjectClass' => $sObjectClass,
'sObjectId' => $sObjectId,
)
);
if(array_key_exists('submit_endpoint', $aFormProperties)) {
$sFormEndpoint = $aFormProperties['submit_endpoint'];
} else {
$sFormEndpoint = $this->oUrlGenerator->generate(
'p_object_' . $sMode,
array(
'sObjectClass' => $sObjectClass,
'sObjectId' => $sObjectId,
)
);
}
break;
case static::ENUM_MODE_APPLY_STIMULUS:
@@ -281,7 +285,8 @@ class ObjectFormHandlerHelper
->SetActionRulesToken($sActionRulesToken)
->SetRenderer($oFormRenderer)
->SetFormProperties($aFormProperties);
$oFormManager->PrepareFormAndHTMLDocument();
$oFormManager->PrepareFields();
$oFormManager->Build();
$aFormData['hidden_fields'] = $oFormManager->GetHiddenFieldsId();
// Check the number of editable fields
@@ -399,7 +404,7 @@ class ObjectFormHandlerHelper
ApplicationContext::MakeObjectUrl($sObjectClass, $sObjectId)
);
}
return $aFormData;
}

View File

@@ -476,8 +476,8 @@
sBody = '{{ 'Error:XHR:Fail'|dict_format(constant('ITOP_APPLICATION_SHORT'))|escape('js') }}';
}
var oModalElem = $('#modal-for-alert');
oModalElem.find('.modal-content .modal-header .modal-title').html(sTitle);
oModalElem.find('.modal-content .modal-body .alert').addClass('alert-danger').html(sBody);
oModalElem.find('.modal-content .modal-header .modal-title').text(sTitle);
oModalElem.find('.modal-content .modal-body .alert').addClass('alert-danger').text(sBody);
oModalElem.modal('show');
};
{% endblock %}

View File

@@ -20,7 +20,7 @@
/** @noinspection PhpUnhandledExceptionInspection */
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-portal/3.1.1', array(
'itop-portal/3.1.3', array(
// Identification
'label' => 'Enhanced Customer Portal',
'category' => 'Portal',

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-problem-mgmt/3.1.1',
'itop-problem-mgmt/3.1.3',
array(
// Identification
//

View File

@@ -19,7 +19,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-profiles-itil/3.1.1',
'itop-profiles-itil/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-request-mgmt-itil/3.1.1',
'itop-request-mgmt-itil/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-request-mgmt/3.1.1',
'itop-request-mgmt/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-service-mgmt-provider/3.1.1',
'itop-service-mgmt-provider/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-service-mgmt/3.1.1',
'itop-service-mgmt/3.1.3',
array(
// Identification
//

View File

@@ -18,7 +18,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-sla-computation/3.1.1',
'itop-sla-computation/3.1.3',
array(
// Identification
//

View File

@@ -25,7 +25,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-storage-mgmt/3.1.1',
'itop-storage-mgmt/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-structure/3.1.1',
'itop-structure/3.1.3',
array(
// Identification
//

View File

@@ -5,7 +5,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-themes-compat/3.1.1',
'itop-themes-compat/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__,
'itop-tickets/3.1.1',
'itop-tickets/3.1.3',
array(
// Identification
//

View File

@@ -16,7 +16,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-virtualization-mgmt/3.1.1',
'itop-virtualization-mgmt/3.1.3',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-welcome-itil/3.1.1',
'itop-welcome-itil/3.1.3',
array(
// Identification
//

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<information>
<version>3.1.1</version>
<version>3.1.3</version>
</information>

View File

@@ -767,12 +767,12 @@ try
$sClass = utils::ReadParam('className', '', false, 'class');
$sRootClass = utils::ReadParam('baseClass', '', false, 'class');
$currentId = utils::ReadParam('currentId', '');
$sTableId = utils::ReadParam('_table_id_', null, false, 'raw_data');
$sTableId = utils::ReadParam('_table_id_', null, false, utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
$sAction = utils::ReadParam('action', '');
$sSelectionMode = utils::ReadParam('selection_mode', null, false, 'raw_data');
$sResultListOuterSelector = utils::ReadParam('result_list_outer_selector', null, false, 'raw_data');
$scssCount = utils::ReadParam('css_count', null, false, 'raw_data');
$sTableInnerId = utils::ReadParam('table_inner_id', $sTableId, false, 'raw_data');
$sSelectionMode = utils::ReadParam('selection_mode');
$sResultListOuterSelector = utils::ReadParam('result_list_outer_selector', null,false, utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER); // actually an Id not a selector
$scssCount = utils::ReadParam('css_count', null,false,utils::ENUM_SANITIZATION_FILTER_ELEMENT_SELECTOR);
$sTableInnerId = utils::ReadParam('table_inner_id', null,false, utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
$oFilter = new DBObjectSearch($sClass);
$oSet = new CMDBObjectSet($oFilter);

View File

@@ -245,11 +245,11 @@ JS
$aMoreInfoBlocks = [];
$oDevelopedQuerySet = new FieldSet(Dict::S('UI:RunQuery:DevelopedQuery'));
$oDevelopedQuerySet->AddSubBlock(UIContentBlockUIBlockFactory::MakeForCode(utils::EscapeHtml($oFilter->ToOQL())));
$oDevelopedQuerySet->AddSubBlock(UIContentBlockUIBlockFactory::MakeForCode($oFilter->ToOQL()));
$aMoreInfoBlocks[] = $oDevelopedQuerySet;
$oSerializedQuerySet = new FieldSet(Dict::S('UI:RunQuery:SerializedFilter'));
$oSerializedQuerySet->AddSubBlock(UIContentBlockUIBlockFactory::MakeForCode(utils::EscapeHtml($oFilter->serialize())));
$oSerializedQuerySet->AddSubBlock(UIContentBlockUIBlockFactory::MakeForCode($oFilter->serialize()));
$aMoreInfoBlocks[] = $oSerializedQuerySet;

View File

@@ -343,6 +343,8 @@ function DisplayEvents(WebPage $oPage, $sClass)
}
}
$sListener = $sListenerClass.'->'.$aListener['callback'][1].'(\Combodo\iTop\Service\Events\EventData $oEventData)';
} else if (is_array($aListener['callback'])) {
$sListener = $aListener['callback'][0].'::'.$aListener['callback'][1];
} else {
$sListener = $aListener['callback'].'(\Combodo\iTop\Service\Events\EventData $oEventData)';
}

View File

@@ -1165,6 +1165,7 @@ EOF
*/
protected function QuoteForPHP($sStr, $bSimpleQuotes = false)
{
$sStr = $sStr ?? '';
if ($bSimpleQuotes)
{
$sEscaped = str_replace(array('\\', "'"), array('\\\\', "\\'"), $sStr);
@@ -3229,10 +3230,11 @@ EOF;
$aEntriesPHP = array();
$oEntries = $oDictionaryNode->GetUniqueElement('entries');
/** @var MFElement $oEntry */
foreach ($oEntries->getElementsByTagName('entry') as $oEntry)
{
$sStringCode = $oEntry->getAttribute('id');
$sValue = $oEntry->GetText();
$sValue = $oEntry->GetText('');
$aEntriesPHP[] = "\t'$sStringCode' => ".self::QuoteForPHP(self::FilterDictString($sValue), true).",";
}
$sEntriesPHP = implode("\n", $aEntriesPHP);
@@ -3267,7 +3269,7 @@ EOF;
file_put_contents($sLanguagesFile, $sLanguagesFileContent);
}
protected static function FilterDictString($s)
protected static function FilterDictString(string $s): string
{
if (strpos($s, '~') !== false)
{

View File

@@ -1295,6 +1295,12 @@ EOF
$aResult['checks'][] = new CheckResult(CheckResult::INFO, "MySQL server's max_connections is set to $iMaxConnections.");
}
$iClusters = $oDBSource->GetClusterNb();
if ($iClusters > 0) {
SetupLog::Warning('Warning - Using Galera will cause malfunctions and data corruptions. Combodo does not support this type of infrastructure.');
$aResult['checks'][] = new CheckResult(CheckResult::WARNING, 'Using Galera will cause malfunctions and data corruptions. Combodo does not support this type of infrastructure.');
}
try {
$aResult['databases'] = $oDBSource->ListDB();
}

View File

@@ -2,24 +2,72 @@
This script allows to install and update iTop via CLI.
For more information, see the official Wiki : [Automated installation [iTop Documentation]](https://www.itophub.io/wiki/page?id=latest:advancedtopics:automatic_install)
For more information, see the official Wiki : [Automated installation [iTop Documentation]](https://www.itophub.io/wiki/page?id=latest:advancedtopics:automatic_install)
# unattended-install.php
## Usage
Execution of the unattended installation
> Note:
> Because the installation runs from the command line, make sure that the current user has enough rights to access the different locations and that the web server will be able to access the files and directories created during the scripted installation. In order to exactly emulate the behavior of
the interactive installation it may be a good practice to run this installation from the user account used for running the web server process.
Launch the script with the following command: ```bash php unattended_install.php --param-file=fresh-install.xml ```
Where: `fresh-install.xml` is the response file containing your desired settings for the installation (there are 4 models available in the folder `xml_setup`: fresh-install.xml, itil-fresh-install.xml, itil-upgrade.xml, upgrade.xml)
Fresh installation parameters
> Important:
> In the case of a fresh installation (<mode>install</mode>), do not forget to complete below mandatory parameters before:
```xml
<database>
<server></server>
<user></user>
<pwd></pwd>
<name></name>
<db_tls_enabled></db_tls_enabled>
<db_tls_ca></db_tls_ca>
<prefix></prefix>
</database>
<url>
</url>
<graphviz_path>/usr/bin/dot</graphviz_path>
<admin_account>
<user></user>
<pwd></pwd>
<language></language>
</admin_account>
<language></language>
```
## Options
To get all available options of the script, you can perform the following command :
```php unattended-install.php --help```
# install-itop.sh
## Usage
#install-itop.sh
You can install your iTop by only using config-itop.php settings and run either
- a non-ITIL iTop fresh installation (use itil-fresh-install.xml to have ITIL modules instead)
```
./install-itop.sh ./xml_setup/fresh-install.xml
```
- a non-ITIL iTop upgrade (use itil-upgrade.xml to have ITIL modules instead)
```
./install-itop.sh ./xml_setup/upgrade.xml
```
- a specific iTop installation by providing both xml setup file
in below example file provided is the one generated by iTop during last setup.
in below example file provided is the one generated by iTop during last setup.
```
./install-itop.sh ../../log/install-2024-04-03.xml
```

View File

@@ -27,6 +27,7 @@ use DBObject;
use DBObjectSearch;
use DBObjectSet;
use Exception;
use ExecutionKPI;
use IssueLog;
use MetaModel;
@@ -58,6 +59,7 @@ class ActivityPanelFactory
*/
public static function MakeForObjectDetails(DBObject $oObject, string $sMode = cmdbAbstractObject::DEFAULT_DISPLAY_MODE)
{
$oKPI = new ExecutionKPI();
$sObjClass = get_class($oObject);
$sObjId = $oObject->GetKey();
@@ -171,6 +173,8 @@ class ActivityPanelFactory
}
}
$oKPI->ComputeStatsForExtension(new ActivityPanelFactory(), 'MakeForObjectDetails');
return $oActivityPanel;
}
}

View File

@@ -45,33 +45,45 @@ class UIContentBlockUIBlockFactory extends AbstractUIBlockFactory
* The \n are replaced by <br>
*
* @api
* @param string $sCode
* @param string $sCode plain text code
* @param string|null $sId
*
* @return \Combodo\iTop\Application\UI\Base\Layout\UIContentBlock
*/
public static function MakeForCode(string $sCode, string $sId = null)
{
$oCode = new UIContentBlock($sId, ['ibo-is-code']);
$sCode = str_replace("\n", '<br>', $sCode);
$oCode->AddSubBlock(new Html($sCode));
$sCode = str_replace("\n", '<br>', \utils::HtmlEntities($sCode));
return $oCode;
return self::MakeFromHTMLCode($sId, $sCode);
}
/**
* Used to display a block of preformatted text in a <pre> tag.
*
* @api
* @param string $sCode
* @param string $sCode plain text code
* @param string|null $sId
*
* @return \Combodo\iTop\Application\UI\Base\Layout\UIContentBlock
*/
public static function MakeForPreformatted(string $sCode, string $sId = null)
{
$sCode = '<pre>'.$sCode.'</pre>';
$sCode = '<pre>'.\utils::HtmlEntities($sCode).'</pre>';
return static::MakeForCode($sCode, $sId);
return self::MakeFromHTMLCode($sId, $sCode);
}
/**
* @param string|null $sId
* @param string $sCode
*
* @return \Combodo\iTop\Application\UI\Base\Layout\UIContentBlock
*/
private static function MakeFromHTMLCode(?string $sId, string $sCode): UIContentBlock
{
$oCode = new UIContentBlock($sId, ['ibo-is-code']);
$oCode->AddSubBlock(new Html($sCode));
return $oCode;
}
}

View File

@@ -53,7 +53,7 @@ class BlockList extends UIContentBlock
{
return '$("#'.$this->sId.'").block();
$.post("ajax.render.php?operation=refreshDashletList",
{ style: "list", filter: "'.$this->sFilter.'", extra_params: '.json_encode($this->aExtraParams).' },
{ style: "list", filter: '.json_encode($this->sFilter).', extra_params: '.json_encode($this->aExtraParams).' },
function(data){
$("#'.$this->sId.'")
.empty()

View File

@@ -7,7 +7,8 @@
*/
use Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderFactory;
use Laminas\Mail\Header\ContentType;
use Laminas\Mail\Header\InReplyTo;
use Laminas\Mail\Header\MessageId;
use Laminas\Mail\Message;
use Laminas\Mail\Protocol\Smtp\Auth\Oauth;
use Laminas\Mail\Transport\File;
@@ -331,11 +332,11 @@ class EMailLaminas extends Email
{
$this->m_aData['message_id'] = $sId;
// Note: Swift will add the angle brackets for you
// Note: The email library will add the angle brackets for you
// so let's remove the angle brackets if present, for historical reasons
$sId = str_replace(array('<', '>'), '', $sId);
$this->m_oMessage->getHeaders()->addHeaderLine('Message-ID', $sId);
$this->m_oMessage->getHeaders()->addHeader((new MessageId())->setId($sId));
}
public function SetReferences($sReferences)
@@ -354,7 +355,11 @@ class EMailLaminas extends Email
*/
public function SetInReplyTo(string $sMessageId)
{
$this->AddToHeader('In-Reply-To', $sMessageId);
// Note: Laminas will add the angle brackets for you
// so let's remove the angle brackets if present, for historical reasons
$sId = str_replace(array('<', '>'), '', $sMessageId);
$this->m_oMessage->getHeaders()->addHeader((new InReplyTo())->setIds([$sId]));
}
/**
@@ -398,19 +403,6 @@ class EMailLaminas extends Email
$oBody->addPart($oAdditionalPart);
}
if ($oBody->isMultiPart()) {
$oContentTypeHeader = $this->m_oMessage->getHeaders();
foreach ($oContentTypeHeader as $oHeader) {
if (!$oHeader instanceof ContentType) {
continue;
}
$oHeader->setType(Mime::MULTIPART_MIXED);
$oHeader->addParameter('boundary', $oBody->getMime()->boundary());
break;
}
}
$this->m_oMessage->setBody($oBody);
}
@@ -431,22 +423,13 @@ class EMailLaminas extends Email
$oNewPart = new Part($sText);
$oNewPart->encoding = Mime::ENCODING_8BIT;
$oNewPart->type = $sMimeType;
$this->m_oMessage->getBody()->addPart($oNewPart);
// setBody called only to refresh Content-Type to multipart/mixed
$this->m_oMessage->setBody($this->m_oMessage->getBody()->addPart($oNewPart));
}
public function AddAttachment($data, $sFileName, $sMimeType)
{
$oBody = $this->m_oMessage->getBody();
if (!$oBody->isMultiPart()) {
$multipart_content = new Part($oBody->generateMessage());
$multipart_content->setType($oBody->getParts()[0]->getType());
$multipart_content->setBoundary($oBody->getMime()->boundary());
$oBody = new Laminas\Mime\Message();
$oBody->addPart($multipart_content);
}
if (!array_key_exists('attachments', $this->m_aData)) {
$this->m_aData['attachments'] = array();
}
@@ -457,23 +440,8 @@ class EMailLaminas extends Email
$oNewAttachment->disposition = Mime::DISPOSITION_ATTACHMENT;
$oNewAttachment->encoding = Mime::ENCODING_BASE64;
$oBody->addPart($oNewAttachment);
if ($oBody->isMultiPart()) {
$oContentTypeHeader = $this->m_oMessage->getHeaders();
foreach ($oContentTypeHeader as $oHeader) {
if (!$oHeader instanceof ContentType) {
continue;
}
$oHeader->setType(Mime::MULTIPART_MIXED);
$oHeader->addParameter('boundary', $oBody->getMime()->boundary());
break;
}
}
$this->m_oMessage->setBody($oBody);
// setBody called only to refresh Content-Type to multipart/mixed
$this->m_oMessage->setBody($this->m_oMessage->getBody()->addPart($oNewAttachment));
}
public function SetSubject($sSubject)

View File

@@ -768,6 +768,15 @@ JS
*/
protected function InjectRendererFileAssets(string $sClass, array $aAttributesCodesToDisplay, $oOutput)
{
// handle abstract class
while(MetaModel::IsAbstract($sClass)){
$aChildClasses = MetaModel::EnumChildClasses($sClass);
if(count($aChildClasses) > 0){
$sClass = $aChildClasses[0];
}
}
// create a fake object to pass to renderers for retrieving global assets
$oItem = MetaModel::NewObject($sClass);
// Iterate throw attributes...
@@ -776,10 +785,13 @@ JS
// Retrieve attribute definition
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
// make form field from attribute
$oField = $oAttDef->MakeFormField($oItem);
// retrieve the form field renderer
$sFieldRendererClass = static::GetFieldRendererClass($oField);
// retrieve renderer global assets
if ($sFieldRendererClass !== null) {
/** @var FieldRenderer $oFieldRenderer */
$oFieldRenderer = new $sFieldRendererClass($oField);

View File

@@ -592,6 +592,7 @@ EOF
*/
private static function GetDivAlert(string $message): string
{
$message = utils::EscapeHtml($message);
return "<div class=\"ibo-csv-import--cell-error ibo-csv-import--cell-message\">$message</div>\n";
}

View File

@@ -46,6 +46,8 @@ class ModuleService
$sExtension = $this->GetModuleNameFromObject($oReflectionClass->getName());
if (strlen($sExtension) !== 0) {
$sSignature .= '['.$sExtension.'] ';
} else {
$sSignature .= '[core] ';
}
$sSignature .= $oReflectionClass->getShortName().'::'.$sMethod.'()';

View File

@@ -4,7 +4,6 @@
{# SET WIDGET #}
{% set oDataProvider = oUIBlock.GetDataProvider() %}
let optionsBeforeFilter;
let oWidget{{ oUIBlock.GetId() }} = $('#{{ oUIBlock.GetId() }}').selectize({
{# Global options #}
@@ -109,7 +108,7 @@ let oWidget{{ oUIBlock.GetId() }} = $('#{{ oUIBlock.GetId() }}').selectize({
// Filter old options data to keep selected values
// (options with force flag will be kept event if they doesn't be part of the current value)
let options = Object.values(me.options);
optionsBeforeFilter = options;
me.optionsBeforeFilter = options;
options = options.filter(item => (typeof(item.force) !== "undefined" && item.force === true) || aSelectedItems.includes(item['{{ oDataProvider.GetDataValueField() }}']));
// Merge kept and new values
options = $.merge(options, res.data.search_data);
@@ -203,7 +202,7 @@ let oWidget{{ oUIBlock.GetId() }} = $('#{{ oUIBlock.GetId() }}').selectize({
onBlur: function(){
this.clearOptionGroups()
this.addOption(optionsBeforeFilter)
this.addOption(this.optionsBeforeFilter)
},
{# plugin combodo_add_button #}

View File

@@ -41,6 +41,7 @@ use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu;
use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Layout\Object\ObjectFactory;
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory;
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockWithJSRefreshCallback;
use iTopWebPage;
use LoginWebPage;
@@ -355,6 +356,22 @@ $oDashletFieldset2->AddSubBlock($oDashletField4);
$oDashletFieldset2->AddSubBlock($oDashletField5);
$oDashletFieldset2->AddSubBlock($oDashletField6);
/////////
// Code
/////////
$oPage->AddUiBlock(TitleUIBlockFactory::MakeNeutral('Code examples (MakeForCode)', 2 ));
$oCode1 = UIContentBlockUIBlockFactory::MakeForCode('function mean(int $a, int $b) {
return ($a + $b)/2
}');
$oPage->AddUiBlock($oCode1);
$oPage->AddUiBlock(TitleUIBlockFactory::MakeNeutral('Code examples (MakeForPreformatted)', 2 ));
$oCode2 = UIContentBlockUIBlockFactory::MakeForPreformatted('function mean(int $a, int $b) {
return ($a + $b)/2
}');
$oPage->AddUiBlock($oCode2);
/////////
// Pill
/////////

View File

@@ -169,6 +169,27 @@ abstract class ItopTestCase extends TestCase
return $sAppRootPath . '/';
}
private static function GetFirstDirUpContainingFile(string $sSearchPath, string $sFileToFindGlobPattern): ?string
{
for ($iDepth = 0; $iDepth < 8; $iDepth++) {
$aGlobFiles = glob($sSearchPath . '/' . $sFileToFindGlobPattern);
if (is_array($aGlobFiles) && (count($aGlobFiles) > 0)) {
return $sSearchPath . '/';
}
$iOffsetSep = strrpos($sSearchPath, '/');
if ($iOffsetSep === false) {
$iOffsetSep = strrpos($sSearchPath, '\\');
if ($iOffsetSep === false) {
// Do not throw an exception here as PHPUnit will not show it clearly when determing the list of test to perform
return 'Could not find the approot file in ' . $sSearchPath;
}
}
$sSearchPath = substr($sSearchPath, 0, $iOffsetSep);
}
return null;
}
/**
* Overload this method to require necessary files through {@see \Combodo\iTop\Test\UnitTest\ItopTestCase::RequireOnceItopFile()}
*
@@ -206,23 +227,6 @@ abstract class ItopTestCase extends TestCase
require_once $this->GetAppRoot() . $sFileRelPath;
}
/**
* Helper to load a module file. The caller test must be in that module !
* Will browse dir up to find a module.*.php
*
* @param string $sFileRelPath for example 'portal/src/Helper/ApplicationHelper.php'
* @since 2.7.10 3.1.1 3.2.0 N°6709 method creation
*/
protected function RequireOnceCurrentModuleFile(string $sFileRelPath): void
{
$aStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
$sCallerFileFullPath = $aStack[0]['file'];
$sCallerDir = dirname($sCallerFileFullPath);
$sModuleRootPath = static::GetFirstDirUpContainingFile($sCallerDir, 'module.*.php');
require_once $sModuleRootPath . $sFileRelPath;
}
/**
* Require once a unit test file (eg. a mock class) from its relative path from the *current* dir.
* This ensure that required files don't crash when unit tests dir is moved in the iTop structure (see N°5608)
@@ -240,26 +244,6 @@ abstract class ItopTestCase extends TestCase
require_once $sCallerDirAbsPath . DIRECTORY_SEPARATOR . $sFileRelPath;
}
private static function GetFirstDirUpContainingFile(string $sSearchPath, string $sFileToFindGlobPattern): ?string
{
for ($iDepth = 0; $iDepth < 8; $iDepth++) {
$aGlobFiles = glob($sSearchPath . '/' . $sFileToFindGlobPattern);
if (is_array($aGlobFiles) && (count($aGlobFiles) > 0)) {
return $sSearchPath . '/';
}
$iOffsetSep = strrpos($sSearchPath, '/');
if ($iOffsetSep === false) {
$iOffsetSep = strrpos($sSearchPath, '\\');
if ($iOffsetSep === false) {
// Do not throw an exception here as PHPUnit will not show it clearly when determing the list of test to perform
return 'Could not find the approot file in ' . $sSearchPath;
}
}
$sSearchPath = substr($sSearchPath, 0, $iOffsetSep);
}
return null;
}
protected function debug($sMsg)
{
if (static::$DEBUG_UNIT_TEST) {
@@ -402,11 +386,11 @@ abstract class ItopTestCase extends TestCase
*/
private function GetProperty(string $sClass, string $sProperty): \ReflectionProperty
{
$class = new \ReflectionClass($sClass);
$property = $class->getProperty($sProperty);
$property->setAccessible(true);
$oClass = new \ReflectionClass($sClass);
$oProperty = $oClass->getProperty($sProperty);
$oProperty->setAccessible(true);
return $property;
return $oProperty;
}
@@ -417,7 +401,7 @@ abstract class ItopTestCase extends TestCase
*
* @since 2.7.8 3.0.3 3.1.0
*/
public function SetNonPublicProperty(object $oObject, string $sProperty, $value)
public function SetNonPublicProperty($oObject, string $sProperty, $value)
{
$oProperty = $this->GetProperty(get_class($oObject), $sProperty);
$oProperty->setValue($oObject, $value);

View File

@@ -57,6 +57,8 @@ class DisplayBlockTest extends ItopCustomDatamodelTestCase
*/
public function testRenderChartAjax(string $sClassToDisplay, string $sAttributeToDisplay, string $sRelatedClass, string $sRelatedClassAttributeToEdit, string $sExpected, string $sNonExpected): void
{
$this->markTestSkipped("Waiting for N°7313 to be fixed, this test was made during the first attempt to resolve N°7313, but as it broke N°7592, N°7594, N°7600 & N°7605, we reverted the change until we make a proper fix in Expression::MakeValueLabel()");
$oUserRequest = new UserRequest();
$oUserRequest->Set('title', 'MyTitle');
$oUserRequest->Set('org_id', $this->getTestOrgId());

View File

@@ -101,13 +101,14 @@ class BulkChangeTest extends ItopDataTestCase
//$this->debug("sStatus:".$sStatus->GetDescription());
$this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription());
foreach ($aRow as $i => $oCell) {
/** @var $oCell \CellChangeSpec */
if ($i !== "finalclass" && $i !== "__STATUS__" && $i !== "__ERRORS__" && array_key_exists($i, $aResult)) {
$this->debug("i:".$i);
$this->debug('GetCLIValue:'.$oCell->GetCLIValue());
$this->debug("aResult:".$aResult[$i]);
$this->assertEquals($aResult[$i], $oCell->GetCLIValue());
$this->assertEquals($aResult[$i], $oCell->GetCLIValue(), "Unexpected CLI result for cell " . $i);
if (null !== $aResultHTML) {
$this->assertEquals($aResultHTML[$i], $oCell->GetHTMLValue());
$this->assertEquals($aResultHTML[$i], $oCell->GetHTMLValue(), "Unexpected HTML result for cell " . $i);
}
}
}

View File

@@ -374,6 +374,27 @@ class CRUDEventTest extends ItopDataTestCase
$this->assertStringStartsWith('CRUD', $oPerson->Get('first_name'), 'The object should have been modified and recorded in DB by EVENT_DB_AFTER_WRITE handler');
}
public function testAfterDeleteObjectAttributesExceptLinkedSetAreUsable()
{
$oPerson = $this->createObject('Person', [
'name' => 'Person_1',
'first_name' => 'Test',
'org_id' => $this->getTestOrgId(),
]);
$oFetchPerson = MetaModel::GetObject('Person', $oPerson->GetKey());
$oEventReceiver = new CRUDEventReceiver($this);
// Set the person's first name during Compute Values
$oEventReceiver->AddCallback(EVENT_DB_AFTER_DELETE, Person::class, 'GetObjectAttributesValues');
$oEventReceiver->RegisterCRUDEventListeners(EVENT_DB_AFTER_DELETE);
$oEventReceiver->RegisterCRUDEventListeners(EVENT_DB_OBJECT_RELOAD);
$oFetchPerson->DBDelete();
$this->assertEquals(1, self::$aEventCallsCount[EVENT_DB_AFTER_DELETE], 'EVENT_DB_AFTER_DELETE must be called when deleting an object and the object attributes must remain accessible');
}
/**
* Modify one object during EVENT_DB_AFTER_WRITE
* Check that the CRUD is protected against infinite loops (when modifying an object in its EVENT_DB_AFTER_WRITE)
@@ -881,6 +902,20 @@ class CRUDEventReceiver extends ClassesWithDebug
$oObject->Set('first_name', 'CRUD_first_name_'.rand());
}
/**
* @noinspection PhpUnusedPrivateMethodInspection Used as a callback
*/
private function GetObjectAttributesValues(EventData $oData): void
{
$this->Debug(__METHOD__);
$oObject = $oData->Get('object');
foreach (MetaModel::ListAttributeDefs(get_class($oObject)) as $sAttCode => $oAttDef) {
if (!$oAttDef->IsLinkSet()) {
$oObject->Get($sAttCode);
}
}
}
/**
* @noinspection PhpUnusedPrivateMethodInspection Used as a callback
*/

View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.7">
<classes>
<class id="TestServer" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<category>bizmodel</category>
<abstract>false</abstract>
<key_type>autoincrement</key_type>
<db_table>test_server</db_table>
<db_key_field>id</db_key_field>
</properties>
<presentation/>
<methods/>
<fields>
<field id="contact_list" xsi:type="AttributeLinkedSetIndirect">
<linked_class>lnkContactTestToServer</linked_class>
<ext_key_to_me>test_server_id</ext_key_to_me>
<ext_key_to_remote>contact_test_id</ext_key_to_remote>
<is_null_allowed>true</is_null_allowed>
</field>
<field id="password_list" xsi:type="AttributeLinkedSet">
<linked_class>PasswordTest</linked_class>
<ext_key_to_me>server_test_id</ext_key_to_me>
<is_null_allowed>true</is_null_allowed>
</field>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
</field>
</fields>
</class>
<class id="ContactTest" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<category>bizmodel</category>
<abstract>false</abstract>
<key_type>autoincrement</key_type>
<db_table>contact_test</db_table>
<db_key_field>id</db_key_field>
</properties>
<presentation/>
<methods/>
<fields>
<field id="password" xsi:type="AttributeEncryptedString">
<sql>password</sql>
</field>
<field id="server_test_list" xsi:type="AttributeLinkedSetIndirect">
<linked_class>lnkContactTestToServer</linked_class>
<ext_key_to_me>contact_test_id</ext_key_to_me>
<ext_key_to_remote>test_server_id</ext_key_to_remote>
<is_null_allowed>true</is_null_allowed>
</field>
</fields>
</class>
<class id="lnkContactTestToServer" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<category>bizmodel</category>
<abstract>false</abstract>
<key_type>autoincrement</key_type>
<db_table>lnk_contact_server_test</db_table>
<db_key_field>id</db_key_field>
</properties>
<presentation/>
<methods/>
<fields>
<field id="contact_test_password" xsi:type="AttributeExternalField" _delta="define">
<extkey_attcode>contact_test_id</extkey_attcode>
<target_attcode>password</target_attcode>
</field>
<field id="test_server_id" xsi:type="AttributeExternalKey" _delta="define">
<target_class>TestServer</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<sql>test_server</sql>
<is_null_allowed>false</is_null_allowed>
</field>
<field id="contact_test_id" xsi:type="AttributeExternalKey" _delta="define">
<target_class>ContactTest</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<sql>contact_test</sql>
<is_null_allowed>false</is_null_allowed>
</field>
</fields>
</class>
<class id="PasswordTest" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<category>bizmodel</category>
<abstract>false</abstract>
<key_type>autoincrement</key_type>
<db_table>password_test</db_table>
<db_key_field>id</db_key_field>
</properties>
<presentation/>
<methods/>
<fields>
<field id="server_test_id" xsi:type="AttributeExternalKey" _delta="define">
<target_class>TestServer</target_class>
<sql>server_test_id</sql>
<on_target_delete>DEL_MANUAL</on_target_delete>
</field>
<field id="password" xsi:type="AttributeEncryptedString" _delta="define">
<sql>password</sql>
</field>
</fields>
</class>
</classes>
</itop_design>

View File

@@ -45,4 +45,43 @@ class EMailTest extends ItopTestCase {
$oConfig->Set('email_transport', $sCurrentEmailTransport);
$oConfig->Set('email_asynchronous', $sCurrentEmailAsync);
}
/**
* @return void
* @throws \ConfigException
* @throws \CoreException
* @covers Email::SetBody()
* @covers Email::Send()
*/
public function testCheckPartsHeadersOnSendEmailWithAttachment(): void
{
$oConfig = utils::GetConfig();
$sCurrentEmailTransport = $oConfig->Get('email_transport');
$sCurrentEmailAsync = $oConfig->Get('email_asynchronous');
// Set our email transport to file, so we can read it after
$oConfig->Set('email_transport', 'LogFile');
$oConfig->Set('email_asynchronous', false);
$oEmail = new Email();
$oEmail->SetRecipientTO('email@email.com');
$oEmail->SetRecipientFrom('email2@email2.com');
$oEmail->SetSubject('dummy subject');
$oEmail->SetBody('dummy body', 'text/plain');
$oEmail->AddAttachment('Dummy attachment', 'attachment.txt', 'text/plain');
// Send the mail and check if there's any issue
$aIssues = [];
$oEmail->Send($aIssues);
$this->assertEmpty($aIssues);
// Check if our charset is correctly set
// We know this file may be used by other future test, but as we can't configure output filename, it is what it is
$sEmailContent = file_get_contents(APPROOT.'log/mail.log');
$this->assertStringContainsString('Content-Type: text/plain; charset=UTF-8', $sEmailContent);
// Set our previous email transport value back, so it doesn't affect other tests
$oConfig->Set('email_transport', $sCurrentEmailTransport);
$oConfig->Set('email_asynchronous', $sCurrentEmailAsync);
}
}

View File

@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace Combodo\iTop\Test\UnitTest\Core;
use ArchivedObjectException;
use AttributeEncryptedString;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use CoreException;
use CoreUnexpectedValue;
use Exception;
use MetaModel;
use ormLinkSet;
use PasswordTest;
use RestResultWithObjects;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class RestServicesSanitizeOutputTest extends ItopCustomDatamodelTestCase
{
private const SIMPLE_PASSWORD = '123456';
/**
* @throws Exception
*/
protected function setUp(): void
{
parent::setUp();
// Workaround to cope with inconsistent settings in itop-config files from the CI
AttributeEncryptedString::$sKey = '6eb9d9afa3ee0fbcebe622a33bf57aaeafb7c37998fd24c403c2522c2d60117f';
}
/**
* @return void
* @throws CoreException
*/
public function testSanitizeAttributeOnRequestedObject()
{
$oContactTest = MetaModel::NewObject('ContactTest', [
'password' => self::SIMPLE_PASSWORD
]
);
$oRestResultWithObject = new RestResultWithObjects();
$oRestResultWithObject->AddObject(0, 'ok', $oContactTest, ['ContactTest' => ['password']]);
$oRestResultWithObject->SanitizeContent();
static::assertJsonStringEqualsJsonString(
'{"objects":{"ContactTest::-1":{"code":0,"message":"ok","class":"ContactTest","key":-1,"fields":{"password":"*****"}}},"code":0,"message":null}',
json_encode($oRestResultWithObject));
}
/**
* @return void
* @throws Exception
*/
public function testSanitizeAttributeExternalFieldOnLink()
{
$oContactTest = $this->createObject('ContactTest', [
'password' => self::SIMPLE_PASSWORD
]
);
$oTestServer = $this->createObject('TestServer', [
'name' => 'test_server',
]);
// create lnkContactTestToServer
$oLnkContactTestToServer = $this->createObject('lnkContactTestToServer', [
'contact_test_id' => $oContactTest->GetKey(),
'test_server_id' => $oTestServer->GetKey()
]);
$oRestResultWithObject = new RestResultWithObjects();
$oRestResultWithObject->AddObject(0, 'ok', $oLnkContactTestToServer,
['lnkContactTestToServer' => ['contact_test_password']]);
$oRestResultWithObject->SanitizeContent();
static::assertStringContainsString(
'*****',
json_encode($oRestResultWithObject));
static::assertStringNotContainsString(
self::SIMPLE_PASSWORD,
json_encode($oRestResultWithObject));
}
/**
* @throws Exception
*/
public function testSanitizeAttributeOnObjectRelatedThroughNNRelation()
{
$oContactTest = $this->createObject('ContactTest', [
'password' => self::SIMPLE_PASSWORD
]);
$oTestServer = $this->createObject('TestServer', [
'name' => 'test_server',
]);
// create lnkContactTestToServer
$this->createObject('lnkContactTestToServer', [
'contact_test_id' => $oContactTest->GetKey(),
'test_server_id' => $oTestServer->GetKey()
]);
$oTestServer->Reload();
$oRestResultWithObject = new RestResultWithObjects();
$oRestResultWithObject->AddObject(0, 'ok', $oTestServer,
['TestServer' => ['contact_list']]);
$oRestResultWithObject->SanitizeContent();
static::assertStringContainsString(
'*****',
json_encode($oRestResultWithObject));
static::assertStringNotContainsString(
self::SIMPLE_PASSWORD,
json_encode($oRestResultWithObject));
}
/**
* @throws CoreException
* @throws CoreUnexpectedValue
* @throws ArchivedObjectException
* @throws Exception
*/
public function testSanitizeOnObjectRelatedThrough1NRelation()
{
$oTestServer = $this->createObject('TestServer', [
'name' => 'my_server',
]);
$oPassword = new PasswordTest();
$oPassword->Set('password', self::SIMPLE_PASSWORD);
$oPassword->Set('server_test_id', $oTestServer->GetKey());
/** @var ormLinkSet $oContactList */
$oContactList = $oTestServer->Get('password_list');
$oContactList->AddItem($oPassword);
$oTestServer->Set('password_list', $oContactList);
$oRestResultWithObject = new RestResultWithObjects();
$oRestResultWithObject->AddObject(0, 'ok', $oTestServer, ['TestServer' => ['id', 'password_list']]);
$oRestResultWithObject->SanitizeContent();
static::assertStringContainsString(
'*****',
json_encode($oRestResultWithObject));
static::assertStringNotContainsString(
self::SIMPLE_PASSWORD,
json_encode($oRestResultWithObject));
}
/**
* @return string Abs path to the XML delta to use for the tests of that class
*/
public function GetDatamodelDeltaAbsPath(): string
{
return __DIR__.'/Delta/delta_test_sanitize_output.xml';
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use CoreException;
use CoreServices;
use CoreUnexpectedValue;
use RestResultWithObjects;
use UserLocal;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class RestServicesTest extends ItopDataTestCase
{
/**
* @return void
* @dataProvider providerTestSanitizeJsonInput
*/
public function testSanitizeJsonInput($sJsonData, $sExpectedJsonDataSanitized)
{
$oRS = new CoreServices();
$sOutputJson = $oRS->SanitizeJsonInput($sJsonData);
static::assertJsonStringEqualsJsonString($sExpectedJsonDataSanitized, $sOutputJson);
}
/**
* @return array[]
*/
public function providerTestSanitizeJsonInput(): array
{
return [
'core/check_credentials' => [
'{"operation": "core/check_credentials", "user": "admin", "password": "admin"}',
'{
"operation": "core/check_credentials",
"user": "admin",
"password": "*****"
}'
],
'core/update' => [
'{"operation": "core/update", "comment": "Update user", "class": "UserLocal", "key": {"id":1}, "output_fields": "first_name, password", "fields": {"password" : "123456"}}',
'{
"operation": "core/update",
"comment": "Update user",
"class": "UserLocal",
"key": {
"id": 1
},
"output_fields": "first_name, password",
"fields": {
"password": "*****"
}
}'
],
'core/create' => [
'{"operation": "core/create", "comment": "Create user", "class": "UserLocal", "fields": {"first_name": "John", "last_name": "Doe", "email": "jd@example/com", "password" : "123456"}}',
'{
"operation": "core/create",
"comment": "Create user",
"class": "UserLocal",
"fields": {
"first_name": "John",
"last_name": "Doe",
"email": "jd@example/com",
"password": "*****"
}
}'
],
];
}
/**
* @param $sOperation
* @param $aJsonData
* @param $sExpectedJsonDataSanitized
* @return void
* @throws CoreException
* @throws CoreUnexpectedValue
* @dataProvider providerTestSanitizeJsonOutput
*/
public function testSanitizeJsonOutput($sOperation, $aJsonData, $sExpectedJsonDataSanitized)
{
$oUser = new UserLocal();
$oUser->Set('password', '123456');
$oRestResultWithObject = new RestResultWithObjects();
$oRestResultWithObject->AddObject(0, 'ok', $oUser, ['UserLocal' => ['login', 'password']]);
$oRestResultWithObject->SanitizeContent();
static::assertJsonStringEqualsJsonString($sExpectedJsonDataSanitized, json_encode($oRestResultWithObject));
}
/**
* @return array[]
*/
public function providerTestSanitizeJsonOutput(): array
{
return [
'core/update' => [
'core/update',
['comment' => 'Update user', 'class' => 'UserLocal', 'key' => ['login' => 'my_example'], 'output_fields' => 'password', 'fields' => ['password' => 'opkB!req57']],
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
],
'core/create' => [
'core/create',
['comment' => 'Create user', 'class' => 'UserLocal', 'fields' => ['password' => 'Azertyuiiop*12', 'login' => 'toto', 'profile_list' => [1]]],
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
],
'core/get' => [
'core/get',
['comment' => 'Get user', 'class' => 'UserLocal', 'key' => ['login' => 'my_example'], 'output_fields' => 'first_name, password'],
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
],
'core/check_credentials' => [
'core/check_credentials',
['user' => 'admin', 'password' => 'admin'],
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
],
];
}
}

View File

@@ -47,5 +47,8 @@
<div>['echo',1]|sort('system')|join</div>
{{ ['echo',1]|sort('system')|join }}
<div>[['id','']|sort('system')</div>
{{['id','']|sort('system')|join}}
POST /subscribe?0=cat+/etc/passwd HTTP/1.1
email="{{ app.request.query.filter(0,0,1024,{'options':'system'}) }}"@attacker.tld

View File

@@ -224,7 +224,11 @@ try
/** @var iRestServiceProvider $oRS */
$oRS = $aOpToRestService[$sOperation]['service_provider'];
$sProvider = get_class($oRS);
if ($oRS instanceof iRestInputSanitizer) {
$sSanitizedJsonInput = $oRS->SanitizeJsonInput($sJsonString);
}
CMDBObject::SetTrackOrigin('webservice-rest');
$oResult = $oRS->ExecOperation($sVersion, $sOperation, $aJsonData);
}
@@ -249,6 +253,7 @@ catch(Exception $e)
//
$sResponse = json_encode($oResult);
if ($sResponse === false)
{
$oJsonIssue = new RestResult();
@@ -280,7 +285,7 @@ if (MetaModel::GetConfig()->Get('log_rest_service'))
$oLog->SetTrim('userinfo', UserRights::GetUser());
$oLog->Set('version', $sVersion);
$oLog->Set('operation', $sOperation);
$oLog->SetTrim('json_input', $sJsonString);
$oLog->SetTrim('json_input', $sSanitizedJsonInput ?? $sJsonString);
$oLog->Set('provider', $sProvider);
$sMessage = $oResult->message;
@@ -290,7 +295,8 @@ if (MetaModel::GetConfig()->Get('log_rest_service'))
}
$oLog->SetTrim('message', $sMessage);
$oLog->Set('code', $oResult->code);
$oLog->SetTrim('json_output', $sResponse);
$oResult->SanitizeContent();
$oLog->SetTrim('json_output', json_encode($oResult, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
$oLog->DBInsertNoReload();
}