Compare commits

...

86 Commits

Author SHA1 Message Date
acognet
2363f2fb21 Merge remote-tracking branch 'origin/develop' into feature/4157
# Conflicts:
#	application/ui.linkswidget.class.inc.php
#	core/dbobject.class.php
2022-09-29 17:49:10 +02:00
Eric Espie
f32f36fc74 N°5551 - System information database size is way off 2022-09-26 11:51:51 +02:00
bdalsass
5157f511fc N°5073 - Implements line actions in a datatable (#337)
* datatable row actions

Below is a sample of extra param to enable feature:

		$aExtraParams['row_actions'] = [
			[
				'tooltip'        => 'add an element',
				'icon_css_class' => 'fa-plus',
				'css_class'      => 'ibo-is-success',
				'level'          => 'secondary',
				'on_action_js'   => 'console.log(aData);',
			],
			[
				'tooltip'        => 'remove an element',
				'icon_css_class' => 'fa-minus',
				'css_class'      => 'ibo-is-danger',
				'level'          => 'secondary',
				'on_action_js'   => 'console.log("You clicked the remove button");',
			],
			[
				'tooltip'        => 'open in new tab',
				'icon_css_class' => 'fa-external-link-square-alt',
				'on_action_js'   => 'window.open("http://localhost/itop-branchs/dev/pages/UI.php?operation=details&class=UserRequest&id=" + aData.id + "&c[menu]=UserRequest%3AOpenRequests");',
			],
			[
				'tooltip'        => 'other actions',
				'icon_css_class' => 'fa-ellipsis-v',
				'on_action_js'   => 'console.log(event);',
			],
		];

* datatable row actions (update)

* datatable row actions (update)

* datatable row actions (add template role)

* datatable row actions (align actions)

* datatable row actions (change template factory make to make standard)

* datatable row actions (use trait to handle row actions)

* datatable row actions (row actions templates)

* datatable row actions (row actions templates)

* datatable row actions (row actions templates)

* datatable row actions (extends to static and form)

* datatable row actions (extends to static and form)

* datatable row actions (code review S)

* datatable row actions (code review S)

* datatable row actions (code review S)

* Update js/dataTables.main.js

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

* Update js/dataTables.main.js

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

* Update sources/Application/UI/Base/Component/DataTable/StaticTable/StaticTable.php

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

* Update templates/base/components/datatable/row-actions/handler.js.twig

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

* datatable row actions (code review M)

* Update js/dataTables.main.js

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

* Update js/dataTables.main.js

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

* Update js/dataTables.main.js

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

* Update sources/Application/UI/Base/Component/DataTable/StaticTable/StaticTable.php

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

* Update sources/Application/UI/Base/Component/DataTable/StaticTable/StaticTable.php

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

* Update sources/Application/UI/Base/Component/DataTable/StaticTable/StaticTable.php

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

* Update js/dataTables.main.js

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

* Update sources/Application/UI/Base/Component/DataTable/DataTableUIBlockFactory.php

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

* Update sources/Application/UI/Base/Component/DataTable/StaticTable/StaticTable.php

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

* Update application/utils.inc.php

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

* datatable row actions (code review M2)

* datatable row actions (code review M3)

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2022-09-26 08:20:28 +02:00
Eric Espie
3196e105a1 N°4756 - small adjustments 2022-09-23 15:10:21 +02:00
Eric Espie
b9d865f881 N°4756 - Fix DBInsert/NoReload call order 2022-09-21 18:08:44 +02:00
Molkobain
2e39a650eb Merge remote-tracking branch 'origin/support/3.0' into develop 2022-09-21 17:48:57 +02:00
Molkobain
c753b57265 Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2022-09-21 16:23:42 +02:00
Molkobain
583ab98210 Fix typo 2022-09-21 16:11:24 +02:00
Pierre Goiffon
e4f6a02de6 Merge remote-tracking branch 'origin/support/3.0' into develop
# Conflicts:
#	lib/composer/autoload_files.php
#	lib/composer/autoload_real.php
#	lib/composer/autoload_static.php
2022-09-21 14:28:25 +02:00
Pierre Goiffon
a5b5518533 Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	core/cmdbsource.class.inc.php
#	test/core/CMDBSource/TransactionsTest.php
2022-09-21 14:21:28 +02:00
Pierre Goiffon
88d743b1cc N°5538 Make PHPUnit test fail if transaction opened but not closed 2022-09-21 14:05:27 +02:00
Pierre Goiffon
7ac4bc95bb ItopDataTestCase : improve log message 2022-09-21 11:51:07 +02:00
Eric Espie
fac455da48 N°4756 - Global event listeners 2022-09-21 10:34:38 +02:00
odain
b01627f39d Merge branch 'saas/3.0' into develop 2022-09-20 16:04:55 +02:00
odain-cbd
766c9f0e7e N°5305 - CSV import ergonomy PR (#332)
Reworked UI feedbacks on following attributes:
- enum
- date
- external key
2022-09-20 16:00:33 +02:00
Eric Espie
d1414a3f34 N°4756 - revert events for datamodel/2.x objects for backward compatibility 2022-09-20 15:35:59 +02:00
Molkobain
6386a302b2 Merge remote-tracking branch 'origin/support/3.0' into develop
# Conflicts:
#	composer.json
2022-09-20 13:44:11 +02:00
Molkobain
7071712a0a N°5535 - Fix PHP max version to 8.1 in composer for iTop 3.0.2+ 2022-09-20 13:42:45 +02:00
Molkobain
31454b2946 Merge remote-tracking branch 'origin/support/3.0' into develop
# Conflicts:
#	core/config.class.inc.php
#	setup/setuputils.class.inc.php
2022-09-20 13:27:20 +02:00
Molkobain
e55ac6002a Increase ITOP_VERSION to 3.0.3-dev 2022-09-20 13:22:18 +02:00
Molkobain
e9c6549847 N°5535 - Fix PHP not validated version to 8.1 in iTop 3.0.2+ 2022-09-20 13:17:59 +02:00
Eric Espie
4aad555649 N°4756 - Fix unit tests, 2022-09-20 12:18:53 +02:00
Eric Espie
93ee565d29 N°4756 - Fix unit tests 2022-09-19 16:59:51 +02:00
Eric Espie
6c097a128b N°4756 - Fix unit tests 2022-09-19 16:49:40 +02:00
Eric Espie
eea3f78cec N°4756 - Fix unit tests 2022-09-19 16:28:13 +02:00
Eric Espie
6c4caf64c8 N°4756 - Fix unit tests 2022-09-19 16:26:02 +02:00
Eric Espie
88f0013330 N°4756 - Fix unit tests 2022-09-19 16:09:02 +02:00
Eric Espie
aa31da34e5 N°5389 - Email Notification templates with AttributeLinkedSetIndirect failed 2022-09-19 15:30:24 +02:00
Eric Espie
71464f6d0e Merge branch 'develop' into feature/faf_event_service
# Conflicts:
#	core/cmdbobject.class.inc.php
#	core/dbobject.class.php
#	core/designdocument.class.inc.php
#	lib/composer/autoload_files.php
#	lib/composer/autoload_static.php
2022-09-19 09:45:08 +02:00
acognet
c7eea3f51f N°4157 - Relations, modify one and add it again fails, the adding is ignored - Dont-work. It's a idee to solve the problem of display modified link, but actually there is a js bug. 2022-07-08 12:24:53 +02:00
acognet
5a77159ece N°4157 - Relations, modify one and add it again fails, the adding is ignored - check uniqueness rule before saving object 2022-07-08 12:15:46 +02:00
Eric Espie
2c265aab44 N°4756 - Ease extensibility for CRUD operations
- Change event order in CRUD
 - Add LinkHostObject for link update
 - Add events EVENT_SERVICE_DB_ARCHIVE and  EVENT_SERVICE_DB_UNARCHIVE
2022-07-08 10:19:54 +02:00
Eric Espie
fe28319d22 Merge from Develop 2022-07-06 14:28:55 +02:00
Eric Espie
7f6f5c0c3b Refactor event name 2022-06-30 16:24:13 +02:00
Eric Espie
682ab44dea Merge branch 'develop' into feature/faf_event_service 2022-06-30 14:22:52 +02:00
Eric Espie
6aef59e42d Merge branch 'develop' into feature/faf_event_service
# Conflicts:
#	core/dbobject.class.php
#	datamodels/2.x/itop-config/config.php
#	lib/composer/autoload_classmap.php
#	lib/composer/autoload_static.php
#	lib/composer/installed.php
2022-06-08 16:51:44 +02:00
Eric Espie
86024107af deprecated includes 2022-06-08 15:55:20 +02:00
Eric Espie
754f87fd0c debug log 2022-06-08 15:54:27 +02:00
Eric Espie
972e894bc5 debug log 2022-06-02 17:32:15 +02:00
Eric Espie
86a2db7e7f Remove AFTER_DISPLAY_PAGE Event 2022-06-02 17:23:07 +02:00
Eric Espie
4c31081de2 Remove AFTER_DISPLAY_PAGE Event 2022-06-02 17:19:43 +02:00
Eric Espie
23c95ebbf3 Block if event is not registered 2022-06-02 13:48:59 +02:00
Eric Espie
e77f21a0b5 refactor Event Listeners 2022-06-01 14:29:43 +02:00
Eric Espie
35e1f080b8 Attachments events 2022-06-01 11:46:00 +02:00
Eric Espie
812c1f6bb4 Migrate datamodel 2022-06-01 11:19:46 +02:00
Eric Espie
9adb7f20ce Event "Compute Values" 2022-06-01 09:11:03 +02:00
Eric Espie
c7e54c66c8 Event "Compute Values" 2022-06-01 09:09:16 +02:00
Eric Espie
f6855b0d2b Display events in the datamodel page 2022-06-01 08:44:12 +02:00
Eric Espie
1ceef602f0 Remove debug 2022-05-31 15:44:56 +02:00
Eric Espie
93cc29f4d9 refactor 2022-05-31 15:42:09 +02:00
Eric Espie
c9317542c8 refactor 2022-05-31 15:37:29 +02:00
Eric Espie
aed8337c51 Protection against reentrance for DBUpdate 2022-05-30 17:03:47 +02:00
Eric Espie
af4a5e1b8d New CRUD behaviour (removed Reload in DBInsert and DBUpdate) and protection against reentrance 2022-05-27 17:46:10 +02:00
Eric Espie
e7c09c83f0 internal doc 2022-05-25 10:59:38 +02:00
Eric Espie
56103d1952 Refactor 2022-05-25 10:38:07 +02:00
Eric Espie
301c308fec Refactor 2022-05-25 10:32:41 +02:00
Eric Espie
b827c68187 Merge branch 'develop' into feature/faf_event_service 2022-05-25 10:04:54 +02:00
Eric Espie
8ba28adf68 Refactor 2022-04-08 15:10:58 +02:00
Eric Espie
34a26d33a1 Fix reloads 2022-04-07 18:15:18 +02:00
Eric Espie
b0a55e057b Events to cmdbAbstract 2022-04-07 17:02:19 +02:00
Eric Espie
5ac9b05b2d new CRUD 2022-04-06 23:51:21 +02:00
Eric Espie
63e582a07f Unit tests 2022-04-05 10:30:47 +02:00
Eric Espie
470076daa2 Unit tests 2022-04-05 10:29:52 +02:00
Eric Espie
f6d92a189b CRUD reentrance protection 2022-04-05 10:28:12 +02:00
Eric Espie
c788c93542 Merge branch 'develop' into feature/faf_event_service 2022-03-30 08:22:47 +02:00
Eric Espie
a773f0d8a2 EventService: refactoring 2022-03-15 17:53:38 +01:00
Eric Espie
29c6b73d93 EventService: refactoring 2022-03-15 17:50:13 +01:00
Eric Espie
5b52ca4776 EventService: phpdoc 2022-03-15 15:45:03 +01:00
Eric Espie
8ddaf1b731 EventService: call FireEvent with only one parameter (PSR14) 2022-03-15 15:41:55 +01:00
Eric Espie
964ce44577 EventService: code cleanup 2022-03-15 15:11:57 +01:00
Eric Espie
cea6c557ce Merge branch 'develop' into feature/faf_event_service 2022-03-15 10:52:45 +01:00
Eric Espie
99819527db Changed the <Hooks> grammar (typo) 2022-02-07 16:28:19 +01:00
Eric Espie
965273009c Changed the <Hooks> grammar to allow module extensibility 2022-02-07 16:10:00 +01:00
Eric Espie
7bee616b1b Update application events definition 2022-02-07 15:25:38 +01:00
Eric Espie
f5302133d9 Add application events definition 2022-02-07 15:06:00 +01:00
Eric Espie
bf2aba1b06 Reformat code 2022-02-07 14:11:00 +01:00
Eric Espie
c04beea38c OnInsert and OnUpdate replacement 2021-12-31 17:07:59 +01:00
Eric Espie
93d88cca37 refactor 2021-12-31 15:26:54 +01:00
Eric Espie
0997750816 cleanup 2021-12-31 15:06:27 +01:00
Eric Espie
427c8b0794 * better comparison 2021-12-31 14:33:23 +01:00
Eric Espie
06008ed8eb * Add KPI object loaded counter 2021-12-31 12:12:32 +01:00
Eric Espie
374b71c017 * refactor
* Add event AFTER_DISPLAY_PAGE
2021-12-31 11:06:03 +01:00
Eric Espie
fba78e7d9b Changed event name to DISPLAY_OBJECT_DETAILS 2021-12-31 08:53:36 +01:00
Eric Espie
551abc861e Merge branch 'develop' into feature/faf_event_service
# Conflicts:
#	application/cmdbabstract.class.inc.php
#	application/loginwebpage.class.inc.php
#	core/dbobject.class.php
#	core/log.class.inc.php
#	lib/composer/autoload_classmap.php
#	lib/composer/autoload_static.php
#	setup/compiler.class.inc.php
#	test/phpunit.xml.dist
2021-12-31 08:42:16 +01:00
Eric
78f51d40f6 DBObject GetValues() 2020-07-17 15:41:52 +02:00
Eric
8bfc54e6b4 Event Service 2020-07-15 14:42:33 +02:00
111 changed files with 4952 additions and 1044 deletions

View File

@@ -299,6 +299,7 @@ abstract class AbstractPreferencesExtension implements iPreferencesExtension
*
* @api
* @package Extensibility
* @deprecated
*/
interface iApplicationUIExtension
{
@@ -441,6 +442,7 @@ interface iApplicationUIExtension
* @api
* @package Extensibility
* @since 2.7.0
* @deprecated
*/
abstract class AbstractApplicationUIExtension implements iApplicationUIExtension
{
@@ -2092,4 +2094,4 @@ class RestUtils
interface iModuleExtension
{
public function __construct();
}
}

View File

@@ -1,6 +1,6 @@
<?php
/*
* @copyright Copyright (C) 2010-2021 Combodo SARL
* @copyright Copyright (C) 2010-2022 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
@@ -42,6 +42,7 @@ use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
use Combodo\iTop\Renderer\BlockRenderer;
use Combodo\iTop\Renderer\Console\ConsoleFormRenderer;
define('OBJECT_PROPERTIES_TAB', 'ObjectProperties');
define('HILIGHT_CLASS_CRITICAL', 'red');
@@ -676,33 +677,26 @@ HTML
$sTargetClass = $oLinkingAttDef->GetTargetClass();
// n:n links => must be allowed to modify the linking class AND read the target class in order to edit the linkedset
if (!UserRights::IsActionAllowed($sLinkedClass,
UR_ACTION_MODIFY) || !UserRights::IsActionAllowed($sTargetClass, UR_ACTION_READ))
{
UR_ACTION_MODIFY) || !UserRights::IsActionAllowed($sTargetClass, UR_ACTION_READ)) {
$iFlags |= OPT_ATT_READONLY;
}
// n:n links => must be allowed to read the linking class AND the target class in order to display the linkedset
if (!UserRights::IsActionAllowed($sLinkedClass,
UR_ACTION_READ) || !UserRights::IsActionAllowed($sTargetClass, UR_ACTION_READ))
{
UR_ACTION_READ) || !UserRights::IsActionAllowed($sTargetClass, UR_ACTION_READ)) {
$iFlags |= OPT_ATT_HIDDEN;
}
}
else
{
} else {
// 1:n links => must be allowed to modify the linked class in order to edit the linkedset
if (!UserRights::IsActionAllowed($sLinkedClass, UR_ACTION_MODIFY))
{
if (!UserRights::IsActionAllowed($sLinkedClass, UR_ACTION_MODIFY)) {
$iFlags |= OPT_ATT_READONLY;
}
// 1:n links => must be allowed to read the linked class in order to display the linkedset
if (!UserRights::IsActionAllowed($sLinkedClass, UR_ACTION_READ))
{
if (!UserRights::IsActionAllowed($sLinkedClass, UR_ACTION_READ)) {
$iFlags |= OPT_ATT_HIDDEN;
}
}
// Non-readable/hidden linkedset... don't display anything
if ($iFlags & OPT_ATT_HIDDEN)
{
if ($iFlags & OPT_ATT_HIDDEN) {
continue;
}
@@ -711,17 +705,13 @@ HTML
$aArgs = array('this' => $this);
$bReadOnly = ($iFlags & (OPT_ATT_READONLY | OPT_ATT_SLAVE));
if ($bEditMode && (!$bReadOnly))
{
if ($bEditMode && (!$bReadOnly)) {
$sInputId = $this->m_iFormId.'_'.$sAttCode;
if ($oAttDef->IsIndirect())
{
if ($oAttDef->IsIndirect()) {
$oLinkingAttDef = MetaModel::GetAttributeDef($sLinkedClass, $oAttDef->GetExtKeyToRemote());
$sTargetClass = $oLinkingAttDef->GetTargetClass();
}
else
{
} else {
$sTargetClass = $sLinkedClass;
}
@@ -731,7 +721,7 @@ HTML
$sDisplayValue = ''; // not used
$sHTMLValue = "<span id=\"field_{$sInputId}\">".self::GetFormElementForField($oPage, $sClass, $sAttCode,
$oAttDef, $oLinkSet, $sDisplayValue, $sInputId, '', $iFlags, $aArgs).'</span>';
$oAttDef, $oOrmLinkSet, $sDisplayValue, $sInputId, '', $iFlags, $aArgs).'</span>';
$this->AddToFieldsMap($sAttCode, $sInputId);
$oPage->add($sHTMLValue);
}
@@ -2376,8 +2366,7 @@ EOF
case 'LinkedSet':
$sInputType = self::ENUM_INPUT_TYPE_LINKEDSET;
if ($oAttDef->IsIndirect()) {
$oWidget = new UILinksWidget($sClass, $sAttCode, $iId, $sNameSuffix,
$oAttDef->DuplicatesAllowed());
$oWidget = new UILinksWidget($sClass, $sAttCode, $iId, $sNameSuffix, $oAttDef->DuplicatesAllowed());
} else {
$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iId, $sNameSuffix);
}
@@ -4024,18 +4013,14 @@ HTML;
$this->Set($sAttCode, $value);
break;
case 'LinkedSet':
if ($this->IsValueModified($value))
{
if ($this->IsValueModified($value)) {
$oLinkSet = $this->Get($sAttCode);
$sLinkedClass = $oAttDef->GetLinkedClass();
if (array_key_exists('to_be_created', $value) && (count($value['to_be_created']) > 0))
{
if (array_key_exists('to_be_created', $value) && (count($value['to_be_created']) > 0)) {
// Now handle the links to be created
foreach ($value['to_be_created'] as $aData)
{
foreach ($value['to_be_created'] as $aData) {
$sSubClass = $aData['class'];
if (($sLinkedClass == $sSubClass) || (is_subclass_of($sSubClass, $sLinkedClass)))
{
if (($sLinkedClass == $sSubClass) || (is_subclass_of($sSubClass, $sLinkedClass))) {
$aObjData = $aData['data'];
$oLink = MetaModel::NewObject($sSubClass);
$oLink->UpdateObjectFromArray($aObjData);
@@ -4247,28 +4232,20 @@ HTML;
case 'LinkedSet':
/** @var AttributeLinkedSet $oAttDef */
$aRawToBeCreated = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbc", '{}',
'raw_data'), true);
$aRawToBeCreated = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbc", '{}', 'raw_data'), true);
$aToBeCreated = array();
foreach($aRawToBeCreated as $aData)
{
foreach ($aRawToBeCreated as $aData) {
$sSubFormPrefix = $aData['formPrefix'];
$sObjClass = isset($aData['class']) ? $aData['class'] : $oAttDef->GetLinkedClass();
$aObjData = array();
foreach($aData as $sKey => $value)
{
if (preg_match("/^attr_$sSubFormPrefix(.*)$/", $sKey, $aMatches))
{
foreach ($aData as $sKey => $value) {
if (preg_match("/^attr_$sSubFormPrefix(.*)$/", $sKey, $aMatches)) {
$oLinkAttDef = MetaModel::GetAttributeDef($sObjClass, $aMatches[1]);
// Recursing over n:n link datetime attributes
// Note: We might need to do it with other attribute types, like Document or redundancy setting.
if ($oLinkAttDef instanceof AttributeDateTime)
{
$aObjData[$aMatches[1]] = $this->PrepareValueFromPostedForm($sSubFormPrefix,
$aMatches[1], $sObjClass, $aData);
}
else
{
if ($oLinkAttDef instanceof AttributeDateTime) {
$aObjData[$aMatches[1]] = $this->PrepareValueFromPostedForm($sSubFormPrefix, $aMatches[1], $sObjClass, $aData);
} else {
$aObjData[$aMatches[1]] = $value;
}
}
@@ -4276,28 +4253,20 @@ HTML;
$aToBeCreated[] = array('class' => $sObjClass, 'data' => $aObjData);
}
$aRawToBeModified = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbm", '{}',
'raw_data'), true);
$aRawToBeModified = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbm", '{}', 'raw_data'), true);
$aToBeModified = array();
foreach($aRawToBeModified as $iObjKey => $aData)
{
foreach ($aRawToBeModified as $iObjKey => $aData) {
$sSubFormPrefix = $aData['formPrefix'];
$sObjClass = isset($aData['class']) ? $aData['class'] : $oAttDef->GetLinkedClass();
$aObjData = array();
foreach($aData as $sKey => $value)
{
if (preg_match("/^attr_$sSubFormPrefix(.*)$/", $sKey, $aMatches))
{
foreach ($aData as $sKey => $value) {
if (preg_match("/^attr_$sSubFormPrefix(.*)$/", $sKey, $aMatches)) {
$oLinkAttDef = MetaModel::GetAttributeDef($sObjClass, $aMatches[1]);
// Recursing over n:n link datetime attributes
// Note: We might need to do it with other attribute types, like Document or redundancy setting.
if ($oLinkAttDef instanceof AttributeDateTime)
{
$aObjData[$aMatches[1]] = $this->PrepareValueFromPostedForm($sSubFormPrefix,
$aMatches[1], $sObjClass, $aData);
}
else
{
if ($oLinkAttDef instanceof AttributeDateTime) {
$aObjData[$aMatches[1]] = $this->PrepareValueFromPostedForm($sSubFormPrefix, $aMatches[1], $sObjClass, $aData);
} else {
$aObjData[$aMatches[1]] = $value;
}
}
@@ -4306,14 +4275,11 @@ HTML;
}
$value = array(
'to_be_created' => $aToBeCreated,
'to_be_created' => $aToBeCreated,
'to_be_modified' => $aToBeModified,
'to_be_deleted' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbd", '[]',
'raw_data'), true),
'to_be_added' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tba", '[]',
'raw_data'), true),
'to_be_removed' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbr", '[]',
'raw_data'), true),
'to_be_deleted' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbd", '[]', 'raw_data'), true),
'to_be_added' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tba", '[]', 'raw_data'), true),
'to_be_removed' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbr", '[]', 'raw_data'), true),
);
break;
@@ -4479,13 +4445,13 @@ HTML;
// Protection against reentrance (e.g. cascading the update of ticket logs)
// Note: This is based on the fix made on r 3190 in DBObject::DBUpdate()
static $aUpdateReentrance = array();
$sKey = get_class($this).'::'.$this->GetKey();
if (array_key_exists($sKey, $aUpdateReentrance))
{
if (!MetaModel::StartReentranceProtection(Metamodel::REENTRANCE_TYPE_UPDATE, $this)) {
$sClass = get_class($this);
$sKey = $this->GetKey();
IssueLog::Debug("CRUD: DBUpdate $sClass::$sKey Rejected (reentrance)", LogChannels::DM_CRUD);
return $res;
}
$aUpdateReentrance[$sKey] = true;
try
{
@@ -4496,13 +4462,13 @@ HTML;
$oExtensionInstance->OnDBUpdate($this, self::GetCurrentChange());
}
}
catch (Exception $e)
{
throw $e;
}
finally
{
unset($aUpdateReentrance[$sKey]);
MetaModel::StopReentranceProtection(Metamodel::REENTRANCE_TYPE_UPDATE, $this);
}
if ($this->IsModified()) {
return $this->DBUpdate();
}
return $res;
@@ -5708,4 +5674,117 @@ JS
'AttributeOneWayPassword',
);
}
/**
* @return void
* @throws \CoreException
*/
final protected function EventInsertRequested()
{
$this->FireEvent(EVENT_SERVICE_DB_INSERT_REQUESTED);
}
/**
* @return void
* @throws \CoreException
*/
final protected function EventInsertBefore()
{
$this->FireEvent(EVENT_SERVICE_DB_ABOUT_TO_INSERT);
}
/**
* @return void
* @throws \CoreException
*/
final protected function EventInsertAfter()
{
$this->FireEvent(EVENT_SERVICE_DB_INSERT_DONE);
}
final protected function EventComputeValues()
{
$this->FireEvent(EVENT_SERVICE_DB_COMPUTE_VALUES);
}
/**
* @param array $aEventData
*
* @return void
* @throws \CoreException
*/
final protected function EventCheckToWrite(array $aEventData)
{
$this->FireEvent(EVENT_SERVICE_DB_CHECK_TO_WRITE, $aEventData);
}
/**
* @param array $aEventData
*
* @return void
* @throws \CoreException
*/
final protected function EventCheckToDelete(array $aEventData)
{
$this->FireEvent(EVENT_SERVICE_DB_CHECK_TO_DELETE, $aEventData);
}
/**
* @return void
* @throws \CoreException
*/
final protected function EventUpdateRequested()
{
$this->FireEvent(EVENT_SERVICE_DB_UPDATE_REQUESTED);
}
/**
* @return void
* @throws \CoreException
*/
final protected function EventUpdateBefore()
{
$this->FireEvent(EVENT_SERVICE_DB_ABOUT_TO_UPDATE);
}
/**
* @param array $aEventData
*
* @return void
* @throws \CoreException
*/
final protected function EventUpdateAfter(array $aEventData)
{
$this->FireEvent(EVENT_SERVICE_DB_UPDATE_DONE, $aEventData);
}
/**
* @return void
* @throws \CoreException
*/
final protected function EventDeleteBefore()
{
$this->FireEvent(EVENT_SERVICE_DB_ABOUT_TO_DELETE);
}
/**
* @return void
* @throws \CoreException
*/
final protected function EventDeleteAfter()
{
$this->FireEvent(EVENT_SERVICE_DB_DELETE_DONE);
}
final protected function EventArchive()
{
$this->FireEvent(EVENT_SERVICE_DB_ARCHIVE);
}
final protected function EventUnarchive()
{
$this->FireEvent(EVENT_SERVICE_DB_UNARCHIVE);
}
}

View File

@@ -185,6 +185,384 @@
</style>
</menu>
</menus>
<events>
<event id="EVENT_SERVICE_DB_INSERT_REQUESTED" _delta="define">
<description>An object insert in the database has been requested. All changes to the object will be persisted automatically.</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<replaces>DBObject::OnInsert</replaces>
<event_data>
<event_datum id="object">
<description>The object inserted</description>
<type>DBObject</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_ABOUT_TO_INSERT" _delta="define">
<description>An object is about to be inserted in the database (no change possible)</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<replaces>DBObject::OnInsert</replaces>
<event_data>
<event_datum id="object">
<description>The object inserted</description>
<type>DBObject</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_INSERT_DONE" _delta="define">
<description>An object has been inserted into the database (but not reloaded). All changes to the object will be persisted automatically.</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<replaces>DBObject::AfterInsert</replaces>
<event_data>
<event_datum id="object">
<description>The object inserted</description>
<type>DBObject</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_UPDATE_REQUESTED" _delta="define">
<description>An object update has been requested. All changes to the object will be persisted automatically.</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<replaces>DBObject::OnUpdate, DBObject::DoComputeValues</replaces>
<event_data>
<event_datum id="object">
<description>The object updated</description>
<type>DBObject</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_ABOUT_TO_UPDATE" _delta="define">
<description>An object is about to be updated in the database (no change possible)</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<replaces>DBObject::OnUpdate</replaces>
<event_data>
<event_datum id="object">
<description>The object updated</description>
<type>DBObject</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_UPDATE_DONE" _delta="define">
<description>An object has been updated into the database and reloaded. All changes to the object will be persisted automatically.</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<replaces>DBObject::AfterUpdate</replaces>
<event_data>
<event_datum id="object">
<description>The object updated</description>
<type>DBObject</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_ABOUT_TO_DELETE" _delta="define">
<description>An object is about to be deleted in the database</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<replaces>DBObject::OnDelete</replaces>
<event_data>
<event_datum id="object">
<description>The object deleted</description>
<type>DBObject</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_DELETE_DONE" _delta="define">
<description>An object has been deleted into the database</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<replaces>DBObject::AfterDelete</replaces>
<event_data>
<event_datum id="object">
<description>The object deleted</description>
<type>DBObject</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_BEFORE_APPLY_STIMULUS" _delta="define">
<description>A stimulus is about to be applied to an object</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<event_data>
<event_datum id="object">
<description>The object where the stimulus is targeted</description>
<type>DBObject</type>
</event_datum>
<event_datum id="stimulus">
<description>Current stimulus applied</description>
<type>string</type>
</event_datum>
<event_datum id="previous_state">
<description>Object previous state</description>
<type>string</type>
</event_datum>
<event_datum id="new_state">
<description>Object new state</description>
<type>string</type>
</event_datum>
<event_datum id="save_object">
<description>The object must be saved in the database</description>
<type>boolean</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_AFTER_APPLY_STIMULUS" _delta="define">
<description>A stimulus has been applied to an object</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<event_data>
<event_datum id="object">
<description>The object where the stimulus is targeted</description>
<type>DBObject</type>
</event_datum>
<event_datum id="stimulus">
<description>Current stimulus applied</description>
<type>string</type>
</event_datum>
<event_datum id="previous_state">
<description>Object previous state</description>
<type>string</type>
</event_datum>
<event_datum id="new_state">
<description>Object new state</description>
<type>string</type>
</event_datum>
<event_datum id="save_object">
<description>The object is asked to be saved in the database</description>
<type>boolean</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_APPLY_STIMULUS_FAILED" _delta="define">
<description>A stimulus has failed</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<event_data>
<event_datum id="action">
<description>The action that failed to apply the stimulus</description>
<type>string</type>
</event_datum>
<event_datum id="object">
<description>The object where the stimulus is targeted</description>
<type>DBObject</type>
</event_datum>
<event_datum id="stimulus">
<description>Current stimulus applied</description>
<type>string</type>
</event_datum>
<event_datum id="previous_state">
<description>Object previous state</description>
<type>string</type>
</event_datum>
<event_datum id="new_state">
<description>Object new state</description>
<type>string</type>
</event_datum>
<event_datum id="save_object">
<description>The object must be saved in the database</description>
<type>boolean</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_OBJECT_RELOAD" _delta="define">
<description>An object has been re-loaded from the database</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<event_data>
<event_datum id="object">
<description>The object re-loaded</description>
<type>DBObject</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_COMPUTE_VALUES" _delta="define">
<description>An object needs to be recomputed after changes</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<replaces>DBObject::ComputeValues</replaces>
<event_data>
<event_datum id="object">
<description>The object inserted</description>
<type>DBObject</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_CHECK_TO_WRITE" _delta="define">
<description>Check an object before it is written into the database (no change possible)</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<replaces>cmdbAbstractObject::DoCheckToWrite</replaces>
<event_data>
<event_datum id="object">
<description>The object to check</description>
<type>DBObject</type>
</event_datum>
<event_datum id="error_messages">
<description>Array of strings where all the errors found during the object checking are added</description>
<type>array</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_CHECK_TO_DELETE" _delta="define">
<description>Check an object before it is deleted from the database (no change possible)</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<replaces>cmdbAbstractObject::DoCheckToDelete</replaces>
<event_data>
<event_datum id="object">
<description>The object to check</description>
<type>DBObject</type>
</event_datum>
<event_datum id="error_messages">
<description>Array of strings where all the errors found during the object checking are added</description>
<type>array</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_ARCHIVE" _delta="define">
<description>An object has been archived</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<event_data>
<event_datum id="object">
<description>The object archived</description>
<type>DBObject</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DB_UNARCHIVE" _delta="define">
<description>An object has been unarchived</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<event_data>
<event_datum id="object">
<description>The object unarchived</description>
<type>DBObject</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_DOWNLOAD_DOCUMENT" _delta="define">
<description>A document has been downloaded from the GUI</description>
<sources>
<source id="Document">Document</source>
</sources>
<event_data>
<event_datum id="object">
<description>The object containing the document</description>
<type>DBObject</type>
</event_datum>
<event_datum id="document">
<description>The document downloaded</description>
<type>ormDocument</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_LOGIN" _delta="define">
<description>Inform the listeners about the connection states</description>
<event_data>
<event_datum id="code">
<description>The login step result code (LoginWebPage::EXIT_CODE_...) </description>
<type>integer</type>
</event_datum>
<event_datum id="state">
<description>Current login state (LoginWebPage::LOGIN_STATE_CONNECTED...)</description>
<type>string</type>
</event_datum>
</event_data>
</event>
</events>
<meta>
<classes>
<class id="cmdbAbstractObject" _delta="define">

View File

@@ -0,0 +1,13 @@
<?php
/*
* @copyright Copyright (C) 2010-2021 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/**
* @since 2.7.8 3.0.3 3.1.0 N°5538
*/
class MySQLTransactionNotClosedException extends MySQLException
{
}

View File

@@ -59,6 +59,7 @@ class LoginBasic extends AbstractLoginFSMExtension
list($sAuthUser, $sAuthPwd) = $this->GetAuthUserAndPassword();
if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, Session::Get('login_mode'), 'internal'))
{
$_SESSION['auth_user'] = $sAuthUser;
$iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS;
return LoginWebPage::LOGIN_FSM_ERROR;
}

View File

@@ -42,6 +42,7 @@ class LoginExternal extends AbstractLoginFSMExtension
$sAuthUser = $this->GetAuthUser();
if (!UserRights::CheckCredentials($sAuthUser, '', Session::Get('login_mode'), 'external'))
{
$_SESSION['auth_user'] = $sAuthUser;
$iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS;
return LoginWebPage::LOGIN_FSM_ERROR;
}
@@ -88,4 +89,4 @@ class LoginExternal extends AbstractLoginFSMExtension
/** @var string $sAuthUser */
return $sAuthUser; // Retrieve the value
}
}
}

View File

@@ -68,6 +68,7 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension
$sAuthPwd = utils::ReadPostedParam('auth_pwd', null, 'raw_data');
if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, Session::Get('login_mode'), 'internal'))
{
$_SESSION['auth_user'] = $sAuthUser;
$iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS;
return LoginWebPage::LOGIN_FSM_ERROR;
}

View File

@@ -57,6 +57,7 @@ class LoginURL extends AbstractLoginFSMExtension
$sAuthPwd = utils::ReadParam('auth_pwd', null, false, 'raw_data');
if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, Session::Get('login_mode'), 'internal'))
{
$_SESSION['auth_user'] = $sAuthUser;
$iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS;
return LoginWebPage::LOGIN_FSM_ERROR;
}
@@ -92,4 +93,4 @@ class LoginURL extends AbstractLoginFSMExtension
}
return LoginWebPage::LOGIN_FSM_CONTINUE;
}
}
}

View File

@@ -26,6 +26,8 @@
use Combodo\iTop\Application\Branding;
use Combodo\iTop\Application\Helper\Session;
use Combodo\iTop\Service\EventData;
use Combodo\iTop\Service\EventService;
/**
* Web page used for displaying the login form
@@ -479,11 +481,13 @@ class LoginWebPage extends NiceWebPage
$iResponse = $oLoginFSMExtensionInstance->LoginAction($sLoginState, $iErrorCode);
if ($iResponse == self::LOGIN_FSM_RETURN)
{
EventService::FireEvent(new EventData(EVENT_SERVICE_LOGIN, null, ['code' => $iErrorCode, 'state' => $sLoginState]));
Session::WriteClose();
return $iErrorCode; // Asked to exit FSM, generally login OK
}
if ($iResponse == self::LOGIN_FSM_ERROR)
{
EventService::FireEvent(new EventData(EVENT_SERVICE_LOGIN, null, ['code' => $iErrorCode, 'state' => $sLoginState]));
$sLoginState = self::LOGIN_STATE_SET_ERROR; // Next state will be error
// An error was detected, skip the other plugins turn
break;
@@ -497,6 +501,7 @@ class LoginWebPage extends NiceWebPage
}
catch (Exception $e)
{
EventService::FireEvent(new EventData(EVENT_SERVICE_LOGIN, null, ['state' => $_SESSION['login_state']]));
IssueLog::Error($e->getTraceAsString());
static::ResetSession();
die($e->getMessage());

View File

@@ -108,15 +108,14 @@ class UILinksWidget
* @throws \CoreUnexpectedValue
* @throws \Exception
*/
protected function GetFormRow(WebPage $oP, DBObject $oLinkedObj, $linkObjOrId, $aArgs, $oCurrentObj, $iUniqueId, $bReadOnly = false)
protected function GetFormRow(WebPage $oP, DBObject $oLinkedObj, $linkObjOrId, $aArgs, $oCurrentObj, $iUniqueId, $bReadOnly = false, $bModified = false)
{
$sPrefix = "$this->m_sAttCode{$this->m_sNameSuffix}";
$aRow = array();
$aFieldsMap = array();
$iKey = 0;
if (is_object($linkObjOrId) && (!$linkObjOrId->IsNew()))
{
if (is_object($linkObjOrId) && (!$linkObjOrId->IsNew())) {
$iKey = $linkObjOrId->GetKey();
$iRemoteObjKey = $linkObjOrId->Get($this->m_sExtKeyToRemote);
$sPrefix .= "[$iKey][";
@@ -125,49 +124,44 @@ class UILinksWidget
$aArgs['wizHelper'] = "oWizardHelper{$this->m_iInputId}{$iKey}";
$aArgs['this'] = $linkObjOrId;
if ($bReadOnly)
{
if ($bReadOnly) {
$aRow['form::checkbox'] = "";
foreach ($this->m_aEditableFields as $sFieldCode)
{
foreach ($this->m_aEditableFields as $sFieldCode) {
$sDisplayValue = $linkObjOrId->GetEditValue($sFieldCode);
$aRow[$sFieldCode] = $sDisplayValue;
}
}
else
{
} else {
$aRow['form::checkbox'] = "<input class=\"selection\" data-remote-id=\"$iRemoteObjKey\" data-link-id=\"$iKey\" data-unique-id=\"$iUniqueId\" type=\"checkbox\" onClick=\"oWidget".$this->m_iInputId.".OnSelectChange();\" value=\"$iKey\">";
foreach ($this->m_aEditableFields as $sFieldCode)
{
foreach ($this->m_aEditableFields as $sFieldCode) {
$sSafeFieldId = $this->GetFieldId($linkObjOrId->GetKey(), $sFieldCode);
$this->AddRowForFieldCode($aRow, $sFieldCode, $aArgs, $linkObjOrId, $oP, $sNameSuffix, $sSafeFieldId);
$aFieldsMap[$sFieldCode] = $sSafeFieldId;
if ($bModified) {
$oP->add_ready_script(
<<<EOF
oWidget{$this->m_iInputId}.AddModified($iUniqueId, {$this->m_iInputId}, $sFieldCode, {$linkObjOrId->Get($sFieldCode)});
EOF
);
}
}
}
$sState = $linkObjOrId->GetState();
$sRemoteKeySafeFieldId = $this->GetFieldId($aArgs['this']->GetKey(), $this->m_sExtKeyToRemote);;
}
else
{
} else {
// form for creating a new record
if (is_object($linkObjOrId))
{
if (is_object($linkObjOrId)) {
// New link existing only in memory
$oNewLinkObj = $linkObjOrId;
$iRemoteObjKey = $oNewLinkObj->Get($this->m_sExtKeyToRemote);
$oNewLinkObj->Set($this->m_sExtKeyToMe,
$oCurrentObj); // Setting the extkey with the object also fills the related external fields
}
else
{
$oNewLinkObj->Set($this->m_sExtKeyToMe, $oCurrentObj); // Setting the extkey with the object also fills the related external fields
} else {
$iRemoteObjKey = $linkObjOrId;
$oNewLinkObj = MetaModel::NewObject($this->m_sLinkedClass);
$oRemoteObj = MetaModel::GetObject($this->m_sRemoteClass, $iRemoteObjKey);
$oNewLinkObj->Set($this->m_sExtKeyToRemote,
$oRemoteObj); // Setting the extkey with the object alsoo fills the related external fields
$oNewLinkObj->Set($this->m_sExtKeyToMe,
$oCurrentObj); // Setting the extkey with the object also fills the related external fields
$oNewLinkObj->Set($this->m_sExtKeyToRemote, $oRemoteObj); // Setting the extkey with the object alsoo fills the related external fields
$oNewLinkObj->Set($this->m_sExtKeyToMe, $oCurrentObj); // Setting the extkey with the object also fills the related external fields
}
$sPrefix .= "[-$iUniqueId][";
$sNameSuffix = "]"; // To make a tabular form
@@ -177,8 +171,7 @@ class UILinksWidget
$sInputValue = $iUniqueId > 0 ? "-$iUniqueId" : "$iUniqueId";
$aRow['form::checkbox'] = "<input class=\"selection\" data-remote-id=\"$iRemoteObjKey\" data-link-id=\"0\" data-unique-id=\"$iUniqueId\" type=\"checkbox\" onClick=\"oWidget".$this->m_iInputId.".OnSelectChange();\" value=\"$sInputValue\">";
if ($iUniqueId > 0)
{
if ($iUniqueId > 0) {
// Rows created with ajax call need OnLinkAdded call.
//
$oP->add_ready_script(
@@ -187,9 +180,7 @@ PrepareWidgets();
oWidget{$this->m_iInputId}.OnLinkAdded($iUniqueId, $iRemoteObjKey);
EOF
);
}
else
{
} else {
// Rows added before loading the form don't have to call OnLinkAdded.
// Listeners are already present and DOM is not recreated
$iPositiveUniqueId = -$iUniqueId;
@@ -378,10 +369,17 @@ JS
$iMaxAddedId = 0;
$iAddedId = -1; // Unique id for new links
$oBlock->aRemoved = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$this->m_sAttCode}_tbd", '[]', 'raw_data'));
$oModified = $oValue->GetModified($this->m_sExtKeyToRemote);
while ($oCurrentLink = $oValue->Fetch()) {
// We try to retrieve the remote object as usual
if (!in_array($oCurrentLink->GetKey(), $oBlock->aRemoved)) {
$oLinkedObj = MetaModel::GetObject($this->m_sRemoteClass, $oCurrentLink->Get($this->m_sExtKeyToRemote), false /* Must not be found */);
$bModified = false;
if (array_key_exists($oCurrentLink->GetKey(), $oModified)) {
$oLinkedObj = MetaModel::GetObject($this->m_sRemoteClass, $oModified[$oCurrentLink->GetKey()], false /* Must not be found */);
$bModified = true;
} else {
$oLinkedObj = MetaModel::GetObject($this->m_sRemoteClass, $oCurrentLink->Get($this->m_sExtKeyToRemote), false /* Must not be found */);
}
// If successful, it means that we can edit its link
if ($oLinkedObj !== null) {
$bReadOnly = false;
@@ -398,11 +396,12 @@ JS
}
$iMaxAddedId = max($iMaxAddedId, $key);
$aForm[$key] = $this->GetFormRow($oPage, $oLinkedObj, $oCurrentLink, $aArgs, $oCurrentObj, $key, $bReadOnly);
$aForm[$key] = $this->GetFormRow($oPage, $oLinkedObj, $oCurrentLink, $aArgs, $oCurrentObj, $key, $bReadOnly, $bModified);
}
}
$oBlock->iMaxAddedId = (int)$iMaxAddedId;
$oDataTable = DataTableUIBlockFactory::MakeForForm("{$this->m_sAttCode}{$this->m_sNameSuffix}", $this->m_aTableConfig, $aForm);
$oDataTable->SetOptions(['select_mode' => 'custom', 'disable_hyperlinks' => true]);
$oBlock->AddSubBlock($oDataTable);

View File

@@ -3143,15 +3143,55 @@ HTML;
*/
public static function AddParameterToUrl(string $sUrl, string $sParamName, string $sParamValue): string
{
if (strpos($sUrl, '?') === false)
{
if (strpos($sUrl, '?') === false) {
$sUrl = $sUrl.'?'.urlencode($sParamName).'='.urlencode($sParamValue);
}
else
{
} else {
$sUrl = $sUrl.'&'.urlencode($sParamName).'='.urlencode($sParamValue);
}
return $sUrl;
}
/**
* Return traits array used by a class and by parent classes hierarchy.
*
* @see https://www.php.net/manual/en/function.class-uses.php#110752
*
* @param string $sClass Class to scan
* @param bool $bAutoload Autoload flag
*
* @return array traits used
* @since 3.1.0
*/
public static function TraitsUsedByClass(string $sClass, bool $bAutoload = true): array
{
$aTraits = [];
do {
$aTraits = array_merge(class_uses($sClass, $bAutoload), $aTraits);
} while ($sClass = get_parent_class($sClass));
foreach ($aTraits as $sTrait => $same) {
$aTraits = array_merge(class_uses($sTrait, $bAutoload), $aTraits);
}
return array_unique($aTraits);
}
/**
* Test trait usage by a class or by parent classes hierarchy.
*
* @param string $sTrait Trait to search for
* @param string $sClass Class to check
*
* @return bool
* @since 3.1.0
*/
public static function IsTraitUsedByClass(string $sTrait, string $sClass): bool
{
return in_array($sTrait, self::TraitsUsedByClass($sClass, true));
}
public static function GetUniqId()
{
return hash('sha256', uniqid(sprintf('%x', rand()), true).sprintf('%x', rand()));
}
}

View File

@@ -6022,7 +6022,9 @@ class AttributeDateTime extends AttributeDBField
public function GetDefaultValue(DBObject $oHostObject = null)
{
// null value will be replaced by the current date, if not already set, in DoComputeValues
if (!$this->IsNullAllowed()) {
return date($this->GetInternalFormat());
}
return $this->GetNullValue();
}
@@ -7812,7 +7814,7 @@ class AttributeBlob extends AttributeDefinition
public function GetDefaultValue(DBObject $oHostObject = null)
{
return "";
return new ormDocument('', '', '');
}
public function IsNullAllowed(DBObject $oHostObject = null)
@@ -8161,6 +8163,11 @@ class AttributeImage extends AttributeBlob
return $oDoc;
}
public function GetDefaultValue(DBObject $oHostObject = null)
{
return new ormDocument('', '', '');
}
/**
* Check that the supplied ormDocument actually contains an image
* {@inheritDoc}
@@ -11352,6 +11359,13 @@ class AttributeTagSet extends AttributeSet
return new ormTagSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems());
}
public function GetDefaultValue(DBObject $oHostObject = null)
{
$oTagSet = new ormTagSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems());
$oTagSet->SetValues([]);
return $oTagSet;
}
public function IsNull($proposedValue)
{
if (is_null($proposedValue))
@@ -13083,7 +13097,7 @@ class AttributeObsolescenceFlag extends AttributeBoolean
public function GetDefaultValue(DBObject $oHostObject = null)
{
return $this->MakeRealValue("", $oHostObject);
return $this->MakeRealValue(false, $oHostObject);
}
public function IsNullAllowed()

View File

@@ -11,7 +11,7 @@ define('UTF8_BOM', chr(239).chr(187).chr(191)); // 0xEF, 0xBB, 0xBF
/**
* CellChangeSpec
* A series of classes, keeping the information about a given cell: could it be changed or not (and why)?
* A series of classes, keeping the information about a given cell: could it be changed or not (and why)?
*
* @package iTopORM
*/
@@ -42,6 +42,17 @@ abstract class CellChangeSpec
return $this->m_sOql;
}
/**
* @since 3.1.0 N°5305
*/
public function GetDisplayableValueAndDescription(): string
{
return sprintf("%s%s",
$this->GetDisplayableValue(),
$this->GetDescription()
);
}
abstract public function GetDescription();
}
@@ -86,26 +97,90 @@ class CellStatus_Issue extends CellStatus_Modify
parent::__construct($proposedValue, $previousValue);
}
public function GetDescription()
public function GetDisplayableValue()
{
if (is_null($this->m_proposedValue))
{
return Dict::Format('UI:CSVReport-Value-SetIssue', $this->m_sReason);
return Dict::Format('UI:CSVReport-Value-SetIssue');
}
return Dict::Format('UI:CSVReport-Value-ChangeIssue', $this->m_proposedValue, $this->m_sReason);
return Dict::Format('UI:CSVReport-Value-ChangeIssue', \utils::EscapeHtml($this->m_proposedValue));
}
public function GetDescription()
{
return $this->m_sReason;
}
/*
* @since 3.1.0 N°5305
*/
public function GetDisplayableValueAndDescription(): string
{
return sprintf("%s. %s",
$this->GetDisplayableValue(),
$this->GetDescription()
);
}
}
class CellStatus_SearchIssue extends CellStatus_Issue
{
public function __construct()
/** @var string|null $m_sAllowedValues */
private $m_sAllowedValues;
/**
* @since 3.1.0 N°5305
* @var string $sSerializedSearch
*/
private $sSerializedSearch;
/** @var string|null $m_sTargetClass */
private $m_sTargetClass;
/**
* CellStatus_SearchIssue constructor.
* @since 3.1.0 N°5305
*
* @param string $sOql : main message
* @param string $sReason : main message
* @param null $sClass : used for additional message that provides allowed values for current class $sClass
* @param null $sAllowedValues : used for additional message that provides allowed values $sAllowedValues for current class
*/
public function __construct($sSerializedSearch, $sReason, $sClass=null, $sAllowedValues=null)
{
parent::__construct(null, null, null);
parent::__construct(null, null, $sReason);
$this->sSerializedSearch = $sSerializedSearch;
$this->m_sAllowedValues = $sAllowedValues;
$this->m_sTargetClass = $sClass;
}
public function GetDisplayableValue()
{
if (null === $this->m_sReason) {
return Dict::Format('UI:CSVReport-Value-NoMatch', '');
}
return $this->m_sReason;
}
public function GetDescription()
{
return Dict::S('UI:CSVReport-Value-NoMatch');
if (\utils::IsNullOrEmptyString($this->m_sAllowedValues) ||
\utils::IsNullOrEmptyString($this->m_sTargetClass)) {
return '';
}
return Dict::Format('UI:CSVReport-Value-NoMatch-PossibleValues', $this->m_sTargetClass, $this->m_sAllowedValues);
}
/**
* @since 3.1.0 N°5305
* @return string
*/
public function GetSearchLinkUrl()
{
return sprintf("UI.php?operation=search&filter=%s",
rawurlencode($this->sSerializedSearch)
);
}
}
@@ -126,11 +201,24 @@ class CellStatus_NullIssue extends CellStatus_Issue
class CellStatus_Ambiguous extends CellStatus_Issue
{
protected $m_iCount;
/**
* @since 3.1.0 N°5305
* @var string
*/
protected $sSerializedSearch;
public function __construct($previousValue, $iCount, $sOql)
/**
* @since 3.1.0 N°5305
*
* @param $previousValue
* @param int $iCount
* @param string $sSerializedSearch
*
*/
public function __construct($previousValue, $iCount, $sSerializedSearch)
{
$this->m_iCount = $iCount;
$this->m_sQuery = $sOql;
$this->sSerializedSearch = $sSerializedSearch;
parent::__construct(null, $previousValue, '');
}
@@ -139,12 +227,23 @@ class CellStatus_Ambiguous extends CellStatus_Issue
$sCount = $this->m_iCount;
return Dict::Format('UI:CSVReport-Value-Ambiguous', $sCount);
}
/**
* @since 3.1.0 N°5305
* @return string
*/
public function GetSearchLinkUrl()
{
return sprintf("UI.php?operation=search&filter=%s",
rawurlencode($this->sSerializedSearch)
);
}
}
/**
* RowStatus
* A series of classes, keeping the information about a given row: could it be changed or not (and why)?
* A series of classes, keeping the information about a given row: could it be changed or not (and why)?
*
* @package iTopORM
*/
@@ -211,6 +310,26 @@ class RowStatus_Issue extends RowStatus
}
}
/**
* class dedicated to testability
* not used/ignored in csv imports UI/CLI
* @since 3.1.0 N°5305
*/
class RowStatus_Error extends RowStatus
{
/** @var string */
protected $m_sError;
public function __construct($sError)
{
$this->m_sError = $sError;
}
public function GetDescription()
{
return $this->m_sError;
}
}
/**
* BulkChange
@@ -220,17 +339,35 @@ class RowStatus_Issue extends RowStatus
*/
class BulkChange
{
protected $m_sClass;
/** @var string */
protected $m_sClass;
protected $m_aData; // Note: hereafter, iCol maybe actually be any acceptable key (string)
// #@# todo: rename the variables to sColIndex
protected $m_aAttList; // attcode => iCol
protected $m_aExtKeys; // aExtKeys[sExtKeyAttCode][sExtReconcKeyAttCode] = iCol;
protected $m_aReconcilKeys; // attcode (attcode = 'id' for the pkey)
protected $m_sSynchroScope; // OQL - if specified, then the missing items will be reported
protected $m_aOnDisappear; // array of attcode => value, values to be set when an object gets out of scope (ignored if no scope has been defined)
protected $m_sDateFormat; // Date format specification, see DateTime::createFromFormat
protected $m_bLocalizedValues; // Values in the data set are localized (see AttributeEnum)
protected $m_aExtKeysMappingCache; // Cache for resolving external keys based on the given search criterias
/** @var array<string, string> attcode as key, iCol as value */
protected $m_aAttList;
/** @var array<string, array<string, string>> sExtKeyAttCode as key, array of sExtReconcKeyAttCode/iCol as value */
protected $m_aExtKeys;
/** @var string[] list of attcode (attcode = 'id' for the pkey) */
protected $m_aReconcilKeys;
/** @var string OQL - if specified, then the missing items will be reported */
protected $m_sSynchroScope;
/**
* @var array<string, mixed> attcode as key, attvalue as value. Values to be set when an object gets out of scope
* (ignored if no scope has been defined)
*/
protected $m_aOnDisappear;
/**
* @see DateTime::createFromFormat
* @var string Date format specification
*/
protected $m_sDateFormat;
/**
* @see AttributeEnum
* @var boolean true if Values in the data set are localized
*/
protected $m_bLocalizedValues;
/** @var array Cache for resolving external keys based on the given search criterias */
protected $m_aExtKeysMappingCache;
public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null, $sDateFormat = null, $bLocalize = false)
{
@@ -261,30 +398,30 @@ class BulkChange
$this->m_sReportCsvSep = $sSeparator;
$this->m_sReportCsvDelimiter = $sDelimiter;
}
protected function ResolveExternalKey($aRowData, $sAttCode, &$aResults)
{
$oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
$oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass());
foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol)
foreach ($this->m_aExtKeys[$sAttCode] as $sReconKeyAttCode => $iCol)
{
if ($sForeignAttCode == 'id')
if ($sReconKeyAttCode == 'id')
{
$value = (int) $aRowData[$iCol];
}
else
{
// The foreign attribute is one of our reconciliation key
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode);
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sReconKeyAttCode);
$value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
}
$oReconFilter->AddCondition($sForeignAttCode, $value, '=');
$oReconFilter->AddCondition($sReconKeyAttCode, $value, '=');
$aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol]));
}
$oExtObjects = new CMDBObjectSet($oReconFilter);
$aKeys = $oExtObjects->ToArray();
return array($oReconFilter->ToOql(), $aKeys);
return array($oReconFilter, $aKeys);
}
// Returns true if the CSV data specifies that the external key must be left undefined
@@ -318,10 +455,10 @@ class BulkChange
{
$aResults = array();
$aErrors = array();
// External keys reconciliation
//
foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig)
foreach($this->m_aExtKeys as $sAttCode => $aReconKeys)
{
// Skip external keys used for the reconciliation process
// if (!array_key_exists($sAttCode, $this->m_aAttList)) continue;
@@ -330,7 +467,7 @@ class BulkChange
if ($this->IsNullExternalKeySpec($aRowData, $sAttCode))
{
foreach ($aKeyConfig as $sForeignAttCode => $iCol)
foreach ($aReconKeys as $sReconKeyAttCode => $iCol)
{
// Default reporting
// $aRowData[$iCol] is always null
@@ -352,25 +489,24 @@ class BulkChange
$oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass());
$aCacheKeys = array();
foreach ($aKeyConfig as $sForeignAttCode => $iCol)
foreach ($aReconKeys as $sReconKeyAttCode => $iCol)
{
// The foreign attribute is one of our reconciliation key
if ($sForeignAttCode == 'id')
if ($sReconKeyAttCode == 'id')
{
$value = $aRowData[$iCol];
}
else
{
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode);
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sReconKeyAttCode);
$value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
}
$aCacheKeys[] = $value;
$oReconFilter->AddCondition($sForeignAttCode, $value, '=');
$oReconFilter->AddCondition($sReconKeyAttCode, $value, '=');
$aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol]));
}
$sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query...
$iForeignKey = null;
$sOQL = '';
// TODO: check if *too long* keys can lead to collisions... and skip the cache in such a case...
if (!array_key_exists($sAttCode, $this->m_aExtKeysMappingCache))
{
@@ -379,9 +515,8 @@ class BulkChange
if (array_key_exists($sCacheKey, $this->m_aExtKeysMappingCache[$sAttCode]))
{
// Cache hit
$iCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c'];
$iObjectFoundCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c'];
$iForeignKey = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['k'];
$sOQL = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['oql'];
// Record the hit
$this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['h']++;
}
@@ -389,34 +524,35 @@ class BulkChange
{
// Cache miss, let's initialize it
$oExtObjects = new CMDBObjectSet($oReconFilter);
$iCount = $oExtObjects->Count();
if ($iCount == 1)
$iObjectFoundCount = $oExtObjects->Count();
if ($iObjectFoundCount == 1)
{
$oForeignObj = $oExtObjects->Fetch();
$iForeignKey = $oForeignObj->GetKey();
}
$this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array(
'c' => $iCount,
'c' => $iObjectFoundCount,
'k' => $iForeignKey,
'oql' => $oReconFilter->ToOql(),
'h' => 0, // number of hits on this cache entry
);
}
switch($iCount)
switch($iObjectFoundCount)
{
case 0:
$aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound');
$aResults[$sAttCode]= new CellStatus_SearchIssue();
break;
$oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter);
$aResults[$sAttCode] = $oCellStatus_SearchIssue;
$aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound');
break;
case 1:
// Do change the external key attribute
$oTargetObj->Set($sAttCode, $iForeignKey);
break;
// Do change the external key attribute
$oTargetObj->Set($sAttCode, $iForeignKey);
break;
default:
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iCount);
$aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iCount, $sOQL);
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iObjectFoundCount);
$aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iObjectFoundCount, $oReconFilter->serialize());
}
}
@@ -433,7 +569,7 @@ class BulkChange
else
{
$aResults[$sAttCode]= new CellStatus_Modify($iForeignObj, $oTargetObj->GetOriginal($sAttCode));
foreach ($aKeyConfig as $sForeignAttCode => $iCol)
foreach ($aReconKeys as $sReconKeyAttCode => $iCol)
{
// Report the change on reconciliation values as well
$aResults[$iCol] = new CellStatus_Modify(utils::HtmlEntities($aRowData[$iCol]));
@@ -446,7 +582,7 @@ class BulkChange
}
}
}
// Set the object attributes
//
foreach ($this->m_aAttList as $sAttCode => $iCol)
@@ -487,7 +623,13 @@ class BulkChange
$value = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
if (is_null($value) && (strlen($aRowData[$iCol]) > 0))
{
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode);
if ($oAttDef instanceof AttributeEnum || $oAttDef instanceof AttributeTagSet){
/** @var AttributeDefinition $oAttributeDefinition */
$oAttributeDefinition = $oAttDef;
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-AllowedValues', $sAttCode, implode(',', $oAttributeDefinition->GetAllowedValues()));
} else {
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode);
}
}
else
{
@@ -504,7 +646,7 @@ class BulkChange
}
}
}
// Reporting on fields
//
$aChangedFields = $oTargetObj->ListChanges();
@@ -556,7 +698,7 @@ class BulkChange
}
}
}
// Checks
//
$res = $oTargetObj->CheckConsistency();
@@ -567,12 +709,101 @@ class BulkChange
}
return $aResults;
}
/**
* search with current permissions did not match
* let's search why and give some more feedbacks to the user through proper labels
*
* @param DBObjectSearch $oDbSearchWithConditions search used to find external key
*
* @return \CellStatus_SearchIssue
* @throws \CoreException
* @throws \MissingQueryArgument
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
*
* @since 3.1.0 N°5305
*/
protected function GetCellSearchIssue($oDbSearchWithConditions) : CellStatus_SearchIssue {
//current search with current permissions did not match
//let's search why and give some more feedback to the user
$sSerializedSearch = $oDbSearchWithConditions->serialize();
// Count all objects with all permissions without any condition
$oDbSearchWithoutAnyCondition = new DBObjectSearch($oDbSearchWithConditions->GetClass());
$oDbSearchWithoutAnyCondition->AllowAllData(true);
$oExtObjectSet = new CMDBObjectSet($oDbSearchWithoutAnyCondition);
$iAllowAllDataObjectCount = $oExtObjectSet->Count();
if ($iAllowAllDataObjectCount === 0) {
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject', $oDbSearchWithConditions->GetClass());
return new CellStatus_SearchIssue($sSerializedSearch, $sReason);
}
// Count all objects with current user permissions
$oDbSearchWithoutAnyCondition->AllowAllData(false);
$oExtObjectSetWithCurrentUserPermissions = new CMDBObjectSet($oDbSearchWithoutAnyCondition);
$iCurrentUserRightsObjectCount = $oExtObjectSetWithCurrentUserPermissions->Count();
if ($iCurrentUserRightsObjectCount === 0){
// No objects visible by current user
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser', $oDbSearchWithConditions->GetClass());
return new CellStatus_SearchIssue($sSerializedSearch, $sReason);
}
try{
$aDisplayedAllowedValues = [];
// Possibles values are displayed to UI user. we have to limit the amount of displayed values
$oExtObjectSetWithCurrentUserPermissions->SetLimit(4);
for($i = 0; $i < 3; $i++){
/** @var \DBObject $oVisibleObject */
$oVisibleObject = $oExtObjectSetWithCurrentUserPermissions->Fetch();
if (is_null($oVisibleObject)){
break;
}
$aCurrentAllowedValueFields = [];
foreach ($oDbSearchWithConditions->GetInternalParams() as $sForeignAttCode => $sValue){
$aCurrentAllowedValueFields[] = $oVisibleObject->Get($sForeignAttCode);
}
$aDisplayedAllowedValues[] = implode(" ", $aCurrentAllowedValueFields);
}
$allowedValues = implode(", ", $aDisplayedAllowedValues);
if ($oExtObjectSetWithCurrentUserPermissions->Count() > 3){
$allowedValues .= "...";
}
} catch(Exception $e) {
IssueLog::Error("failure during CSV import when fetching few visible objects: ", null,
[ 'target_class' => $oDbSearchWithConditions->GetClass(), 'criteria' => $oDbSearchWithConditions->GetCriteria(), 'message' => $e->getMessage()]
);
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser', $oDbSearchWithConditions->GetClass());
return new CellStatus_SearchIssue($sSerializedSearch, $sReason);
}
if ($iAllowAllDataObjectCount != $iCurrentUserRightsObjectCount) {
// No match and some objects NOT visible by current user. including current search maybe...
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser', $oDbSearchWithConditions->GetClass());
return new CellStatus_SearchIssue($sSerializedSearch, $sReason, $oDbSearchWithConditions->GetClass(), $allowedValues);
}
// No match. This is not linked to any right issue
// Possible values: DD,DD
$aCurrentValueFields = [];
foreach ($oDbSearchWithConditions->GetInternalParams() as $sValue){
$aCurrentValueFields[] = $sValue;
}
$value =implode(" ", $aCurrentValueFields);
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch', $value);
return new CellStatus_SearchIssue($sSerializedSearch, $sReason, $oDbSearchWithConditions->GetClass(), $allowedValues);
}
protected function PrepareMissingObject(&$oTargetObj, &$aErrors)
{
$aResults = array();
$aErrors = array();
// External keys
//
foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig)
@@ -585,7 +816,7 @@ class BulkChange
$aResults[$iCol] = new CellStatus_Void('?');
}
}
// Update attributes
//
foreach($this->m_aOnDisappear as $sAttCode => $value)
@@ -596,7 +827,7 @@ class BulkChange
}
$oTargetObj->Set($sAttCode, $value);
}
// Reporting on fields
//
$aChangedFields = $oTargetObj->ListChanges();
@@ -616,7 +847,7 @@ class BulkChange
$aResults[$iCol]= new CellStatus_Void($oTargetObj->Get($sAttCode));
}
}
// Checks
//
$res = $oTargetObj->CheckConsistency();
@@ -674,14 +905,16 @@ class BulkChange
}
$aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors);
if (count($aErrors) > 0)
{
$sErrors = implode(', ', $aErrors);
$aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
//__ERRORS__ used by tests only
$aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors);
return $oTargetObj;
}
// Check that any external key will have a value proposed
$aMissingKeys = array();
foreach (MetaModel::GetExternalKeys($this->m_sClass) as $sExtKeyAttCode => $oExtKey)
@@ -689,7 +922,7 @@ class BulkChange
if (!$oExtKey->IsNullAllowed())
{
if (!array_key_exists($sExtKeyAttCode, $this->m_aExtKeys) && !array_key_exists($sExtKeyAttCode, $this->m_aAttList))
{
{
$aMissingKeys[] = $oExtKey->GetLabel();
}
}
@@ -745,14 +978,16 @@ class BulkChange
{
$sErrors = implode(', ', $aErrors);
$aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
//__ERRORS__ used by tests only
$aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors);
return;
}
$aChangedFields = $oTargetObj->ListChanges();
if (count($aChangedFields) > 0)
{
$aResult[$iRow]["__STATUS__"] = new RowStatus_Modify(count($aChangedFields));
// Optionaly record the results
//
if ($oChange)
@@ -794,9 +1029,11 @@ class BulkChange
{
$sErrors = implode(', ', $aErrors);
$aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
//__ERRORS__ used by tests only
$aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors);
return;
}
$aChangedFields = $oTargetObj->ListChanges();
if (count($aChangedFields) > 0)
{
@@ -821,7 +1058,7 @@ class BulkChange
$aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(0);
}
}
public function Process(CMDBChange $oChange = null)
{
if ($oChange)
@@ -866,7 +1103,7 @@ class BulkChange
foreach ($this->m_aAttList as $sAttCode => $iCol)
{
if ($sAttCode == 'id') continue;
$oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
if ($oAttDef instanceof AttributeDateTime) // AttributeDate is derived from AttributeDateTime
{
@@ -881,14 +1118,18 @@ class BulkChange
$sFormat = $sDateFormat;
}
$oFormat = new DateTimeFormat($sFormat);
$sDateExample = $oFormat->Format(new DateTime('2022-10-23 16:25:33'));
$sRegExp = $oFormat->ToRegExpr('/');
if (!preg_match($sRegExp, $this->m_aData[$iRow][$iCol]))
$sErrorMsg = Dict::Format('UI:CSVReport-Row-Issue-ExpectedDateFormat', $sDateExample);
if (!preg_match($sRegExp, $sValue))
{
$aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
$aResult[$iRow][$iCol] = new CellStatus_Issue(utils::HtmlEntities($sValue), null, $sErrorMsg);
}
else
{
$oDate = DateTime::createFromFormat($sFormat, $this->m_aData[$iRow][$iCol]);
$oDate = DateTime::createFromFormat($sFormat, $sValue);
if ($oDate !== false)
{
$sNewDate = $oDate->format($oAttDef->GetInternalFormat());
@@ -898,7 +1139,7 @@ class BulkChange
{
// Leave the cell unchanged
$aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
$aResult[$iRow][$sAttCode] = new CellStatus_Issue(null, utils::HtmlEntities($this->m_aData[$iRow][$iCol]), Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
$aResult[$iRow][$iCol] = new CellStatus_Issue($sValue, null, $sErrorMsg);
}
}
}
@@ -952,23 +1193,26 @@ class BulkChange
else
{
// The value has to be found or verified
list($sQuery, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
/** var DBObjectSearch $oReconFilter */
list($oReconFilter, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
if (count($aMatches) == 1)
{
$oRemoteObj = reset($aMatches); // first item
$valuecondition = $oRemoteObj->GetKey();
$aResult[$iRow][$sAttCode] = new CellStatus_Void($oRemoteObj->GetKey());
}
}
elseif (count($aMatches) == 0)
{
$aResult[$iRow][$sAttCode] = new CellStatus_SearchIssue();
}
$oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter);
$aResult[$iRow][$sAttCode] = $oCellStatus_SearchIssue;
}
else
{
$aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $sQuery);
$aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $oReconFilter->serialize());
}
}
}
}
else
{
@@ -1019,7 +1263,7 @@ class BulkChange
default:
// Found several matches, ambiguous
$aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Ambiguous'));
$aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->ToOql());
$aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->serialize());
$aResult[$iRow]["finalclass"]= 'n/a';
}
}
@@ -1110,7 +1354,7 @@ class BulkChange
}
}
$oBulkChanges->Seek(0);
$aDetails = array();
while ($oChange = $oBulkChanges->Fetch())
{
@@ -1274,7 +1518,7 @@ EOF
$oOldTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('oldvalue'));
$sOldValue = $oOldTarget->GetHyperlink();
}
$sNewValue = Dict::S('UI:UndefinedObject');
if ($oOperation->Get('newvalue') != 0)
{
@@ -1300,11 +1544,11 @@ EOF
}
else
{
$aAttributes[$sAttCode] = 1;
$aAttributes[$sAttCode] = 1;
}
}
}
$aDetails = array();
foreach($aObjects as $iUId => $aObjData)
{
@@ -1356,6 +1600,6 @@ EOF
$aConfig[$sAttCode] = array('label' => MetaModel::GetLabel($sClass, $sAttCode), 'description' => MetaModel::GetDescription($sClass, $sAttCode));
}
$oPage->table($aConfig, $aDetails);
}
}
}

View File

@@ -499,7 +499,7 @@ abstract class CMDBObject extends DBObject
$oMyChangeOp->Set("objkey", $this->GetKey());
$oMyChangeOp->Set("attcode", $sAttCode);
$oMyChangeOp->Set("oldvalue", $original);
$oMyChangeOp->Set("newvalue", $value[$sAttCode]);
$oMyChangeOp->Set("newvalue", $value);
$iId = $oMyChangeOp->DBInsertNoReload();
}
elseif ($oAttDef instanceOf AttributeCustomFields)
@@ -640,20 +640,6 @@ abstract class CMDBObject extends DBObject
return $newKey;
}
public function DBUpdate()
{
// Copy the changes list before the update (the list should be reset afterwards)
$aChanges = $this->ListChanges();
if (count($aChanges) == 0)
{
return;
}
$ret = parent::DBUpdate();
return $ret;
}
/**
* @param null $oDeletionPlan
*

View File

@@ -129,6 +129,22 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'event_service.debug.filter_events' => [
'type' => 'array',
'description' => 'Filter Event Service debug by events',
'default' => '',
'value' => '',
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'event_service.debug.filter_sources' => [
'type' => 'array',
'description' => 'Filter Event Service debug by event sources',
'default' => '',
'value' => '',
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'app_env_label' => [
'type' => 'string',
'description' => 'Label displayed to describe the current application environment, defaults to the environment name (e.g. "production")',

View File

@@ -270,4 +270,4 @@
</class>
</classes>
</meta>
</itop_design>
</itop_design>

File diff suppressed because it is too large Load Diff

View File

@@ -182,6 +182,26 @@ class DesignElement extends \DOMElement
return $this->ownerDocument->GetNodes($sXPath, $this);
}
public static function ToArray(DesignElement $oNode)
{
$aRes = [];
if ($oNode->GetNodes('./*')->length == 0) {
return $oNode->GetText('');
}
foreach ($oNode->GetNodes('./*') as $oSubNode) {
/** @var \Combodo\iTop\DesignElement $oSubNode */
$aSubArray = DesignElement::ToArray($oSubNode);
if ($oSubNode->hasAttribute('id')) {
$aRes[$oSubNode->getAttribute('id')] = $aSubArray;
} else {
$aRes[$oSubNode->tagName] = $aSubArray;
}
}
return $aRes;
}
/**
* Create an HTML representation of the DOM, for debugging purposes
*

View File

@@ -77,7 +77,7 @@ class EMail implements iEMail
* @return void
* @throws \ConfigException
* @throws \CoreException
* @since 2.7.>8 3.0.3 3.1.0 N°4947 Method creation, to factorize same code in children classes
* @since 2.7.8 3.0.3 3.1.0 N°4947 Method creation, to factorize same code in children classes
*/
protected function InitRecipientFrom()
{

View File

@@ -299,7 +299,7 @@ class ExecutionKPI
*/
private static function Push(ExecutionKPI $oExecutionKPI)
{
array_push(self::$m_aExecutionStack, $oExecutionKPI);
self::$m_aExecutionStack[] = $oExecutionKPI;
}
/**
@@ -449,4 +449,3 @@ class ExecutionKPI
return 0;
}
}

View File

@@ -569,6 +569,14 @@ class LogChannels
public const INLINE_IMAGE = 'InlineImage';
public const PORTAL = 'portal';
/**
* @var string
* @since 3.1.0 specific channel for event service
*/
public const EVENT_SERVICE = 'EventService';
public const DM_CRUD = 'DMCRUD';
}

View File

@@ -128,6 +128,10 @@ abstract class MetaModel
/** @var string */
protected static $m_sEnvironment = 'production';
public const REENTRANCE_TYPE_UPDATE = 'update';
protected static $m_aReentranceProtection = [];
/**
* MetaModel constructor.
*/
@@ -6785,6 +6789,19 @@ abstract class MetaModel
}
$sClass = $aRow[$sClassAlias."finalclass"];
}
// if an object is already being updated, then this method will return this object instead of recreating a new one.
// At this point the method DBUpdate of a new object with the same class and id won't do anything due to reentrance protection,
// so to ensure that the potential modifications are correctly saved, the object currently being updated is returned.
// DBUpdate() method then will take care that all the modifications will be saved.
if (array_key_exists($sClassAlias.'id', $aRow)) {
$iKey = $aRow[$sClassAlias."id"];
$oObject = self::GetReentranceObject(Metamodel::REENTRANCE_TYPE_UPDATE, $sClass, $iKey);
if ($oObject !== false) {
return $oObject;
}
}
return new $sClass($aRow, $sClassAlias, $aAttToLoad, $aExtendedDataSpec);
}
@@ -7543,6 +7560,36 @@ abstract class MetaModel
/** @var AttributeEnum $oAttDef */
return $oAttDef->GetStyle($sValue);
}
protected static function GetReentranceObject($sType, $sClass, $sKey)
{
if (isset(self::$m_aReentranceProtection[$sType][$sClass][$sKey])) {
return self::$m_aReentranceProtection[$sType][$sClass][$sKey];
}
return false;
}
/**
* @param $sType
* @param \DBObject $oObject
*
* @return bool true if reentry possible
*/
public static function StartReentranceProtection($sType, DBObject $oObject)
{
if (isset(self::$m_aReentranceProtection[$sType][get_class($oObject)][$oObject->GetKey()])) {
return false;
}
self::$m_aReentranceProtection[$sType][get_class($oObject)][$oObject->GetKey()] = $oObject;
return true;
}
public static function StopReentranceProtection($sType, DBObject $oObject)
{
if (isset(self::$m_aReentranceProtection[$sType][get_class($oObject)][$oObject->GetKey()])) {
unset(self::$m_aReentranceProtection[$sType][get_class($oObject)][$oObject->GetKey()]);
}
}
}

View File

@@ -25,6 +25,9 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Service\EventData;
use Combodo\iTop\Service\EventService;
/**
* ormDocument
@@ -193,7 +196,6 @@ class ormDocument
* @param string $sContentDisposition Either 'inline' or 'attachment'
* @param string $sSecretField The attcode of the field containing a "secret" to be provided in order to retrieve the file
* @param string $sSecretValue The value of the secret to be compared with the value of the attribute $sSecretField
* @return none
*/
public static function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null)
{
@@ -212,6 +214,12 @@ class ormDocument
$oDocument = $oObj->Get($sAttCode);
if (is_object($oDocument))
{
$aEventData = array(
'debug_info' => $oDocument->GetFileName(),
'object' => $oObj,
'document' => $oDocument,
);
EventService::FireEvent(new EventData(EVENT_SERVICE_DOWNLOAD_DOCUMENT, $sClass, $aEventData));
$oPage->TrashUnexpectedOutput();
$oPage->SetContentType($oDocument->GetMimeType());
$oPage->SetContentDisposition($sContentDisposition,$oDocument->GetFileName());

View File

@@ -383,9 +383,8 @@ class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator
$iPreservedCount = count($this->aPreserved);
if ($this->iCursor < $iPreservedCount)
{
$iRet = current($this->aPreserved);
$this->oOriginalSet->Seek($iRet);
$oRet = $this->oOriginalSet->Fetch();
$sId = key($this->aPreserved);
$oRet = MetaModel::GetObject($this->sClass, $sId);
}
else
{
@@ -609,16 +608,32 @@ class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator
$aAdded = $this->aAdded;
$aModified = $this->aModified;
$aRemoved = array();
if (count($this->aRemoved) > 0)
{
if (count($this->aRemoved) > 0) {
$oSearch = new DBObjectSearch($this->sClass);
$oSearch->AddCondition('id', $this->aRemoved, 'IN');
$oSet = new DBObjectSet($oSearch);
$aRemoved = $oSet->ToArray();
}
return array_merge($aAdded, $aModified, $aRemoved);
}
/**
* Get the list of all modified (added, modified and removed) links
*
* @return array of link objects
* @throws \Exception
*/
public function GetModified($sExtKeyToMe)
{
$aModified = [];
foreach ($this->aModified as $oObj) {
$aModified[$oObj->GetKey()] = $oObj->Get($sExtKeyToMe);
}
return $aModified;
}
/**
* @param DBObject $oHostObject
*
@@ -662,8 +677,7 @@ class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator
{
$aCheckLinks[] = $iLinkId;
}
foreach ($this->aModified as $iLinkId => $oLink)
{
foreach ($this->aModified as $iLinkId => $oLink) {
$aCheckLinks[] = $oLink->GetKey();
}
@@ -699,8 +713,7 @@ class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator
// Write the links according to the existing links
//
foreach ($this->aAdded as $oLink)
{
foreach ($this->aAdded as $oLink) {
// Make sure that the objects in the set point to "this"
$oLink->Set($sExtKeyToMe, $oHostObject->GetKey());
@@ -736,6 +749,7 @@ class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator
$oLink->DBClone();
}
}
$oLink->SetLinkHostObject($oHostObject);
$oLink->DBWrite();
$this->aPreserved[$oLink->GetKey()] = $oLink;

View File

@@ -119,6 +119,10 @@ $ibo-fieldsorter--selected--background-color: $ibo-color-blue-200 !default;
&.selected {
background-color: $ibo-datatable--row--background-color--is-selected;
}
.ibo-datatable--row-actions-toolbar{
justify-content: end;
}
}
}

View File

@@ -27,11 +27,6 @@ tr.ibo-csv-import--row-unchanged td {
border-bottom: 1px $ibo-color-grey-400 solid;
}
.wizContainer table tr.ibo-csv-import--row-error td {
border-bottom: 1px $ibo-color-grey-400 solid;
background-color: $ibo-color-red-200;
}
tr.ibo-csv-import--row-modified td {
border-bottom: 1px $ibo-color-grey-400 solid;
}
@@ -44,4 +39,4 @@ tr.ibo-csv-import--row-added td {
font-size: 4em;
color: $ibo-color-primary-400;
margin: 20px;
}
}

View File

@@ -14,6 +14,22 @@ use DBObjectSet;
class DBToolsUtils
{
private static bool $bAnalyzed = false;
private final static function AnalyzeTables()
{
if (self::$bAnalyzed) {
return;
}
$oResult = CMDBSource::Query('SHOW TABLES;');
while ($aRow = $oResult->fetch_array()) {
$sTable = $aRow['0'];
CMDBSource::Query("ANALYZE TABLE `$sTable`; ");
}
self::$bAnalyzed = true;
}
/**
* @return int
* @throws \CoreException
@@ -22,6 +38,7 @@ class DBToolsUtils
*/
public final static function GetDatabaseSize()
{
self::AnalyzeTables();
$sSchema = CMDBSource::DBName();
$sReq = <<<EOF
@@ -48,6 +65,7 @@ EOF;
*/
public final static function GetDBDataSize()
{
self::AnalyzeTables();
$sSchema = CMDBSource::DBName();
$sReq = <<<EOF
@@ -74,6 +92,7 @@ EOF;
*/
public final static function GetDBIndexSize()
{
self::AnalyzeTables();
$sSchema = CMDBSource::DBName();
$sReq = <<<EOF
@@ -127,6 +146,7 @@ EOF;
public static function GetDBTablesInfo()
{
self::AnalyzeTables();
$sSchema = CMDBSource::DBName();
$sReq = <<<EOF

View File

@@ -255,4 +255,50 @@
</presentation>
</class>
</classes>
<events>
<event id="EVENT_SERVICE_ADD_ATTACHMENT_TO_OBJECT" _delta="define">
<description>An attachment has been added to an object</description>
<replaces>Attachment::AfterUpdate</replaces>
<sources>
<source id="Attachment">Attachment</source>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<event_data>
<event_datum id="object">
<description>The attachment updated</description>
<type>DBObject</type>
</event_datum>
<event_datum id="target_object">
<description>The object to which the attachment is linked</description>
<type>DBObject</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_SERVICE_REMOVE_ATTACHMENT_FROM_OBJECT" _delta="define">
<description>An attachment has been removed from an object</description>
<replaces>Attachment::AfterUpdate</replaces>
<sources>
<source id="Attachment">Attachment</source>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<event_data>
<event_datum id="object">
<description>The attachment updated</description>
<type>DBObject</type>
</event_datum>
<event_datum id="target_object">
<description>The object to which the attachment is linked</description>
<type>DBObject</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
</events>
</itop_design>

View File

@@ -304,6 +304,8 @@ class AttachmentPlugIn implements iApplicationUIExtension, iApplicationObjectExt
// Remove attachments that are no longer attached to the current object
if (in_array($oAttachment->GetKey(), $aRemovedAttachmentIds))
{
$aData = ['target_object' => $oObject];
$oAttachment->FireEvent(EVENT_SERVICE_REMOVE_ATTACHMENT_FROM_OBJECT, $aData);
$oAttachment->DBDelete();
$aActions[] = self::GetActionChangeOp($oAttachment, false /* false => deletion */);
}
@@ -332,6 +334,8 @@ class AttachmentPlugIn implements iApplicationUIExtension, iApplicationObjectExt
$oAttachment->DBUpdate();
// temporary attachment confirmed, list it in the history
$aActions[] = self::GetActionChangeOp($oAttachment, true /* true => creation */);
$aData = ['target_object' => $oObject];
$oAttachment->FireEvent(EVENT_SERVICE_ADD_ATTACHMENT_TO_OBJECT, $aData);
}
}
}

View File

@@ -13,9 +13,7 @@ use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory;
use Combodo\iTop\Config\Validator\iTopConfigAstValidator;
use Combodo\iTop\Config\Validator\iTopConfigSyntaxValidator;
require_once(APPROOT.'application/application.inc.php');
require_once(APPROOT.'application/startup.inc.php');
require_once(APPROOT.'application/loginwebpage.class.inc.php');
/**

View File

@@ -1472,7 +1472,7 @@
<code><![CDATA[ protected function OnInsert()
{
parent::OnInsert();
parent::OnInsert();
$this->ComputeImpactedItems();
$this->SetIfNull('last_update', time());
$this->SetIfNull('start_date', time());
@@ -1484,7 +1484,7 @@
<type>Overload-DBObject</type>
<code><![CDATA[ protected function OnUpdate()
{
parent::OnUpdate();
parent::OnUpdate();
$aChanges = $this->ListChanges();
if (array_key_exists('functionalcis_list', $aChanges))
{

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design version="3.1">
<events>
<event id="EVENT_SERVICE_DISPLAY_OBJECT_DETAILS" _delta="define">
<description>An object details is about to be displayed to a user</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<arguments>
<argument id="object">
<description>The object displayed</description>
<type>DBObject</type>
</argument>
<argument id="debug_info">
<description>Debug string</description>
<type>string</type>
</argument>
</arguments>
</event>
</events>
</itop_design>

View File

@@ -195,6 +195,8 @@ class ObjectController extends BrickController
$sObjectClass = get_class($oObject);
$sObjectId = $oObject->GetKey();
$oObject->FireEvent(EVENT_SERVICE_DISPLAY_OBJECT_DETAILS);
$aData = array('sMode' => 'view');
$aData['form'] = $oObjectFormHandler->HandleForm($oRequest, $aData['sMode'], $sObjectClass, $sObjectId);
$aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:View:Title', MetaModel::GetName($sObjectClass),

View File

@@ -1198,7 +1198,7 @@
</state>
</states>
</lifecycle>
<methods>
<methods>
<method id="GetTicketRefFormat">
<static>true</static>
<access>public</access>
@@ -1343,7 +1343,6 @@
<type>Overload-DBObject</type>
<code><![CDATA[ public function ComputeValues()
{
// Compute the priority of the ticket
$this->Set('priority', $this->ComputePriority());

View File

@@ -216,7 +216,7 @@
<count_max>0</count_max>
</field>
</fields>
<methods>
<methods>
<method id="DBInsertNoReload">
<static>false</static>
<access>public</access>

View File

@@ -644,9 +644,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Vyberte třídu pro hledání: ',
'UI:CSVReport-Value-Modified' => 'Upraveno',
'UI:CSVReport-Value-SetIssue' => 'Nemůže být změněno - důvod: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Nemůže být změněno na %1$s - důvod: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Žádná shoda',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Chybí povinná hodnota',
'UI:CSVReport-Value-Ambiguous' => 'Nejednoznačné: nalezeno %1$s objektů',
'UI:CSVReport-Row-Unchanged' => 'nezměněn',

View File

@@ -633,9 +633,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Vælg klasse at søge efter: ',
'UI:CSVReport-Value-Modified' => 'Ændret',
'UI:CSVReport-Value-SetIssue' => 'Kunne ikke ændres - årsag: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Kunne ikke ændres til %1$s - årsag: %2$s',
'UI:CSVReport-Value-NoMatch' => 'No match',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Mangler obligatorisk værdi',
'UI:CSVReport-Value-Ambiguous' => 'Tvetydig: fandt %1$s objekter',
'UI:CSVReport-Row-Unchanged' => 'Uændret',

View File

@@ -633,9 +633,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Wählen Sie für die Suche die Klasse aus: ',
'UI:CSVReport-Value-Modified' => 'Modifiziert',
'UI:CSVReport-Value-SetIssue' => 'Konnte nicht geändert werden - Grund: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Konnte nicht zu %1$s geändert werden - Grund: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Kein Treffer',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Pflichtfeld fehlt',
'UI:CSVReport-Value-Ambiguous' => 'Doppeldeutig: %1$s Objekte gefunden',
'UI:CSVReport-Row-Unchanged' => 'Unverändert',

View File

@@ -656,9 +656,14 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Select the class to search: ',
'UI:CSVReport-Value-Modified' => 'Modified',
'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s',
'UI:CSVReport-Value-NoMatch' => 'No match',
'UI:CSVReport-Value-SetIssue' => 'Invalid value for attribute',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'',
'UI:CSVReport-Value-NoMatch-PossibleValues' => 'Some possible \'%1$s\' value(s): %2$s',
'UI:CSVReport-Value-NoMatch-NoObject' => 'There are no \'%1$s\' objects',
'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => 'There are no \'%1$s\' objects found with your current profile',
'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => 'There are some \'%1$s\' objects not visible with your current profile',
'UI:CSVReport-Value-Missing' => 'Missing mandatory value',
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects',
'UI:CSVReport-Row-Unchanged' => 'unchanged',
@@ -672,11 +677,13 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:CSVReport-Value-Issue-Readonly' => 'The attribute \'%1$s\' is read-only and cannot be modified (current value: %2$s, proposed value: %3$s)',
'UI:CSVReport-Value-Issue-Format' => 'Failed to process input: %1$s',
'UI:CSVReport-Value-Issue-NoMatch' => 'Unexpected value for attribute \'%1$s\': no match found, check spelling',
'UI:CSVReport-Value-Issue-AllowedValues' => 'Allowed \'%1$s\' value(s): %2$s',
'UI:CSVReport-Value-Issue-Unknown' => 'Unexpected value for attribute \'%1$s\': %2$s',
'UI:CSVReport-Row-Issue-Inconsistent' => 'Attributes not consistent with each others: %1$s',
'UI:CSVReport-Row-Issue-Attribute' => 'Unexpected attribute value(s)',
'UI:CSVReport-Row-Issue-MissingExtKey' => 'Could not be created, due to missing external key(s): %1$s',
'UI:CSVReport-Row-Issue-DateFormat' => 'wrong date format',
'UI:CSVReport-Row-Issue-ExpectedDateFormat' => 'Expected format: %1$s',
'UI:CSVReport-Row-Issue-Reconciliation' => 'failed to reconcile',
'UI:CSVReport-Row-Issue-Ambiguous' => 'ambiguous reconciliation',
'UI:CSVReport-Row-Issue-Internal' => 'Internal error: %1$s, %2$s',
@@ -806,6 +813,11 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:Schema:DisplaySelector/Code' => 'Code~~',
'UI:Schema:Attribute/Filter' => 'Filter~~',
'UI:Schema:DefaultNullValue' => 'Default null : "%1$s"~~',
'UI:Schema:Events' => 'Events',
'UI:Schema:Events:Defined' => 'Defined events',
'UI:Schema:Events:NoEvent' => 'No event defined',
'UI:Schema:Events:Listeners' => 'Event listeners',
'UI:Schema:Events:NoListener' => 'No event listener',
'UI:LinksWidget:Autocomplete+' => 'Type the first 3 characters...',
'UI:Edit:SearchQuery' => 'Select a predefined query',
'UI:Edit:TestQuery' => 'Test query',

View File

@@ -644,9 +644,9 @@ Esperamos distrute de esta versión tanto como nosotros la imaginamos y creamos.
'UI:UniversalSearch:LabelSelectTheClass' => 'Seleccione la clase a buscar: ',
'UI:CSVReport-Value-Modified' => 'Modificado',
'UI:CSVReport-Value-SetIssue' => 'No puede ser modificado - motivo: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'No puede ser cambiado a %1$s - motivo: %2$s',
'UI:CSVReport-Value-NoMatch' => 'No hay Coincidencias',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Falta valor obligatorio',
'UI:CSVReport-Value-Ambiguous' => 'Ambigüedad: encontrados %1$s objetos',
'UI:CSVReport-Row-Unchanged' => 'Sin Cambios',

View File

@@ -639,9 +639,14 @@ Nous espérons que vous aimerez cette version autant que nous avons eu du plaisi
'UI:UniversalSearch:LabelSelectTheClass' => 'Sélectionnez le type d\'objets à rechercher : ',
'UI:CSVReport-Value-Modified' => 'Modifié',
'UI:CSVReport-Value-SetIssue' => 'Modification impossible - cause : %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Ne peut pas prendre la valeur \'%1$s\' - cause : %2$s',
'UI:CSVReport-Value-NoMatch' => 'Pas de correspondance',
'UI:CSVReport-Value-SetIssue' => 'Valeur invalide',
'UI:CSVReport-Value-ChangeIssue' => 'Ne peut pas prendre la valeur \'%1$s\'',
'UI:CSVReport-Value-NoMatch' => 'Pas de correspondance avec \'%1$s\'',
'UI:CSVReport-Value-NoMatch-PossibleValues' => 'Valeur(s) possible(s) pour l\'objet \'%1$s\' : %2$s',
'UI:CSVReport-Value-NoMatch-NoObject' => 'Il n\'y a aucun objet \'%1$s\'',
'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => 'Il n\'y a aucun objet \'%1$s\' visible par votre utilisateur',
'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => 'Il existe des objet(s) \'%1$s\' non visible(s) par votre utilisateur',
'UI:CSVReport-Value-Missing' => 'Absence de valeur obligatoire',
'UI:CSVReport-Value-Ambiguous' => 'Ambigüité: %1$d objets trouvés',
'UI:CSVReport-Row-Unchanged' => 'inchangé',

View File

@@ -633,9 +633,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Keresendő osztály kiválasztása:',
'UI:CSVReport-Value-Modified' => 'Modified~~',
'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s~~',
'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s~~',
'UI:CSVReport-Value-NoMatch' => 'No match~~',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Missing mandatory value~~',
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects~~',
'UI:CSVReport-Row-Unchanged' => 'unchanged~~',

View File

@@ -644,9 +644,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Seleziona la classe per la ricerca: ',
'UI:CSVReport-Value-Modified' => 'Modified~~',
'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s~~',
'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s~~',
'UI:CSVReport-Value-NoMatch' => 'No match~~',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Missing mandatory value~~',
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects~~',
'UI:CSVReport-Row-Unchanged' => 'unchanged~~',

View File

@@ -633,9 +633,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => '検索するクラスを選択してください。',
'UI:CSVReport-Value-Modified' => '修正済み',
'UI:CSVReport-Value-SetIssue' => '変更出来ません - 理由: %1$s',
'UI:CSVReport-Value-ChangeIssue' => '%1$s へ変更出来ません - 理由: %2$s',
'UI:CSVReport-Value-NoMatch' => 'マッチしません',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => '必須の値がありません',
'UI:CSVReport-Value-Ambiguous' => 'あいまいな値: %1$s オブジェクト',
'UI:CSVReport-Row-Unchanged' => '未変更',

View File

@@ -644,9 +644,9 @@ We hopen dat je even hard van deze versie geniet als dat we zelf ervan hebben ge
'UI:UniversalSearch:LabelSelectTheClass' => 'Selecteer de klasse om te zoeken: ',
'UI:CSVReport-Value-Modified' => 'Aangepast',
'UI:CSVReport-Value-SetIssue' => 'Kon niet worden aangepast - reden: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Kon niet worden aangepast naar %1$s - reden: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Geen match',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Ontbrekende verplichte waarde',
'UI:CSVReport-Value-Ambiguous' => 'Onduidelijk: gevonden %1$s objecten',
'UI:CSVReport-Row-Unchanged' => 'onveranderd',

View File

@@ -643,9 +643,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Wybierz klasę do przeszukania: ',
'UI:CSVReport-Value-Modified' => 'Zmodyfikowano',
'UI:CSVReport-Value-SetIssue' => 'Nie można było zmienić - powód: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Nie można zmienić na %1$s - powód: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Nie pasuje',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Brak wymaganej wartości',
'UI:CSVReport-Value-Ambiguous' => 'Uwaga: znaleziono %1$s obiektów',
'UI:CSVReport-Row-Unchanged' => 'niezmieniony',

View File

@@ -644,9 +644,9 @@ Esperamos que você goste desta versão tanto quanto gostamos de imaginá-la e c
'UI:UniversalSearch:LabelSelectTheClass' => 'Selecione a classe para pesquisar: ',
'UI:CSVReport-Value-Modified' => 'Modificado',
'UI:CSVReport-Value-SetIssue' => 'Não pode ser modificado - motivo: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Não pode ser modificado para %1$s - motivo: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Não corresponde',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Faltando valor obrigatório',
'UI:CSVReport-Value-Ambiguous' => 'Ambíguo: encontrado %1$s objeto(s)',
'UI:CSVReport-Row-Unchanged' => 'inalterado',

View File

@@ -645,9 +645,9 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
'UI:UniversalSearch:LabelSelectTheClass' => 'Выбор класса для поиска: ',
'UI:CSVReport-Value-Modified' => 'Изменен',
'UI:CSVReport-Value-SetIssue' => 'Не может быть изменен - причина: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Не может быть изменен %1$s - причина: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Нет совпадений',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Отсутствует обязательное значение',
'UI:CSVReport-Value-Ambiguous' => 'Неоднозначное сопоставление: найдено %1$s объектов',
'UI:CSVReport-Row-Unchanged' => 'без изменений',

View File

@@ -634,9 +634,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Vyberte triedu na vyhľadávanie: ',
'UI:CSVReport-Value-Modified' => 'Upravený',
'UI:CSVReport-Value-SetIssue' => 'Nemožno zmeniť - dôvod: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Nemožno zmeniť na %1$s - dôvod: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Žiadna zhoda',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Chýbajúca povinná hodnota',
'UI:CSVReport-Value-Ambiguous' => 'Nejednoznačné: nájdených %1$s objektov',
'UI:CSVReport-Row-Unchanged' => 'Nezmený',

View File

@@ -661,9 +661,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Aranacak sınıfı seçiniz: ',
'UI:CSVReport-Value-Modified' => 'Değiştiridi',
'UI:CSVReport-Value-SetIssue' => 'Değiştirilemedi - Sebep: %1$s',
'UI:CSVReport-Value-ChangeIssue' => '%1$s olarak değiştirilemedi - Sebep: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Eşleşme yok',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Eksik Zorunlu Değer',
'UI:CSVReport-Value-Ambiguous' => 'Belirsiz: %1$s nesnelerini buldum',
'UI:CSVReport-Row-Unchanged' => 'Değiştirilmedi',

View File

@@ -19,14 +19,16 @@
// Display DataTable
Dict::Add('EN US', 'English', 'English', array(
'UI:Datatables:Language:Processing' => 'Please wait...',
'UI:Datatables:Language:LengthMenu' => '_MENU_ per page',
'UI:Datatables:Language:ZeroRecords' => 'No result',
'UI:Datatables:Language:Info' => '_TOTAL_ item(s)',
'UI:Datatables:Language:InfoEmpty' => 'No information',
'UI:Datatables:Language:EmptyTable' => 'No data available in this table',
'UI:Datatables:Language:Error' => 'An error occured while running the query',
'UI:Datatables:Language:DisplayLength:All' => 'All',
'UI:Datatables:Language:Sort:Ascending' => 'enable for an ascending sort',
'UI:Datatables:Language:Sort:Descending' => 'enable for a descending sort',
'UI:Datatables:Language:Processing' => 'Please wait...',
'UI:Datatables:Language:LengthMenu' => '_MENU_ per page',
'UI:Datatables:Language:ZeroRecords' => 'No result',
'UI:Datatables:Language:Info' => '_TOTAL_ item(s)',
'UI:Datatables:Language:InfoEmpty' => 'No information',
'UI:Datatables:Language:EmptyTable' => 'No data available in this table',
'UI:Datatables:Language:Error' => 'An error occured while running the query',
'UI:Datatables:Language:DisplayLength:All' => 'All',
'UI:Datatables:Language:Sort:Ascending' => 'enable for an ascending sort',
'UI:Datatables:Language:Sort:Descending' => 'enable for a descending sort',
'UI:Datatables:Column:RowActions:Label' => '',
'UI:Datatables:Column:RowActions:Description' => '',
));

View File

@@ -18,14 +18,16 @@
*/
// Display DataTable
Dict::Add('FR FR', 'French', 'Français', array(
'UI:Datatables:Language:Processing' => 'Patientez ...',
'UI:Datatables:Language:LengthMenu' => '_MENU_ par page',
'UI:Datatables:Language:ZeroRecords' => 'Pas de résultat',
'UI:Datatables:Language:Info' => '_TOTAL_ élément(s)',
'UI:Datatables:Language:InfoEmpty' => 'Pas d\'information',
'UI:Datatables:Language:EmptyTable' => 'Pas de résultat',
'UI:Datatables:Language:Error' => 'Erreur lors du chargement des données',
'UI:Datatables:Language:DisplayLength:All' => 'Tous',
'UI:Datatables:Language:Sort:Ascending' => 'tri croissant',
'UI:Datatables:Language:Sort:Descending' => 'tri décroissant',
'UI:Datatables:Language:Processing' => 'Patientez ...',
'UI:Datatables:Language:LengthMenu' => '_MENU_ par page',
'UI:Datatables:Language:ZeroRecords' => 'Pas de résultat',
'UI:Datatables:Language:Info' => '_TOTAL_ élément(s)',
'UI:Datatables:Language:InfoEmpty' => 'Pas d\'information',
'UI:Datatables:Language:EmptyTable' => 'Pas de résultat',
'UI:Datatables:Language:Error' => 'Erreur lors du chargement des données',
'UI:Datatables:Language:DisplayLength:All' => 'Tous',
'UI:Datatables:Language:Sort:Ascending' => 'tri croissant',
'UI:Datatables:Language:Sort:Descending' => 'tri décroissant',
'UI:Datatables:Column:RowActions:Label' => '',
'UI:Datatables:Column:RowActions:Description' => '',
));

View File

@@ -649,9 +649,9 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'UI:UniversalSearch:LabelSelectTheClass' => '选择要搜索的类别: ',
'UI:CSVReport-Value-Modified' => '已修改',
'UI:CSVReport-Value-SetIssue' => '无法修改 - 原因: %1$s',
'UI:CSVReport-Value-ChangeIssue' => '无法修改成 %1$s - 原因: %2$s',
'UI:CSVReport-Value-NoMatch' => '不匹配',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => '缺少必填项',
'UI:CSVReport-Value-Ambiguous' => '模糊匹配: 找到 %1$s 个对象',
'UI:CSVReport-Row-Unchanged' => '保持不变',

View File

@@ -78,4 +78,30 @@ function getMultipleSelectionParams(listId)
});
return oRes;
}
}
/**
* Return column JSON declaration for row actions.
* Could be part of column or columnDefs declaration of datatable.js.
*
* @param sTableId
* @param iColumnTargetIndex
* @returns {*}
* @since 3.1.0
*/
function getRowActionsColumnDefinition(sTableId, iColumnTargetIndex = -1)
{
let aColumn = {
type: "html",
orderable: false,
render: function ( data, type, row, meta ) {
return $(`#${sTableId}_actions_buttons_template`).html();
}
};
if (iColumnTargetIndex !== -1) {
aColumn['targets'] = iColumnTargetIndex;
}
return aColumn;
}

View File

@@ -385,12 +385,10 @@ function LinksWidget(id, sClass, sAttCode, iInputId, sSuffix, bDuplicates, oWizH
*/
this.OnValueChange = function (iLink, iUniqueId, sAttCode, value, $oSourceObject) {
let sFormPrefix = me.iInputId;
if (iLink > 0)
{
if (iLink > 0) {
// Modifying an existing link
let oModified = me.aModified[iLink];
if (oModified == undefined)
{
if (oModified == undefined) {
// Still not marked as modified
oModified = {};
oModified['formPrefix'] = sFormPrefix;
@@ -398,17 +396,26 @@ function LinksWidget(id, sClass, sAttCode, iInputId, sSuffix, bDuplicates, oWizH
// Weird formatting, aligned with the output of the direct links widget (new links to be created)
oModified['attr_'+sFormPrefix+sAttCode] = value;
me.aModified[iLink] = oModified;
}
else
{
} else {
// Modifying a newly added link - the structure should already be up to date
if (iUniqueId < 0)
{
if (iUniqueId < 0) {
iUniqueId = -iUniqueId;
}
me.aAdded[iUniqueId]['attr_'+sFormPrefix+sAttCode] = value;
}
};
this.AddModified = function (iLink, sFormPrefix, sAttCode, value) {
// Modifying an existing link
let oModified = me.aModified[iLink];
if (oModified == undefined) {
// Still not marked as modified
oModified = {};
oModified['formPrefix'] = sFormPrefix;
}
// Weird formatting, aligned with the output of the direct links widget (new links to be created)
oModified['attr_'+sFormPrefix+sAttCode] = value;
me.aModified[iLink] = oModified;
};
this.OnFormSubmit = function () {
let oDiv = $('#linkedset_'+me.id);

View File

@@ -2,6 +2,11 @@
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
exit(1);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit7f81b4a2a468a061c306af5e447a9a9f::getLoader();

View File

@@ -149,7 +149,7 @@ class ClassLoader
/**
* @return string[] Array of classname => path
* @psalm-var array<string, string>
* @psalm-return array<string, string>
*/
public function getClassMap()
{

View File

@@ -29,10 +29,6 @@ class InstalledVersions
* @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**

View File

@@ -2,7 +2,7 @@
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
@@ -227,6 +227,7 @@ return array(
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\StaticTable\\FormTableRow\\FormTableRow' => $baseDir . '/sources/Application/UI/Base/Component/DataTable/StaticTable/FormTableRow/FormTableRow.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\StaticTable\\FormTable\\FormTable' => $baseDir . '/sources/Application/UI/Base/Component/DataTable/StaticTable/FormTable/FormTable.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\StaticTable\\StaticTable' => $baseDir . '/sources/Application/UI/Base/Component/DataTable/StaticTable/StaticTable.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\tTableRowActions' => $baseDir . '/sources/Application/UI/Base/Component/DataTable/tTableRowActions.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\FieldBadge\\FieldBadge' => $baseDir . '/sources/Application/UI/Base/Component/FieldBadge/FieldBadge.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\FieldBadge\\FieldBadgeUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Component/FieldBadge/FieldBadgeUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\FieldSet\\FieldSet' => $baseDir . '/sources/Application/UI/Base/Component/FieldSet/FieldSet.php',
@@ -272,6 +273,8 @@ return array(
'Combodo\\iTop\\Application\\UI\\Base\\Component\\QuickCreate\\QuickCreateHelper' => $baseDir . '/sources/Application/UI/Base/Component/QuickCreate/QuickCreateHelper.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Spinner\\Spinner' => $baseDir . '/sources/Application/UI/Base/Component/Spinner/Spinner.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Spinner\\SpinnerUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Component/Spinner/SpinnerUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Template\\Template' => $baseDir . '/sources/Application/UI/Base/Component/Template/Template.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Template\\TemplateUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Component/Template/TemplateUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Text\\Text' => $baseDir . '/sources/Application/UI/Base/Component/Text/Text.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Title\\Title' => $baseDir . '/sources/Application/UI/Base/Component/Title/Title.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Title\\TitleUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Component/Title/TitleUIBlockFactory.php',
@@ -410,6 +413,9 @@ return array(
'Combodo\\iTop\\Renderer\\FieldRenderer' => $baseDir . '/sources/Renderer/FieldRenderer.php',
'Combodo\\iTop\\Renderer\\FormRenderer' => $baseDir . '/sources/Renderer/FormRenderer.php',
'Combodo\\iTop\\Renderer\\RenderingOutput' => $baseDir . '/sources/Renderer/RenderingOutput.php',
'Combodo\\iTop\\Service\\EventData' => $baseDir . '/sources/Application/Service/EventData.php',
'Combodo\\iTop\\Service\\EventHelper' => $baseDir . '/sources/Application/Service/EventHelper.php',
'Combodo\\iTop\\Service\\EventService' => $baseDir . '/sources/Application/Service/EventService.php',
'CompileCSSService' => $baseDir . '/application/compilecssservice.class.inc.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'Config' => $baseDir . '/core/config.class.inc.php',
@@ -1015,6 +1021,7 @@ return array(
'MySQLHasGoneAwayException' => $baseDir . '/application/exceptions/mysql/MySQLHasGoneAwayException.php',
'MySQLNoTransactionException' => $baseDir . '/application/exceptions/mysql/MySQLNoTransactionException.php',
'MySQLQueryHasNoResultException' => $baseDir . '/application/exceptions/mysql/MySQLQueryHasNoResultException.php',
'MySQLTransactionNotClosedException' => $baseDir . '/application/exceptions/mysql/MySQLTransactionNotClosedException.php',
'NestedQueryExpression' => $baseDir . '/core/oql/expression.class.inc.php',
'NestedQueryOqlExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php',
'NewObjectMenuNode' => $baseDir . '/application/menunode.class.inc.php',
@@ -1385,6 +1392,7 @@ return array(
'RotatingLogFileNameBuilder' => $baseDir . '/core/log.class.inc.php',
'RowStatus' => $baseDir . '/core/bulkchange.class.inc.php',
'RowStatus_Disappeared' => $baseDir . '/core/bulkchange.class.inc.php',
'RowStatus_Error' => $baseDir . '/core/bulkchange.class.inc.php',
'RowStatus_Issue' => $baseDir . '/core/bulkchange.class.inc.php',
'RowStatus_Modify' => $baseDir . '/core/bulkchange.class.inc.php',
'RowStatus_NewObj' => $baseDir . '/core/bulkchange.class.inc.php',

View File

@@ -2,24 +2,24 @@
// autoload_files.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php',
'23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php',
'0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'c9d07b32a2e02bc0fc582d4f0c1b56cc' => $vendorDir . '/laminas/laminas-servicemanager/src/autoload.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'c9d07b32a2e02bc0fc582d4f0c1b56cc' => $vendorDir . '/laminas/laminas-servicemanager/src/autoload.php',
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
);

View File

@@ -2,7 +2,7 @@
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(

View File

@@ -2,7 +2,7 @@
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(

View File

@@ -25,33 +25,20 @@ class ComposerAutoloaderInit7f81b4a2a468a061c306af5e447a9a9f
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit7f81b4a2a468a061c306af5e447a9a9f', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit7f81b4a2a468a061c306af5e447a9a9f', 'loadClassLoader'));
$includePaths = require __DIR__ . '/include_paths.php';
$includePaths[] = get_include_path();
set_include_path(implode(PATH_SEPARATOR, $includePaths));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f::getInitializer($loader));
} else {
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f::getInitializer($loader));
$loader->setClassMapAuthoritative(true);
$loader->register(true);
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
$includeFiles = \Composer\Autoload\ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f::$files;
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire7f81b4a2a468a061c306af5e447a9a9f($fileIdentifier, $file);
}
@@ -60,11 +47,16 @@ class ComposerAutoloaderInit7f81b4a2a468a061c306af5e447a9a9f
}
}
/**
* @param string $fileIdentifier
* @param string $file
* @return void
*/
function composerRequire7f81b4a2a468a061c306af5e447a9a9f($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}

View File

@@ -7,22 +7,22 @@ namespace Composer\Autoload;
class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
{
public static $files = array (
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'0d59ee240a4cd96ddbb4ff164fccea4d' => __DIR__ . '/..' . '/symfony/polyfill-php73/bootstrap.php',
'23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php',
'0d59ee240a4cd96ddbb4ff164fccea4d' => __DIR__ . '/..' . '/symfony/polyfill-php73/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'c9d07b32a2e02bc0fc582d4f0c1b56cc' => __DIR__ . '/..' . '/laminas/laminas-servicemanager/src/autoload.php',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'c9d07b32a2e02bc0fc582d4f0c1b56cc' => __DIR__ . '/..' . '/laminas/laminas-servicemanager/src/autoload.php',
'8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php',
);
public static $prefixLengthsPsr4 = array (
@@ -592,6 +592,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\StaticTable\\FormTableRow\\FormTableRow' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/DataTable/StaticTable/FormTableRow/FormTableRow.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\StaticTable\\FormTable\\FormTable' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/DataTable/StaticTable/FormTable/FormTable.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\StaticTable\\StaticTable' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/DataTable/StaticTable/StaticTable.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\tTableRowActions' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/DataTable/tTableRowActions.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\FieldBadge\\FieldBadge' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/FieldBadge/FieldBadge.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\FieldBadge\\FieldBadgeUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/FieldBadge/FieldBadgeUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\FieldSet\\FieldSet' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/FieldSet/FieldSet.php',
@@ -637,6 +638,8 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Application\\UI\\Base\\Component\\QuickCreate\\QuickCreateHelper' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/QuickCreate/QuickCreateHelper.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Spinner\\Spinner' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Spinner/Spinner.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Spinner\\SpinnerUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Spinner/SpinnerUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Template\\Template' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Template/Template.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Template\\TemplateUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Template/TemplateUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Text\\Text' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Text/Text.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Title\\Title' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Title/Title.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Title\\TitleUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Title/TitleUIBlockFactory.php',
@@ -775,6 +778,9 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Renderer\\FieldRenderer' => __DIR__ . '/../..' . '/sources/Renderer/FieldRenderer.php',
'Combodo\\iTop\\Renderer\\FormRenderer' => __DIR__ . '/../..' . '/sources/Renderer/FormRenderer.php',
'Combodo\\iTop\\Renderer\\RenderingOutput' => __DIR__ . '/../..' . '/sources/Renderer/RenderingOutput.php',
'Combodo\\iTop\\Service\\EventData' => __DIR__ . '/../..' . '/sources/Application/Service/EventData.php',
'Combodo\\iTop\\Service\\EventHelper' => __DIR__ . '/../..' . '/sources/Application/Service/EventHelper.php',
'Combodo\\iTop\\Service\\EventService' => __DIR__ . '/../..' . '/sources/Application/Service/EventService.php',
'CompileCSSService' => __DIR__ . '/../..' . '/application/compilecssservice.class.inc.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Config' => __DIR__ . '/../..' . '/core/config.class.inc.php',
@@ -1380,6 +1386,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'MySQLHasGoneAwayException' => __DIR__ . '/../..' . '/application/exceptions/mysql/MySQLHasGoneAwayException.php',
'MySQLNoTransactionException' => __DIR__ . '/../..' . '/application/exceptions/mysql/MySQLNoTransactionException.php',
'MySQLQueryHasNoResultException' => __DIR__ . '/../..' . '/application/exceptions/mysql/MySQLQueryHasNoResultException.php',
'MySQLTransactionNotClosedException' => __DIR__ . '/../..' . '/application/exceptions/mysql/MySQLTransactionNotClosedException.php',
'NestedQueryExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php',
'NestedQueryOqlExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php',
'NewObjectMenuNode' => __DIR__ . '/../..' . '/application/menunode.class.inc.php',
@@ -1750,6 +1757,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'RotatingLogFileNameBuilder' => __DIR__ . '/../..' . '/core/log.class.inc.php',
'RowStatus' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'RowStatus_Disappeared' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'RowStatus_Error' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'RowStatus_Issue' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'RowStatus_Modify' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'RowStatus_NewObj' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',

View File

@@ -2,7 +2,7 @@
// include_paths.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(

View File

@@ -5,7 +5,7 @@
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => '7b60c9c71af3167cc075063ce67b836b96a9e2f0',
'reference' => '8a3e07dd80c8316d68ad44a892c51f4ed5de572c',
'name' => 'combodo/itop',
'dev' => true,
),
@@ -25,7 +25,7 @@
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => '7b60c9c71af3167cc075063ce67b836b96a9e2f0',
'reference' => '8a3e07dd80c8316d68ad44a892c51f4ed5de572c',
'dev_requirement' => false,
),
'combodo/tcpdf' => array(

View File

@@ -138,7 +138,7 @@ try {
}
return $aResult;
}
/**
* Return the most frequent (and regularly occuring) character among the given set, in the specified lines
* @param array $aCSVData The input data, one entry per line
@@ -174,7 +174,7 @@ try {
}
$iLine++;
}
$aScores = array();
foreach($aGuesses as $sSep => $aData)
{
@@ -185,7 +185,7 @@ try {
$sSeparator = $aKeys[0]; // Take the first key, the one with the best score
return $sSeparator;
}
/**
* Try to predict the CSV parameters based on the input data
* @param string $sCSVData The input data
@@ -196,10 +196,10 @@ try {
$aData = explode("\n", $sCSVData);
$sSeparator = GuessFromFrequency($aData, array("\t", ',', ';', '|')); // Guess the most frequent (and regular) character on each line
$sQualifier = GuessFromFrequency($aData, array('"', "'")); // Guess the most frequent (and regular) character on each line
return array('separator' => $sSeparator, 'qualifier' => $sQualifier);
}
/**
* Display a banner for the special "synchro" mode
* @param WebPage $oP The Page for the output
@@ -215,6 +215,7 @@ try {
* Add a paragraph to the body of the page
*
* @param string $s_html
* @param ?string $sLinkUrl
*
* @return string
*/
@@ -259,9 +260,9 @@ try {
$sSynchroScope = utils::ReadParam('synchro_scope', '', false, 'raw_data');
$sDateTimeFormat = utils::ReadParam('date_time_format', 'default');
$sCustomDateTimeFormat = utils::ReadParam('custom_date_time_format', (string)AttributeDateTime::GetFormat(), false, 'raw_data');
$sChosenDateFormat = ($sDateTimeFormat == 'default') ? (string)AttributeDateTime::GetFormat() : $sCustomDateTimeFormat;
if (!empty($sSynchroScope))
{
$oSearch = DBObjectSearch::FromOQL($sSynchroScope);
@@ -276,7 +277,7 @@ try {
$sSynchroScope = '';
$aSynchroUpdate = null;
}
// Parse the data set
$oCSVParser = new CSVParser($sCSVData, $sSeparator, $sTextQualifier, MetaModel::GetConfig()->Get('max_execution_time_per_loop'));
$aData = $oCSVParser->ToArray($iSkippedLines);
@@ -286,10 +287,10 @@ try {
$aResult[] = $sTextQualifier.implode($sTextQualifier.$sSeparator.$sTextQualifier, array_shift($aData)).$sTextQualifier; // Remove the first line and store it in case of error
$iRealSkippedLines++;
}
// Format for the line numbers
$sMaxLen = (strlen(''.count($aData)) < 3) ? 3 : strlen(''.count($aData)); // Pad line numbers to the appropriate number of chars, but at least 3
// Compute the list of search/reconciliation criteria
$aSearchKeys = array();
foreach($aSearchFields as $index => $sDummy)
@@ -303,16 +304,16 @@ try {
}
else
{
$aSearchKeys[$sSearchField] = '';
$aSearchKeys[$sSearchField] = '';
}
if (!MetaModel::IsValidFilterCode($sClassName, $sSearchField))
{
// Remove invalid or unmapped search fields
$aSearchFields[$index] = null;
unset($aSearchKeys[$sSearchField]);
unset($aSearchKeys[$sSearchField]);
}
}
// Compute the list of fields and external keys to process
$aExtKeys = array();
$aAttributes = array();
@@ -345,13 +346,13 @@ try {
}
else
{
$aAttributes[$sAttCode] = $iIndex;
$aAttributes[$sAttCode] = $iIndex;
}
}
}
}
}
}
$oMyChange = null;
if (!$bSimulate)
{
@@ -360,7 +361,7 @@ try {
CMDBObject::SetCurrentChangeFromParams($sUserString, CMDBChangeOrigin::CSV_INTERACTIVE);
$oMyChange = CMDBObject::GetCurrentChange();
}
$oBulk = new BulkChange(
$sClassName,
$aData,
@@ -370,7 +371,7 @@ try {
empty($sSynchroScope) ? null : $sSynchroScope,
$aSynchroUpdate,
$sChosenDateFormat, // date format
true // localize
true // localize
);
$oBulk->SetReportHtml();
@@ -437,7 +438,6 @@ try {
case 'RowStatus_NewObj':
$iCreated++;
$sFinalClass = $aResRow['finalclass'];
$sStatus = '<img src="../images/added.png" title="'.Dict::S('UI:CSVReport-Icon-Created').'">';
$sCSSRowClass = 'ibo-csv-import--row-added';
if ($bSimulate) {
@@ -453,7 +453,7 @@ try {
case 'RowStatus_Issue':
$iErrors++;
$sMessage .= GetDivAlert($oStatus->GetDescription());
$sStatus = '<img src="../images/error.png" title="'.Dict::S('UI:CSVReport-Icon-Error').'">';//translate
$sStatus = '<div class="ibo-csv-import--cell-error"><i class="fas fa-exclamation-triangle" title="'.Dict::S('UI:CSVReport-Icon-Error').'" /></div>';//translate
$sCSSMessageClass = 'ibo-csv-import--cell-error';
$sCSSRowClass = 'ibo-csv-import--row-error';
if (array_key_exists($iLine, $aData)) {
@@ -474,33 +474,36 @@ try {
if (isset($aExternalKeysByColumn[$iNumber - 1])) {
$sExtKeyName = $aExternalKeysByColumn[$iNumber - 1];
$oExtKeyCellStatus = $aResRow[$sExtKeyName];
switch (get_class($oExtKeyCellStatus)) {
case 'CellStatus_Issue':
case 'CellStatus_SearchIssue':
case 'CellStatus_NullIssue':
case 'CellStatus_Ambiguous':
$sCellMessage .= GetDivAlert($oExtKeyCellStatus->GetDescription());
break;
default:
// Do nothing
}
$oCellStatus = $oExtKeyCellStatus;
}
$sHtmlValue = $oCellStatus->GetDisplayableValue();
switch (get_class($oCellStatus)) {
case 'CellStatus_Issue':
case 'CellStatus_NullIssue':
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error">'.Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue).$sCellMessage.'</div>';
break;
case 'CellStatus_SearchIssue':
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error">ERROR: '.$sHtmlValue.$sCellMessage.'</div>';
$aTableRow[$sClassName.'/'.$sAttCode] = sprintf("%s%s%s%s%s%s",
'<a href="',
$oCellStatus->GetSearchLinkUrl(),
'"><div class="ibo-csv-import--cell-error">',
Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue),
GetDivAlert($oCellStatus->GetDescription()),
'<i class="fas fa-search"></i></div><a/>'
);
break;
case 'CellStatus_Ambiguous':
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error" >'.Dict::Format('UI:CSVReport-Object-Ambiguous', $sHtmlValue).$sCellMessage.'</div>';
$aTableRow[$sClassName.'/'.$sAttCode] = sprintf("%s%s%s%s%s%s",
'<a href="',
$oCellStatus->GetSearchLinkUrl(),
'"><i class="fas fa-search"/><div class="ibo-csv-import--cell-error">',
Dict::Format('UI:CSVReport-Object-Ambiguous', $sHtmlValue),
GetDivAlert($oCellStatus->GetDescription()),
'<i class="fas fa-search"></i></div><a/>'
);
break;
case 'CellStatus_Modify':
@@ -589,7 +592,7 @@ try {
$oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxUnchanged));
$oPage->add_ready_script("$('#show_created').on('click', function(){ToggleRows('ibo-csv-import--row-added')})");
$oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel('<img src="../images/error.png">&nbsp;'.sprintf($aDisplayFilters['errors'], $iErrors), '', "1", "show_errors", "checkbox");
$oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel('<i class="fas fa-exclamation-triangle" style="color:#A33; background-color: #FFF0F0;">&nbsp;'.sprintf($aDisplayFilters['errors'], $iErrors).'</i></i>', '', "1", "show_errors", "checkbox");
$oCheckBoxUnchanged->GetInput()->SetIsChecked(true);
$oCheckBoxUnchanged->SetBeforeInput(false);
$oCheckBoxUnchanged->GetInput()->AddCSSClass('ibo-input-checkbox');
@@ -676,7 +679,7 @@ try {
EOF
);
}
$sErrors = json_encode(Dict::Format('UI:CSVImportError_items', $iErrors));
$sCreated = json_encode(Dict::Format('UI:CSVImportCreated_items', $iCreated));
$sModified = json_encode(Dict::Format('UI:CSVImportModified_items', $iModified));
@@ -771,7 +774,7 @@ EOF
{
return null;
}
}
/**
* Perform the actual load of the CSV data and display the results
@@ -795,7 +798,7 @@ EOF
$oField->AddSubBlock($oText);
}
}
/**
* Simulate the load of the CSV data and display the results
* @param WebPage $oPage The web page to display the wizard
@@ -807,7 +810,7 @@ EOF
$oPage->AddSubBlock($oPanel);
ProcessCSVData($oPage, true /* simulate */);
}
/**
* Select the mapping between the CSV column and the fields of the objects
* @param WebPage $oPage The web page to display the wizard
@@ -920,10 +923,10 @@ EOF
$aSearchFields = utils::ReadParam('search_field', array(), false, 'field_name');
$sFieldsMapping = addslashes(json_encode($aFieldsMapping));
$sSearchFields = addslashes(json_encode($aSearchFields));
$oPage->add_ready_script("DoMapping('$sFieldsMapping', '$sSearchFields');"); // There is already a class selected, run the mapping
}
$oPage->add_script(
<<<EOF
var aDefaultKeys = new Array();
@@ -1139,7 +1142,7 @@ EOF
EOF
);
}
/**
* Select the options of the CSV load and check for CSV parsing errors
* @param WebPage $oPage The current web page
@@ -1163,7 +1166,7 @@ EOF
$sCSVData = utils::ReadPostedParam('csvdata', '', 'raw_data');
}
$sEncoding = utils::ReadParam('encoding', 'UTF-8');
// Compute a subset of the data set, now that we know the charset
if ($sEncoding == 'UTF-8')
{
@@ -1180,7 +1183,7 @@ EOF
{
$sUTF8Data = iconv($sEncoding, 'UTF-8//IGNORE//TRANSLIT', $sCSVData);
}
$aGuesses = GuessParameters($sUTF8Data); // Try to predict the parameters, based on the input data
$iSkippedLines = utils::ReadParam('nb_skipped_lines', '');
@@ -1188,7 +1191,7 @@ EOF
$sTextQualifier = utils::ReadParam('text_qualifier', '', false, 'raw_data');
if ($sTextQualifier == '') // May be set to an empty value by the previous page
{
$sTextQualifier = $aGuesses['qualifier'];
$sTextQualifier = $aGuesses['qualifier'];
}
$sOtherTextQualifier = in_array($sTextQualifier, array('"', "'")) ? '' : $sTextQualifier;
$bHeaderLine = utils::ReadParam('header_line', 0);
@@ -1606,7 +1609,7 @@ EOF
null, AjaxTab::ENUM_TAB_PLACEHOLDER_MISC);
}
}
switch($iStep)
{
case 11:
@@ -1614,45 +1617,45 @@ EOF
$oPage = new AjaxPage('');
BulkChange::DisplayImportHistory($oPage);
$oPage->add_ready_script('$("#CSVImportHistory table.listResults").tableHover();');
$oPage->add_ready_script('$("#CSVImportHistory table.listResults").tablesorter( { widgets: ["myZebra", "truncatedList"]} );');
$oPage->add_ready_script('$("#CSVImportHistory table.listResults").tablesorter( { widgets: ["myZebra", "truncatedList"]} );');
break;
case 10:
// Case generated by BulkChange::DisplayImportHistory
$iChange = (int)utils::ReadParam('changeid', 0);
BulkChange::DisplayImportHistoryDetails($oPage, $iChange);
break;
case 5:
LoadData($oPage);
break;
case 4:
Preview($oPage);
break;
case 3:
SelectMapping($oPage);
break;
case 2:
SelectOptions($oPage);
break;
case 1:
case 6: // Loop back here when we are done
default:
Welcome($oPage);
}
$oPage->output();
}
catch(CoreException $e)
{
require_once(APPROOT.'/setup/setuppage.class.inc.php');
$oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
$oP->error(Dict::Format('UI:Error_Details', $e->getHtmlDesc()));
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
$oP->error(Dict::Format('UI:Error_Details', $e->getHtmlDesc()));
$oP->output();
if (MetaModel::IsLogEnabledIssue())
@@ -1680,8 +1683,8 @@ catch(Exception $e)
{
require_once(APPROOT.'/setup/setuppage.class.inc.php');
$oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
$oP->error(Dict::Format('UI:Error_Details', $e->getMessage()));
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
$oP->error(Dict::Format('UI:Error_Details', $e->getMessage()));
$oP->output();
if (MetaModel::IsLogEnabledIssue())
@@ -1701,4 +1704,4 @@ catch(Exception $e)
IssueLog::Error($e->getMessage());
}
}
}

View File

@@ -12,6 +12,7 @@ use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectOptionUIBlockF
use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentWithSideContent;
use Combodo\iTop\Service\EventService;
require_once('../approot.inc.php');
require_once(APPROOT.'/application/application.inc.php');
@@ -264,7 +265,92 @@ function DisplayTriggers($oPage, $sClass)
cmdbAbstractObject::DisplaySet($oPage, $oSet, array('block_id' => 'triggers'));
}
function DisplayEvents(WebPage $oPage, $sClass)
{
$aEvents = EventService::GetEventsByClass($sClass);
$aColumns = [
'event' => ['label' => 'Event'],
'description' => ['label' => 'Description'],
];
$aRows = [];
foreach ($aEvents as $sEvent => $aEventInfo) {
$aDesc = $aEventInfo['description'];
$aRows[] = [
'event' => $sEvent,
'description' => $aDesc['description'] ?? '',
];
}
$oTable = DataTableUIBlockFactory::MakeForStaticData(Dict::S('UI:Schema:Events:Defined'), $aColumns, $aRows);
$oPage->AddSubBlock($oTable);
$aSources = [];
if (MetaModel::IsAbstract($sClass)) {
foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sChildClass) {
if (!MetaModel::IsAbstract($sChildClass)) {
$oObject = MetaModel::NewObject($sChildClass);
$aSources[] = $oObject->GetObjectUniqId();
break;
}
}
foreach (MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, false) as $sParentClass) {
$aSources[] = $sParentClass;
}
} else {
$oObject = MetaModel::NewObject($sClass);
$aSources[] = $oObject->GetObjectUniqId();
foreach (MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, false) as $sParentClass) {
$aSources[] = $sParentClass;
}
}
$aListeners = [];
foreach (array_keys($aEvents) as $sEvent) {
$aListeners = array_merge($aListeners, EventService::GetListeners($sEvent, $aSources));
}
usort($aListeners, function ($a, $b) {
if ($a['event'] == $b['event']) {
if ($a['priority'] == $b['priority']) {
return 0;
}
return ($a['priority'] > $b['priority']) ? 1 : -1;
}
return ($a['event'] > $b['event']) ? 1 : -1;
});
$aColumns = [
'event' => ['label' => 'Event'],
'listener' => ['label' => 'Listener'],
'priority' => ['label' => 'Priority'],
'module' => ['label' => 'Module'],
];
$aRows = [];
$oReflectionClass = new ReflectionClass($sClass);
foreach ($aListeners as $aListener) {
if (is_object($aListener['callback'][0])) {
$sListenerClass = $sClass;
if ($aListener['callback'][0] != $sClass) {
$oListenerReflectionClass = new ReflectionClass(get_class($aListener['callback'][0]));
if (!$oListenerReflectionClass->isSubclassOf($sClass)) {
$sListenerClass = get_class($aListener['callback'][0]);
} elseif (!$oReflectionClass->hasMethod($aListener['callback'][1])) {
continue;
}
}
$sListener = $sListenerClass.'->'.$aListener['callback'][1].'(\Combodo\iTop\Service\EventData $oEventData)';
} else {
$sListener = $aListener['callback'][0].'::'.$aListener['callback'][1].'(\Combodo\iTop\Service\EventData $oEventData)';
}
$aRows[] = [
'event' => $aListener['event'],
'listener' => $sListener,
'priority' => $aListener['priority'],
'module' => $aListener['module'],
];
}
$oTable = DataTableUIBlockFactory::MakeForStaticData(Dict::S('UI:Schema:Events:Listeners'), $aColumns, $aRows);
$oPage->AddSubBlock($oTable);
}
/**
* Display the list of classes from the business model
*/
@@ -1061,6 +1147,9 @@ EOF
$oPage->SetCurrentTab('UI:Schema:Triggers');
DisplayTriggers($oPage, $sClass);
$oPage->SetCurrentTab('UI:Schema:Events');
DisplayEvents($oPage, $sClass);
$oPage->SetCurrentTab();
$oPage->SetCurrentTabContainer();
}

View File

@@ -372,6 +372,7 @@ class MFCompiler
}
$this->LoadSnippets();
$this->LoadGlobalEventListeners();
// Compile, module by module
//
@@ -418,7 +419,16 @@ class MFCompiler
$sCompiledCode .= $this->CompileConstant($oConstant)."\n";
}
}
$oEvents = $this->oFactory->ListEvents($sModuleName);
if ($oEvents->length > 0)
{
foreach($oEvents as $oEvent)
{
$sCompiledCode .= $this->CompileEvent($oEvent, $sModuleName)."\n";
}
}
if (array_key_exists($sModuleName, $this->aSnippets))
{
foreach( $this->aSnippets[$sModuleName]['before'] as $aSnippet)
@@ -1082,6 +1092,20 @@ EOF
return $sRet;
}
protected function CompileEvent(DesignElement $oEvent, string $sModuleName)
{
$sName = $oEvent->getAttribute('id');
$aEventDescription = DesignElement::ToArray($oEvent);
$sDescription = var_export($aEventDescription, true);
$sConstant = $sName;
$sOutput = "define('$sConstant', '$sName');\n";
$sOutput .= "Combodo\iTop\Service\EventService::RegisterEvent('$sName', $sDescription, '$sModuleName');\n";
return $sOutput;
}
protected function CompileConstant($oConstant)
{
$sName = $oConstant->getAttribute('id');
@@ -1273,6 +1297,7 @@ EOF
foreach($oIndexes->getElementsByTagName('index') as $oIndex)
{
$sIndexId = $oIndex->getAttribute('id');
/** @var DesignElement $oAttributes */
$oAttributes = $oIndex->GetUniqueElement('attributes');
foreach ($oAttributes->getElementsByTagName('attribute') as $oAttribute) {
$aIndexes[$sIndexId][] = $oAttribute->getAttribute('id');
@@ -1281,6 +1306,52 @@ EOF
$aClassParams['indexes'] = var_export($aIndexes, true);
}
$sEvents = '';
$sMethods = '';
$oHooks = $oClass->GetOptionalElement('event_listeners');
if ($oHooks) {
foreach ($oHooks->getElementsByTagName('listener') as $oListener) {
/** @var DesignElement $oListener */
$oEventNode = $oListener->GetUniqueElement('event');
/** @var DesignElement $oEventNode $oEventNode */
$sEventName = $oEventNode->GetText();
$sListenerId = $oListener->getAttribute('id');
$oCallback = $oListener->GetUniqueElement('callback', false);
if (is_object($oCallback)) {
$sCallback = $oCallback->GetText();
} else {
$oExecute = $oListener->GetUniqueElement('execute', true);
$sExecute = trim($oExecute->GetText());
$sCallback = "EventHook_{$sEventName}_$sListenerId";
$sCallbackFct = preg_replace('@^function\s*\(@', "public function $sCallback(", $sExecute);
if ($sExecute == $sCallbackFct) {
throw new DOMFormatException("Malformed tag <execute> in class: $sClass hook: $sEventName listener: $sListenerId");
}
$sMethods .= "\n $sCallbackFct\n\n";
}
if (strpos($sCallback, '::') === false) {
$sEventListener = '[$this, \''.$sCallback.'\']';
} else {
$sEventListener = "'$sCallback'";
}
$sListenerPriority = (float)($oListener->GetChildText('priority', '0'));
$sEvents .= "\n Combodo\iTop\Service\EventService::RegisterListener(\"$sEventName\", $sEventListener, \$this->m_sObjectUniqId, \"$sListenerId\", null, $sListenerPriority, '$sModuleRelativeDir');";
}
}
if (!empty($sEvents))
{
$sMethods .= <<<EOF
protected function RegisterEvents()
{
parent::RegisterEvents();
$sEvents
}
EOF;
}
if ($oArchive = $oProperties->GetOptionalElement('archive')) {
$bEnabled = $this->GetPropBoolean($oArchive, 'enabled', false);
$aClassParams['archive'] = $bEnabled;
@@ -2077,7 +2148,6 @@ EOF
}
// Methods
$sMethods = "";
$oMethods = $oClass->GetUniqueElement('methods');
foreach($oMethods->getElementsByTagName('method') as $oMethod)
{
@@ -2208,6 +2278,7 @@ $sLifecycle
$sHighlightScale
$sZlists;
EOF;
// some other stuff (magical attributes like friendlyName) are done in MetaModel::InitClasses and though not present in the
// generated PHP
$sPHP .= $this->GeneratePhpCodeForClass($sClassName, $sParentClass, $sClassParams, $sInitMethodCalls, $bIsAbstractClass, $sMethods, $aRequiredFiles, $sCodeComment);
@@ -3538,6 +3609,103 @@ EOF;
foreach($this->aSnippets as $sModuleId => $void)
{
uasort($this->aSnippets[$sModuleId]['before'], array(get_class($this), 'SortOnRank'));
uasort($this->aSnippets[$sModuleId]['after'], array(get_class($this), 'SortOnRank'));
}
}
/**
* @throws \DOMFormatException
*/
protected function LoadGlobalEventListeners()
{
$sClassName = 'GlobalEventListeners';
$sModuleId = '_core_';
if (!array_key_exists($sModuleId, $this->aSnippets)) {
$this->aSnippets[$sModuleId] = ['before' => [], 'after' => []];
}
$oEventListeners = $this->oFactory->GetNodes('/itop_design/event_listeners/listener');
$aEventListeners = [];
foreach ($oEventListeners as $oListener) {
/** @var \DOMElement $oListener */
$sListenerId = $oListener->getAttribute('id');
$sEventName = $oListener->GetChildText('event');
$oExecute = $oListener->GetUniqueElement('execute');
$sExecute = trim($oExecute->GetText());
$sCallback = "{$sEventName}_{$sListenerId}";
$sCallbackFct = preg_replace('@^function\s*\(@', "public static function $sCallback(", $sExecute);
if ($sExecute == $sCallbackFct) {
throw new DOMFormatException("Malformed tag <execute> in event: $sEventName listener: $sListenerId");
}
$fPriority = (float)($oListener->GetChildText('priority', '0'));
$aFilters = [];
$oFilters = $oListener->GetNodes('filters/filter');
foreach ($oFilters as $oFilter) {
$aFilters[] = $oFilter->GetText();
}
if (empty($aFilters)) {
$sEventSource = 'null';
} else {
$sEventSource = '["'.implode('", "', $aFilters).'"]';
}
$aContexts = [];
$oContexts = $oListener->GetNodes('contexts/context');
foreach ($oContexts as $oContext) {
$aContexts[] = $oContext->GetText();
}
if (empty($aContexts)) {
$sContext = 'null';
} else {
$sContext = '["'.implode('", "', $aContexts).'"]';
}
$aEventListeners[] = array(
'event_name' => $sEventName,
'callback' => $sCallback,
'content' => $sCallbackFct,
'priority' => $fPriority,
'source' => $sEventSource,
'context' => $sContext,
);
}
if (empty($aEventListeners)) {
return;
}
$sRegister = '';
$sMethods = '';
foreach ($aEventListeners as $aListener) {
$sCallback = $aListener['callback'];
$sEventName = $aListener['event_name'];
$sEventSource = $aListener['source'];
$sContext = $aListener['context'];
$sPriority = $aListener['priority'];
$sRegister .= "\nCombodo\iTop\Service\EventService::RegisterListener(\"$sEventName\", '$sClassName::$sCallback', $sEventSource, null, $sContext, $sPriority, '$sModuleId');";
$sCallbackFct = $aListener['content'];
$sMethods .= "\n $sCallbackFct\n\n";
}
$sContent = <<<PHP
class $sClassName
{
$sMethods
}
$sRegister
PHP;
$fOrder = 0;
$this->aSnippets[$sModuleId]['after'][] = array(
'rank' => $fOrder,
'content' => $sContent,
'snippet_id' => $sClassName,
);
foreach ($this->aSnippets as $sModuleId => $void) {
uasort($this->aSnippets[$sModuleId]['after'], array(get_class($this), 'SortOnRank'));
}
}

View File

@@ -580,6 +580,8 @@ class ModelFactory
$this->oMeta = $this->oDOMDocument->CreateElement('meta');
$this->oRoot->AppendChild($this->oMeta);
$this->oMeta = $this->oDOMDocument->CreateElement('events');
$this->oRoot->AppendChild($this->oMeta);
foreach ($aRootNodeExtensions as $sElementName)
{
@@ -860,6 +862,14 @@ class ModelFactory
$oNode->SetAttribute('_created_in', $sModuleName);
}
}
$oNodeList = $oXPath->query('/itop_design/events/event');
foreach ($oNodeList as $oNode)
{
if ($oNode->getAttribute('_created_in') == '')
{
$oNode->SetAttribute('_created_in', $sModuleName);
}
}
$oNodeList = $oXPath->query('/itop_design/menus/menu');
foreach ($oNodeList as $oNode)
{
@@ -1252,6 +1262,19 @@ EOF
return $this->GetNodes("/itop_design/constants/constant[@_created_in='$sModuleName']");
}
/**
* List all events from the DOM, for a given module
*
* @param string $sModuleName
*
* @return \DOMNodeList
* @throws Exception
*/
public function ListEvents($sModuleName)
{
return $this->GetNodes("/itop_design/events/event[@_created_in='$sModuleName']");
}
/**
* List all classes from the DOM, for a given module
*

View File

@@ -0,0 +1,90 @@
<?php
/**
* @copyright Copyright (C) 2010-2020 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Service;
/**
* Data given to the Event Service callbacks
* Class EventServiceData
*
* @package Combodo\iTop\Service
*/
class EventData
{
private $sEvent;
private $mEventSource;
private $aEventData;
private $aCallbackData;
/**
* EventServiceData constructor.
*
* @param string $sEvent
* @param string|array|null $mEventSource
* @param array $aEventData
*/
public function __construct(string $sEvent, $mEventSource = null, array $aEventData = [])
{
$this->sEvent = $sEvent;
$this->aEventData = $aEventData;
$this->mEventSource = $mEventSource;
$this->aCallbackData = [];
}
/**
* @return string
*/
public function GetEvent()
{
return $this->sEvent;
}
public function Get($sParam)
{
if (is_array($this->aEventData) && isset($this->aEventData[$sParam])) {
return $this->aEventData[$sParam];
}
if (is_array($this->aCallbackData) && isset($this->aCallbackData[$sParam])) {
return $this->aCallbackData[$sParam];
}
return null;
}
/**
* @return mixed
*/
public function GetEventSource()
{
return $this->mEventSource;
}
/**
* @return array
*/
public function GetEventData(): array
{
return $this->aEventData;
}
/**
* @param mixed $aCallbackData
*/
public function SetCallbackData($aCallbackData)
{
$this->aCallbackData = $aCallbackData;
}
/**
* @return mixed
*/
public function GetCallbackData()
{
return $this->aCallbackData;
}
}

View File

@@ -0,0 +1,109 @@
<?php
/*
* @copyright Copyright (C) 2010-2022 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Service;
use IssueLog;
use LogChannels;
use SetupUtils;
use utils;
class EventHelper
{
public static function GetCachedClasses($sModuleName, callable $ListBuilder)
{
$aClasses = [];
$sCacheFileName = '';
if (!utils::IsDevelopmentEnvironment()) {
// Try to read from cache
$sCacheFileName = utils::GetCachePath()."EventsClassList/$sModuleName.php";
if (is_file($sCacheFileName)) {
$aClasses = include $sCacheFileName;
}
}
if (empty($aClasses)) {
$aClasses = call_user_func($ListBuilder);
if (!utils::IsDevelopmentEnvironment() && !empty($aClasses)) {
// Save to cache
$sCacheContent = "<?php\n\nreturn ".var_export($aClasses, true).";";
SetupUtils::builddir(dirname($sCacheFileName));
file_put_contents($sCacheFileName, $sCacheContent);
}
}
return $aClasses;
}
public static function Trace($sMessage)
{
IssueLog::Trace($sMessage, LogChannels::EVENT_SERVICE);
}
public static function Debug($sMessage, $sEvent, $sources)
{
$oConfig = utils::GetConfig();
$aLogEvents = $oConfig->Get('event_service.debug.filter_events');
$aLogSources = $oConfig->Get('event_service.debug.filter_sources');
if (is_array($aLogEvents)) {
if (!in_array($sEvent, $aLogEvents)) {
return;
}
}
if (is_array($aLogSources)) {
if (!self::MatchEventSource($aLogSources, $sources)) {
return;
}
}
IssueLog::Debug($sMessage, LogChannels::EVENT_SERVICE);
}
public static function Error($sMessage)
{
IssueLog::Error($sMessage, LogChannels::EVENT_SERVICE);
}
public static function MatchEventSource($srcRegistered, $srcEvent): bool
{
if (empty($srcRegistered)) {
// no filtering
return true;
}
if (empty($srcEvent)) {
// no match (the registered source is not empty)
return false;
}
if (is_string($srcRegistered)) {
$aSrcRegistered = [$srcRegistered];
} elseif (is_array($srcRegistered)) {
$aSrcRegistered = $srcRegistered;
} else {
$aSrcRegistered = [];
}
if (is_string($srcEvent)) {
$aSrcEvent = [$srcEvent];
} elseif (is_array($srcEvent)) {
$aSrcEvent = $srcEvent;
} else {
$aSrcEvent = [];
}
foreach ($aSrcEvent as $sSrcEvent) {
if (in_array($sSrcEvent, $aSrcRegistered)) {
// sources matches
return true;
}
}
// no match
return false;
}
}

View File

@@ -0,0 +1,294 @@
<?php
/**
* @copyright Copyright (C) 2010-2020 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Service;
use Closure;
use ContextTag;
use CoreException;
use Exception;
use ExecutionKPI;
use ReflectionClass;
class EventService
{
public static $aEventListeners = [];
private static $iEventIdCounter = 0;
private static $aEventDescription = [];
/**
* Register a callback for a specific event
*
* @param string $sEvent corresponding event
* @param callable $callback The callback to call
* @param array|string|null $sEventSource event filtering depending on the source of the event
* @param mixed $aCallbackData optional data given by the registrar to the callback
* @param array|string|null $context context filter
* @param float $fPriority optional priority for callback order
*
* @return string Id of the registration (used for unregistering)
*
*/
public static function RegisterListener(string $sEvent, callable $callback, $sEventSource = null, $aCallbackData = [], $context = null, float $fPriority = 0.0, $sModuleId = ''): string
{
if (!is_callable($callback, false, $sName)) {
return false;
}
$aEventCallbacks = self::$aEventListeners[$sEvent] ?? [];
$sId = 'event_'.self::$iEventIdCounter++;
$aEventCallbacks[] = array(
'id' => $sId,
'event' => $sEvent,
'callback' => $callback,
'source' => $sEventSource,
'name' => $sName,
'data' => $aCallbackData,
'context' => $context,
'priority' => $fPriority,
'module' => $sModuleId,
);
usort($aEventCallbacks, function ($a, $b) {
$fPriorityA = $a['priority'];
$fPriorityB = $b['priority'];
if ($fPriorityA == $fPriorityB) {
return 0;
}
return ($fPriorityA < $fPriorityB) ? -1 : 1;
});
self::$aEventListeners[$sEvent] = $aEventCallbacks;
$iTotalRegistrations = 0;
foreach (self::$aEventListeners as $aEvent) {
$iTotalRegistrations += count($aEvent);
}
$sLogEventName = "$sEvent:".self::GetSourcesAsString($sEventSource);
EventHelper::Trace("Registering event '$sLogEventName' for '$sName' with id '$sId' (total $iTotalRegistrations)");
return $sId;
}
public static function GetListenersAsJSON()
{
return json_encode(self::$aEventListeners, JSON_PRETTY_PRINT);
}
/**
* Fire an event. Call all the callbacks registered for this event.
*
* @param \Combodo\iTop\Service\EventData $oEventData
*
* @throws \Exception from the callback
*/
public static function FireEvent(EventData $oEventData)
{
$sEvent = $oEventData->GetEvent();
if (!array_key_exists($sEvent, self::$aEventDescription)) {
$sError = "Fire event error: Event $sEvent is not registered";
EventHelper::Error($sError);
throw new CoreException($sError);
}
$eventSource = $oEventData->GetEventSource();
$oKPI = new ExecutionKPI();
$sLogEventName = "$sEvent - ".self::GetSourcesAsString($eventSource).' '.json_encode($oEventData->GetEventData());
EventHelper::Trace("Fire event '$sLogEventName'");
if (!isset(self::$aEventListeners[$sEvent])) {
EventHelper::Debug("No listener for '$sLogEventName'", $sEvent, $eventSource);
$oKPI->ComputeStats('FireEvent', $sEvent);
return;
}
foreach (self::GetListeners($sEvent, $eventSource) as $aEventCallback) {
if (!self::MatchContext($aEventCallback['context'])) {
continue;
}
$sName = $aEventCallback['name'];
EventHelper::Debug("Fire event '$sLogEventName' calling '$sName'", $sEvent, $eventSource);
try {
$oEventData->SetCallbackData($aEventCallback['data']);
call_user_func($aEventCallback['callback'], $oEventData);
}
catch (Exception $e) {
EventHelper::Error("Event '$sLogEventName' for '$sName' id {$aEventCallback['id']} failed with error: ".$e->getMessage());
throw $e;
}
}
EventHelper::Debug("End of event '$sLogEventName'", $sEvent, $eventSource);
$oKPI->ComputeStats('FireEvent', $sEvent);
}
public static function GetListeners($sEvent, $eventSource)
{
$aListeners = [];
if (isset(self::$aEventListeners[$sEvent])) {
foreach (self::$aEventListeners[$sEvent] as $aEventCallback) {
if (EventHelper::MatchEventSource($aEventCallback['source'], $eventSource)) {
$aListeners[] = $aEventCallback;
}
}
}
return $aListeners;
}
private static function MatchContext($registeredContext): bool
{
if (empty($registeredContext)) {
return true;
}
if (is_string($registeredContext)) {
$aContexts = array($registeredContext);
} elseif (is_array($registeredContext)) {
$aContexts = $registeredContext;
} else {
return false;
}
foreach ($aContexts as $sContext) {
if (ContextTag::Check($sContext)) {
return true;
}
}
return false;
}
private static function GetSourcesAsString($srcRegistered): string
{
if (empty($srcRegistered)) {
return '';
}
if (is_string($srcRegistered)) {
return substr($srcRegistered, 0, 30);
}
if (is_array($srcRegistered)) {
return substr(implode(',', $srcRegistered), 0, 30);
}
return '';
}
/**
* Unregister a previously registered callback
*
* @param string $sId the callback registration id
*/
public static function UnRegisterListener(string $sId)
{
$bRemoved = self::Browse(function ($sEvent, $idx, $aEventCallback) use ($sId) {
if ($aEventCallback['id'] == $sId) {
$sName = self::$aEventListeners[$sEvent][$idx]['name'];
unset (self::$aEventListeners[$sEvent][$idx]);
EventHelper::Trace("Unregistered callback '$sName' id $sId' on event '$sEvent'");
return false;
}
return true;
});
if (!$bRemoved) {
EventHelper::Trace("No registration found for callback '$sId'");
}
}
/**
* Unregister an event
*
* @param string $sEvent event to unregister
*/
public static function UnRegisterEventListeners(string $sEvent)
{
if (!isset(self::$aEventListeners[$sEvent])) {
EventHelper::Trace("No registration found for event '$sEvent'");
return;
}
unset(self::$aEventListeners[$sEvent]);
EventHelper::Trace("Unregistered all the callbacks on event '$sEvent'");
}
/**
* Unregister all the events
*/
public static function UnRegisterAll()
{
self::$aEventListeners = array();
EventHelper::Trace("Unregistered all events");
}
/**
* Browse all the registrations
*
* @param \Closure $callback function($sEvent, $idx, $aEventCallback) to call (return false to interrupt the browsing)
*
* @return bool true if interrupted else false
*/
private static function Browse(closure $callback): bool
{
foreach (self::$aEventListeners as $sEvent => $aCallbackList) {
foreach ($aCallbackList as $idx => $aEventCallback) {
if (call_user_func($callback, $sEvent, $idx, $aEventCallback) === false) {
return true;
}
}
}
return false;
}
// For information only
public static function RegisterEvent(string $sEvent, array $aDescription, string $sModule)
{
if (isset(self::$aEventDescription[$sEvent])) {
$sPrevious = self::$aEventDescription[$sEvent]['module'];
EventHelper::Error("The Event $sEvent defined by $sModule has already been defined in $sPrevious, check your delta");
}
self::$aEventDescription[$sEvent] = [
'name'=> $sEvent,
'description' => $aDescription,
'module' => $sModule,
];
}
public static function GetEventsByClass($sClass)
{
$aRes = [];
$oClass = new ReflectionClass($sClass);
foreach (self::$aEventDescription as $sEvent => $aEventInfo) {
if (isset($aEventInfo['description']['sources'])) {
foreach ($aEventInfo['description']['sources'] as $sSource) {
if ($sClass == $sSource || $oClass->isSubclassOf($sSource)) {
$aRes[$sEvent] = $aEventInfo;
}
}
}
}
return $aRes;
}
// Intentionally duplicated from SetupUtils, not yet loaded when RegisterEvent is called
private static function FromCamelCase($sInput) {
$sPattern = '!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!';
preg_match_all($sPattern, $sInput, $aMatches);
$aRet = $aMatches[0];
foreach ($aRet as &$sMatch) {
$sMatch = $sMatch == strtoupper($sMatch) ?
strtolower($sMatch) :
lcfirst($sMatch);
}
return implode('_', $aRet);
}
public static function GetDefinedEventsAsJSON()
{
return json_encode(self::$aEventDescription, JSON_PRETTY_PRINT);
}
}

View File

@@ -21,6 +21,7 @@ use DataTableConfig;
class DataTable extends UIContentBlock
{
use tJSRefreshCallback;
use tTableRowActions;
// Overloaded constants
public const BLOCK_CODE = 'ibo-datatable';
@@ -51,6 +52,7 @@ class DataTable extends UIContentBlock
*/
protected $aInitDisplayData;
/**
* Panel constructor.
*
@@ -250,4 +252,5 @@ class DataTable extends UIContentBlock
return [];
}
}

View File

@@ -12,6 +12,7 @@ use appUserPreferences;
use AttributeLinkedSet;
use cmdbAbstractObject;
use Combodo\iTop\Application\UI\Base\AbstractUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\CollapsibleSection\CollapsibleSection;
use Combodo\iTop\Application\UI\Base\Component\DataTable\StaticTable\FormTable\FormTable;
use Combodo\iTop\Application\UI\Base\Component\DataTable\StaticTable\FormTableRow\FormTableRow;
@@ -19,8 +20,10 @@ use Combodo\iTop\Application\UI\Base\Component\DataTable\StaticTable\StaticTable
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
use Combodo\iTop\Application\UI\Base\Component\Html\HtmlFactory;
use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Template\TemplateUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Toolbar\ToolbarUIBlockFactory;
use Combodo\iTop\Application\UI\Base\iUIBlock;
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
use Combodo\iTop\Controller\AjaxRenderController;
use DBObjectSet;
@@ -180,6 +183,46 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
return $oContainer;
}
/**
* Make a row actions toolbar template.
*
* @param iUIBlock $oTable datatable object that needs to use tTableRowActions trait
*
* @return \Combodo\iTop\Application\UI\Base\Component\Template\Template
* @throws \Exception
* @since 3.1.0
*/
public static function MakeActionRowToolbarTemplate(iUIBlock $oTable)
{
// test trait
$sTableClass = get_class($oTable);
if (!utils::IsTraitUsedByClass(tTableRowActions::class, $sTableClass)) {
throw new \Exception("DataTableUIBlockFactory::MakeActionRowToolbarTemplate: {$sTableClass} iUIBlock needs tTableRowActions trait");
}
// row actions template
$oTemplate = TemplateUIBlockFactory::MakeStandard($oTable->GetId().'_actions_buttons_template');
// row actions toolbar container
$oToolbar = ToolbarUIBlockFactory::MakeStandard();
$oToolbar->AddCSSClass('ibo-datatable--row-actions-toolbar');
// for each action...create an icon button
foreach ($oTable->GetRowActions() as $iKey => $aAction) {
$oButton = ButtonUIBlockFactory::MakeIconAction(
array_key_exists('icon_classes', $aAction) ? $aAction['icon_classes'] : 'fas fa-question',
array_key_exists('tooltip', $aAction) ? $aAction['tooltip'] : '',
array_key_exists('name', $aAction) ? $aAction['name'] : 'undefined'
);
$oButton->SetDataAttributes(['action-id' => $iKey]);
$oToolbar->AddSubBlock($oButton);
}
$oTemplate->AddSubBlock($oToolbar);
return $oTemplate;
}
/**
* Make a basis Panel component
*
@@ -457,9 +500,8 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
} else {
$aOptions['sSelectedRows'] = '[]';
}
$aExtraParams['table_id']=$sTableId;
$aExtraParams['list_id']=$sListId;
$aExtraParams['table_id'] = $sTableId;
$aExtraParams['list_id'] = $sListId;
$oDataTable->SetOptions($aOptions);
$oDataTable->SetAjaxUrl(utils::GetAbsoluteUrlAppRoot()."pages/ajax.render.php");
@@ -475,6 +517,12 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
$oDataTable->SetResultColumns($oCustomSettings->aColumns);
$oDataTable->SetInitDisplayData(AjaxRenderController::GetDataForTable($oSet, $aClassAliases, $aColumnsToLoad, $sIdName, $aExtraParams));
// row actions
if (isset($aExtraParams['row_actions'])) {
$oDataTable->SetRowActions($aExtraParams['row_actions']);
}
return $oDataTable;
}
@@ -713,6 +761,11 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
$oDataTable->SetResultColumns($oCustomSettings->aColumns);
$oDataTable->SetInitDisplayData(AjaxRenderController::GetDataForTable($oSet, $aClassAliases, $aColumnsToLoad, $sIdName, $aExtraParams));
// row actions
if (isset($aExtraParams['row_actions'])) {
$oDataTable->SetRowActions($aExtraParams['row_actions']);
}
return $oDataTable;
}
@@ -908,6 +961,7 @@ JS;
* @param array $aExtraParams
* @param string $sFilter
* @param array $aOptions
* @param array $aRowActions @since 3.1.0
* *
* $aColumns =[
* 'nameField1' => ['label' => labelFIeld1, 'description' => descriptionField1],
@@ -917,7 +971,7 @@ JS;
*
* @return \Combodo\iTop\Application\UI\Base\Layout\UIContentBlock
*/
public static function MakeForStaticData(string $sTitle, array $aColumns, array $aData, ?string $sId = null, array $aExtraParams = [], string $sFilter = "", array $aOptions = [])
public static function MakeForStaticData(string $sTitle, array $aColumns, array $aData, ?string $sId = null, array $aExtraParams = [], string $sFilter = "", array $aOptions = [], array $aRowActions = null)
{
$oBlock = new UIContentBlock();
if ($sTitle != "") {
@@ -925,6 +979,13 @@ JS;
$oBlock->AddSubBlock($oTitle);
}
$oTable = new StaticTable($sId, [], $aExtraParams);
if ($aRowActions != null) {
$oTable->SetRowActions($aRowActions);
$aColumns['actions'] = [
'label' => Dict::S('UI:Datatables:Column:RowActions:Label'),
'description' => Dict::S('UI:Datatables:Column:RowActions:Description'),
];
}
$oTable->SetColumns($aColumns);
$oTable->SetData($aData);
$oTable->SetFilter($sFilter);
@@ -940,6 +1001,7 @@ JS;
* @param array $aColumns
* @param array $aData
* @param string $sFilter
* @param array $aRowActions @since 3.1.0
*
* $aColumns =[
* 'nameField1' => ['label' => labelFIeld1, 'description' => descriptionField1],
@@ -949,10 +1011,17 @@ JS;
*
* @return \Combodo\iTop\Application\UI\Base\Component\DataTable\StaticTable\FormTable\FormTable
*/
public static function MakeForForm(string $sRef, array $aColumns, array $aData = [], string $sFilter = '')
public static function MakeForForm(string $sRef, array $aColumns, array $aData = [], string $sFilter = '', array $aRowActions = null)
{
$oTable = new FormTable("datatable_".$sRef);
$oTable->SetRef($sRef);
if ($aRowActions != null) {
$oTable->SetRowActions($aRowActions);
$aColumns['actions'] = [
'label' => Dict::S('UI:Datatables:Column:RowActions:Label'),
'description' => Dict::S('UI:Datatables:Column:RowActions:Description'),
];
}
$oTable->SetColumns($aColumns);
$oTable->SetFilter($sFilter);
@@ -970,24 +1039,44 @@ JS;
public static function GetAllowedParams(): array
{
return [
'surround_with_panel', /** bool embed table into a Panel */
'menu', /** bool display table menu */
'view_link', /** bool display the friendlyname column with links to the objects details */
'link_attr', /** string link att code */
'object_id', /** int Id of the object linked */
'target_attr', /** string target att code of the link */
'selection_mode', /** bool activate selection */
'selection_type', /** string 'multiple' or 'single' */
'extra_fields', /** string comma separated list of link att code to display ('alias.attcode')*/
'zlist', /** string name of the zlist to display when 'extra_fields' is not set */
'display_limit', /** bool if true pagination is used (default = true) */
'table_id', /** string datatable id */
'cssCount', /** string external counter (input hidden) js selector */
'selected_rows', /** array list of Ids already selected when displaying the datatable */
'display_aliases', /** string comma separated list of class aliases to display */
'list_id', /** string list outer id */
'selection_enabled', /** list of id in witch select is allowed, if not exists all lines are selectable */
'id_for_select', /**give definition of id for select checkbox*/
'surround_with_panel',
/** bool embed table into a Panel */
'menu',
/** bool display table menu */
'view_link',
/** bool display the friendlyname column with links to the objects details */
'link_attr',
/** string link att code */
'object_id',
/** int Id of the object linked */
'target_attr',
/** string target att code of the link */
'selection_mode',
/** bool activate selection */
'selection_type',
/** string 'multiple' or 'single' */
'extra_fields',
/** string comma separated list of link att code to display ('alias.attcode')*/
'zlist',
/** string name of the zlist to display when 'extra_fields' is not set */
'display_limit',
/** bool if true pagination is used (default = true) */
'table_id',
/** string datatable id */
'cssCount',
/** string external counter (input hidden) js selector */
'selected_rows',
/** array list of Ids already selected when displaying the datatable */
'display_aliases',
/** string comma separated list of class aliases to display */
'list_id',
/** string list outer id */
'selection_enabled',
/** list of id in witch select is allowed, if not exists all lines are selectable */
'id_for_select',
/**give definition of id for select checkbox*/
'row_actions',
/** array of blocks displayed on every row */
];
}
}

View File

@@ -9,6 +9,7 @@ namespace Combodo\iTop\Application\UI\Base\Component\DataTable\StaticTable\FormT
use Combodo\iTop\Application\UI\Base\Component\DataTable\StaticTable\FormTableRow\FormTableRow;
use Combodo\iTop\Application\UI\Base\Component\DataTable\StaticTable\StaticTable;
use Combodo\iTop\Application\UI\Base\Component\DataTable\tTableRowActions;
use Combodo\iTop\Application\UI\Base\iUIBlock;
/**
@@ -19,10 +20,10 @@ use Combodo\iTop\Application\UI\Base\iUIBlock;
class FormTable extends StaticTable
{
// Overloaded constants
public const BLOCK_CODE = 'ibo-formtable';
public const REQUIRES_ANCESTORS_DEFAULT_JS_FILES = true;
public const REQUIRES_ANCESTORS_DEFAULT_CSS_FILES = true;
public const DEFAULT_HTML_TEMPLATE_REL_PATH = 'base/components/datatable/static/formtable/layout';
public const BLOCK_CODE = 'ibo-formtable';
public const REQUIRES_ANCESTORS_DEFAULT_JS_FILES = true;
public const REQUIRES_ANCESTORS_DEFAULT_CSS_FILES = true;
public const DEFAULT_HTML_TEMPLATE_REL_PATH = 'base/components/datatable/static/formtable/layout';
public const DEFAULT_JS_ON_READY_TEMPLATE_REL_PATH = 'base/components/datatable/static/formtable/layout';
/** @var string */

View File

@@ -2,6 +2,7 @@
namespace Combodo\iTop\Application\UI\Base\Component\DataTable\StaticTable;
use Combodo\iTop\Application\UI\Base\Component\DataTable\tTableRowActions;
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
use Combodo\iTop\Application\UI\Base\tJSRefreshCallback;
use utils;
@@ -18,12 +19,13 @@ use utils;
class StaticTable extends UIContentBlock
{
use tJSRefreshCallback;
use tTableRowActions;
// Overloaded constants
public const BLOCK_CODE = 'ibo-datatable';
public const DEFAULT_HTML_TEMPLATE_REL_PATH = 'base/components/datatable/static/layout';
public const BLOCK_CODE = 'ibo-datatable';
public const DEFAULT_HTML_TEMPLATE_REL_PATH = 'base/components/datatable/static/layout';
public const DEFAULT_JS_ON_READY_TEMPLATE_REL_PATH = 'base/components/datatable/static/layout';
public const DEFAULT_JS_FILES_REL_PATH = [
public const DEFAULT_JS_FILES_REL_PATH = [
'node_modules/datatables.net/js/jquery.dataTables.min.js',
'node_modules/datatables.net-fixedheader/js/dataTables.fixedHeader.min.js',
'node_modules/datatables.net-responsive/js/dataTables.responsive.min.js',
@@ -59,7 +61,7 @@ class StaticTable extends UIContentBlock
private $aExtraParams;
/*@var string $sUrlForRefresh*/
private $sFilter;
/** @var array $aOptions
/** @var array $aOptions
* List of specific options for display datatable
*/
private $aOptions;
@@ -81,6 +83,17 @@ class StaticTable extends UIContentBlock
return $this->aColumns;
}
/**
* Return columns count.
*
* @return int
* @since 3.1.0
*/
public function GetColumnsCount(): int
{
return count($this->aColumns);
}
/**
* @param array $aColumns
*
@@ -129,8 +142,8 @@ class StaticTable extends UIContentBlock
{
//$('#".$this->sId."').DataTable().clear().rows.add(data).draw()
$aParams = [
'style' => 'list',
'filter' => $this->sFilter,
'style' => 'list',
'filter' => $this->sFilter,
'extra_params' => $this->aExtraParams,
];
@@ -140,7 +153,7 @@ class StaticTable extends UIContentBlock
$('#".$this->sId."').dataTable().fnAddData(data);
});";
}
/**
* @return mixed
*/
@@ -149,6 +162,7 @@ class StaticTable extends UIContentBlock
if (isset($this->aOptions[$sOption])) {
return $this->aOptions[$sOption];
}
return null;
}

View File

@@ -0,0 +1,73 @@
<?php
/*
* @copyright Copyright (C) 2010-2022 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Application\UI\Base\Component\DataTable;
/**
* Trait tTableRowActions
*
* This brings the ability to add action rows to tables.
*
* @internal
* @package Combodo\iTop\Application\UI\Base\Component\DataTable
* @since 3.1.0
*/
trait tTableRowActions
{
/**
* @var $aRowActions array array of row actions
* action => {
* tooltip: string,
* icon_classes: string,
* js_row_action: string
* }
*/
protected $aRowActions;
/**
* Set row actions.
*
* @param array $aRowActions
*
* @return $this
*/
public function SetRowActions(array $aRowActions)
{
$this->aRowActions = $aRowActions;
return $this;
}
/**
* Get row actions.
*
* @return array
*/
public function GetRowActions(): array
{
return $this->aRowActions;
}
/**
* Return true if row actions is set and not empty.
*
* @return bool
*/
public function HasRowActions(): bool
{
return isset($this->aRowActions) && count($this->aRowActions);
}
/**
* Return row actions template.
*
* @return \Combodo\iTop\Application\UI\Base\Component\Template\Template
*/
public function GetRowActionsTemplate()
{
return DataTableUIBlockFactory::MakeActionRowToolbarTemplate($this);
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* Copyright (C) 2013-2022 Combodo SARL
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
*/
namespace Combodo\iTop\Application\UI\Base\Component\Template;
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
/**
* Class Template
*
* @author Benjamin Dalsass<benjamin.dalsass@combodo.com>
* @package Combodo\iTop\Application\UI\Base\Component\Template
* @since 3.1.0
*/
class Template extends UIContentBlock
{
// Overloaded constants
public const BLOCK_CODE = 'ibo-template';
public const DEFAULT_HTML_TEMPLATE_REL_PATH = 'base/components/template/layout';
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* Copyright (C) 2013-2022 Combodo SARL
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
*/
namespace Combodo\iTop\Application\UI\Base\Component\Template;
use Combodo\iTop\Application\UI\Base\AbstractUIBlockFactory;
/**
* Class TemplateUIBlockFactory
*
* @api
*
* @author Benjamin Dalsass <benjamin.dalsass@combodo.com>
* @package Combodo\iTop\Application\UI\Base\Component\Template
* @since 3.1.0
* @link
*/
class TemplateUIBlockFactory extends AbstractUIBlockFactory
{
/** @inheritDoc */
public const TWIG_TAG_NAME = 'UITemplate';
/** @inheritDoc */
public const UI_BLOCK_CLASS_NAME = Template::class;
/**
* Make a Template component
*
* @return \Combodo\iTop\Application\UI\Base\Component\Template\Template
*/
public static function MakeStandard(string $sId)
{
return new Template($sId);
}
}

View File

@@ -16,6 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* Simple web page with no includes or fancy formatting, useful to generateXML documents
* The page adds the content-type text/XML and the encoding into the headers

View File

@@ -5,6 +5,7 @@
*/
/**
* Class DownloadPage
*

View File

@@ -4,6 +4,7 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
/**
* Class JsonPage
*

View File

@@ -142,7 +142,6 @@ EOF
/**
* Generates the PDF document and returns the PDF content as a string
*
* @return string
* @see WebPage::output()
*/
public function output()

View File

@@ -174,6 +174,7 @@ class UnauthenticatedWebPage extends NiceWebPage
$oKpi->ComputeAndReport(get_class($this).' output');
echo $sHtml;
$oKpi->ComputeAndReport('Echoing ('.round(strlen($sHtml) / 1024).' Kb)');
ExecutionKPI::ReportStats();
}
/**

View File

@@ -23,6 +23,7 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
/**
* Simple web page with no includes or fancy formatting, useful to generateXML documents
* The page adds the content-type text/XML and the encoding into the headers

View File

@@ -20,5 +20,12 @@
{% for aColumn in oUIBlock.GetDisplayColumns() %}
<th class="ibo-datatable-header" {% if aColumn["description"] is not empty %}title="{{ aColumn["description"] }}"{% endif %}>{{ aColumn["attribute_label"] }} </th>
{% endfor %}
{% if oUIBlock.HasRowActions() %}
<th class="ibo-datatable-header">{{ 'UI:Datatables:Column:RowActions:Label'|dict_s }} </th>
{% endif %}
</thead>
</table>
</table>
{% if oUIBlock.HasRowActions() %}
{{ render_block(oUIBlock.GetRowActionsTemplate()) }}
{% endif %}

View File

@@ -208,6 +208,9 @@ var oTable{{ sListIDForVarSuffix }} = $('#{{ oUIBlock.GetId() }}').DataTable({
}
},
{% endfor %}
{% if oUIBlock.HasRowActions() %}
getRowActionsColumnDefinition('{{ oUIBlock.GetId() }}'),
{% endif %}
],
ajax: $.fn.dataTable.pipeline({
url: "{{ oUIBlock.GetAjaxUrl() }}",
@@ -415,4 +418,6 @@ if(window.ResizeObserver){
}, 120);
});
oTable{{ sListIDForVarSuffix }}Resize.observe($('#{{ oUIBlock.GetId() }}')[0]);
}
}
{% include 'base/components/datatable/row-actions/handler.js.twig' %}

View File

@@ -0,0 +1,15 @@
{# @copyright Copyright (C) 2010-2022 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
// for each row action
{% if oUIBlock.HasRowActions() %}
{% for aAction in oUIBlock.GetRowActions() %}
$('#{{ oUIBlock.GetId() }} tbody').on('click', 'button[data-action-id="{{ loop.index0 }}"]', function() {
let iActionId = $(this).data('action-id');
let oDatatable = $('#{{ oUIBlock.GetId() }}').DataTable();
let oTrElement = $(this).closest('tr');
let aData = oDatatable.row(oTrElement).data();
{{ aAction.js_row_action|raw }};
});
{% endfor %}
{% endif %}

View File

@@ -13,8 +13,12 @@
</tr>
</thead>
<tbody>
{% for oSubBlock in oUIBlock.GetRows() %}
{{ render_block(oSubBlock, {aPage: aPage}) }}
{% for oRowBlock in oUIBlock.GetRows() %}
{{ render_block(oRowBlock, {aPage: aPage}) }}
{% endfor %}
</tbody>
</table>
</table>
{% if oUIBlock.HasRowActions() %}
{{ render_block(oUIBlock.GetRowActionsTemplate()) }}
{% endif %}

View File

@@ -20,7 +20,10 @@ var oTable{{ sListIDForVarSuffix }} = $('#{{ oUIBlock.GetId() }}').DataTable({
},
{% endif %}
columnDefs: [
{orderable: false, targets: 0}
{orderable: false, targets: 0},
{% if oUIBlock.HasRowActions() %}
getRowActionsColumnDefinition('{{ oUIBlock.GetId() }}', {{ oUIBlock.GetColumnsCount() - 1 }}),
{% endif %}
],
{% endif %}
drawCallback: function (settings) {
@@ -123,4 +126,6 @@ if (window.ResizeObserver)
{% endif %}
}
}
{% include 'base/components/datatable/row-actions/handler.js.twig' %}

View File

@@ -44,4 +44,8 @@
</tr>
{% endfor %}
</tbody>
</table>
</table>
{% if oUIBlock.HasRowActions() %}
{{ render_block(oUIBlock.GetRowActionsTemplate()) }}
{% endif %}

View File

@@ -54,6 +54,9 @@ var oTable{{ sListIDForVarSuffix }} = $('#{{ oUIBlock.GetId() }}').DataTable({
sortable: true
},
{% endfor %}
{% if oUIBlock.HasRowActions() %}
getRowActionsColumnDefinition('{{ oUIBlock.GetId() }}'),
{% endif %}
],
drawCallback: function (settings) {
if(settings.json)
@@ -103,4 +106,6 @@ if (window.ResizeObserver)
}, 120);
});
oStaticTable{{ sListIDForVarSuffix }}Resize.observe($('#{{ oUIBlock.GetId() }}')[0]);
}
}
{% include 'base/components/datatable/row-actions/handler.js.twig' %}

View File

@@ -0,0 +1,9 @@
{# @copyright Copyright (C) 2010-2022 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% apply spaceless %}
<template id="{{ oUIBlock.GetId() }}" data-role="ibo-template">
{% for oSubBlock in oUIBlock.GetSubBlocks() %}
{{ render_block(oSubBlock, {aPage: aPage}) }}
{% endfor %}
</template>
{% endapply %}

View File

@@ -27,8 +27,10 @@ namespace Combodo\iTop\Test\UnitTest;
*/
use ArchivedObjectException;
use CMDBSource;
use CMDBObject;
use CMDBSource;
use Combodo\iTop\Service\EventData;
use Combodo\iTop\Service\EventService;
use Contact;
use DBObject;
use DBObjectSet;
@@ -70,9 +72,13 @@ define('TAG_ATTCODE', 'domains');
class ItopDataTestCase extends ItopTestCase
{
private $iTestOrgId;
// For cleanup
private $aCreatedObjects = array();
// Counts
public $aReloadCount = [];
const USE_TRANSACTION = true;
const CREATE_TEST_ORG = false;
@@ -96,6 +102,8 @@ class ItopDataTestCase extends ItopTestCase
{
$this->CreateTestOrganization();
}
EventService::RegisterListener(EVENT_SERVICE_DB_OBJECT_RELOAD, [$this, 'CountObjectReload']);
}
/**
@@ -119,12 +127,13 @@ class ItopDataTestCase extends ItopTestCase
$this->debug("Removing $sClass::$iKey");
$oObject->DBDelete();
}
catch (Exception $e)
{
$this->debug($e->getMessage());
catch (Exception $e) {
$this->debug("Error when removing created objects: $sClass::$iKey. Exception message: ".$e->getMessage());
}
}
}
parent::tearDown();
}
/**
@@ -446,9 +455,9 @@ class ItopDataTestCase extends ItopTestCase
$oUserProfile = new URP_UserProfile();
$oUserProfile->Set('profileid', $iProfileId);
$oUserProfile->Set('reason', 'UNIT Tests');
/** @var DBObjectSet $oSet */
/** @var \ormLinkSet $oSet */
$oSet = $oUser->Get('profile_list');
$oSet->AddObject($oUserProfile);
$oSet->AddItem($oUserProfile);
$oUser = $this->updateObject('UserLocal', $oUser->GetKey(), array(
'profile_list' => $oSet,
));
@@ -788,6 +797,49 @@ class ItopDataTestCase extends ItopTestCase
return $oOrg;
}
public function ResetReloadCount()
{
$this->aReloadCount = [];
}
public function DebugReloadCount($sMsg, $bResetCount = true)
{
$iTotalCount = 0;
$aTotalPerClass = [];
foreach ($this->aReloadCount as $sClass => $aCountByKeys) {
$iClassCount = 0;
foreach ($aCountByKeys as $iCount) {
$iClassCount += $iCount;
}
$iTotalCount += $iClassCount;
$aTotalPerClass[$sClass] = $iClassCount;
}
$this->debug("$sMsg - $iTotalCount reload(s)");
foreach ($this->aReloadCount as $sClass => $aCountByKeys) {
$this->debug(" $sClass => $aTotalPerClass[$sClass] reload(s)");
foreach ($aCountByKeys as $sKey => $iCount) {
$this->debug(" $sClass::$sKey => $iCount");
}
}
if ($bResetCount) {
$this->ResetReloadCount();
}
}
public function CountObjectReload(EventData $oData)
{
$oObject = $oData->Get('object');
$sClass = get_class($oObject);
$sKey = $oObject->GetKey();
$iCount = $this->GetObjectReloadCount($sClass, $sKey);
$this->aReloadCount[$sClass][$sKey] = 1 + $iCount;
}
public function GetObjectReloadCount($sClass, $sKey)
{
return $this->aReloadCount[$sClass][$sKey] ?? 0;
}
/**
* Assert that a series of operations will trigger a given number of MySL queries
*
@@ -797,7 +849,7 @@ class ItopDataTestCase extends ItopTestCase
* @throws \MySQLException
* @throws \MySQLQueryHasNoResultException
*/
protected static function assertDBQueryCount($iExpectedCount, callable $oFunction)
protected function assertDBQueryCount($iExpectedCount, callable $oFunction)
{
$iInitialCount = (int) CMDBSource::QueryToScalar("SHOW SESSION STATUS LIKE 'Queries'", 1);
$oFunction();
@@ -805,12 +857,12 @@ class ItopDataTestCase extends ItopTestCase
$iCount = $iFinalCount - 1 - $iInitialCount;
if ($iCount != $iExpectedCount)
{
static::fail("Expected $iExpectedCount queries. $iCount have been executed.");
$this->fail("Expected $iExpectedCount queries. $iCount have been executed.");
}
else
{
// Otherwise PHP Unit will consider that no assertion has been made
static::assertTrue(true);
// Otherwise, PHP Unit will consider that no assertion has been made
$this->assertTrue(true);
}
}
@@ -835,7 +887,7 @@ class ItopDataTestCase extends ItopTestCase
}
/**
* Import a consistent set of iTop objects from the specified XML text string
* Import a consistent set of iTop objects from the specified XML text string
* @param string $sXmlDataset
* @param boolean $bSearch If true, a search will be performed on each object (based on its reconciliation keys)
* before trying to import it (existing objects will be updated)

View File

@@ -25,6 +25,8 @@ namespace Combodo\iTop\Test\UnitTest;
* Time: 11:21
*/
use CMDBSource;
use MySQLTransactionNotClosedException;
use PHPUnit\Framework\TestCase;
use SetupUtils;
@@ -48,15 +50,26 @@ class ItopTestCase extends TestCase
@include_once getcwd().'/approot.inc.php'; // this is when launching phpunit from within the IDE
}
/**
* @throws \MySQLTransactionNotClosedException see N°5538
* @since 2.7.8 3.0.3 3.1.0 N°5538
*/
protected function tearDown(): void
{
parent::tearDown();
if (CMDBSource::IsInsideTransaction()) {
// Nested transactions were opened but not finished !
throw new MySQLTransactionNotClosedException('Some DB transactions were opened but not closed ! Fix the code by adding ROLLBACK or COMMIT statements !', []);
}
}
protected function debug($sMsg)
{
if (DEBUG_UNIT_TEST)
{
if (is_string($sMsg))
{
echo "$sMsg\n";
}
else {
{
if (DEBUG_UNIT_TEST) {
if (is_string($sMsg)) {
echo "$sMsg\n";
} else {
/** @noinspection ForgottenDebugOutputInspection */
print_r($sMsg);
}

View File

@@ -0,0 +1,344 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Core;
use CMDBSource;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use MetaModel;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*
* created a dedicated test for external keys imports.
*
* Class BulkChangeExtKeyTest
*
* @package Combodo\iTop\Test\UnitTest\Core
*/
class BulkChangeExtKeyTest extends ItopDataTestCase {
const CREATE_TEST_ORG = true;
/**
* this test may delete Person objects to cover all usecases
* DO NOT CHANGE USE_TRANSACTION value to avoid any DB loss!
*/
const USE_TRANSACTION = true;
private $sUid;
protected function setUp() : void {
parent::setUp();
require_once(APPROOT.'core/bulkchange.class.inc.php');
}
private function deleteAllRacks(){
$oSearch = \DBSearch::FromOQL("SELECT Rack");
$oSet = new \DBObjectSet($oSearch);
$iCount = $oSet->Count();
if ($iCount != 0){
while ($oRack = $oSet->Fetch()){
$oRack->DBDelete();
}
}
}
/**
* @dataProvider ReconciliationKeyProvider
*/
public function testExternalFieldIssueImportFail_NoObjectAtAll($bIsRackReconKey){
$this->deleteAllRacks();
$this->performBulkChangeTest(
'There are no \'Rack\' objects',
"",
null,
$bIsRackReconKey
);
}
public function createRackObjects($aRackDict) {
foreach ($aRackDict as $iOrgId => $aRackNames) {
foreach ($aRackNames as $sRackName) {
$this->createObject('Rack', ['name' => $sRackName, 'description' => "${sRackName}Desc", 'org_id' => $iOrgId]);
}
}
}
private function createAnotherUserInAnotherOrg() {
$oOrg2 = $this->CreateOrganization('UnitTestOrganization2');
$oProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'Configuration Manager'), true);
$sUid = $this->GetUid();
$oUserProfile = new \URP_UserProfile();
$oUserProfile->Set('profileid', $oProfile->GetKey());
$oUserProfile->Set('reason', 'UNIT Tests');
$oSet = \DBObjectSet::FromObject($oUserProfile);
$oPerson = $this->CreatePerson('666', $oOrg2->GetKey());
$oUser = $this->createObject('UserLocal', array(
'contactid' => $oPerson->GetKey(),
'login' => $sUid,
'password' => "ABCdef$sUid@12345",
'language' => 'EN US',
'profile_list' => $oSet,
));
$oAllowedOrgList = $oUser->Get('allowed_org_list');
/** @var \URP_UserOrg $oUserOrg */
$oUserOrg = \MetaModel::NewObject('URP_UserOrg', ['allowed_org_id' => $oOrg2->GetKey(),]);
$oAllowedOrgList->AddItem($oUserOrg);
$oUser->Set('allowed_org_list', $oAllowedOrgList);
$oUser->DBWrite();
return [$oOrg2, $oUser];
}
public function ReconciliationKeyProvider(){
return [
'rack_id NOT a reconcilication key' => [ false ],
'rack_id reconcilication key' => [ true ],
];
}
/**
* @dataProvider ReconciliationKeyProvider
*/
public function testExternalFieldIssueImportFail_NoObjectVisibleByCurrentUser($bIsRackReconKey){
$this->deleteAllRacks();
$this->createRackObjects(
[
$this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4']
]
);
list($oOrg2, $oUser) = $this->createAnotherUserInAnotherOrg();
\UserRights::Login($oUser->Get('login'));
$this->performBulkChangeTest(
"There are no 'Rack' objects found with your current profile",
"",
$oOrg2,
$bIsRackReconKey
);
}
/**
* @dataProvider ReconciliationKeyProvider
*/
public function testExternalFieldIssueImportFail_SomeObjectVisibleByCurrentUser($bIsRackReconKey){
$this->deleteAllRacks();
list($oOrg2, $oUser) = $this->createAnotherUserInAnotherOrg();
$this->createRackObjects(
[
$this->getTestOrgId() => ['RackTest1', 'RackTest2'],
$oOrg2->GetKey() => ['RackTest3', 'RackTest4'],
]
);
\UserRights::Login($oUser->Get('login'));
$this->performBulkChangeTest(
"There are some 'Rack' objects not visible with your current profile",
"Some possible 'Rack' value(s): RackTest3, RackTest4",
$oOrg2,
$bIsRackReconKey
);
}
/**
* @dataProvider ReconciliationKeyProvider
*/
public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser($bIsRackReconKey){
$this->deleteAllRacks();
$this->createRackObjects(
[
$this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4']
]
);
$this->performBulkChangeTest(
"No match for value 'UnexistingRack'",
"Some possible 'Rack' value(s): RackTest1, RackTest2, RackTest3...",
null,
$bIsRackReconKey
);
}
/**
* @dataProvider ReconciliationKeyProvider
*/
public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser_AmbigousMatch($bIsRackReconKey){
$this->deleteAllRacks();
$this->createRackObjects(
[
$this->getTestOrgId() => ['UnexistingRack', 'UnexistingRack']
]
);
$this->performBulkChangeTest(
"invalid value for attribute",
"Ambiguous: found 2 objects",
null,
$bIsRackReconKey,
null,
null,
null,
'Found 2 matches'
);
}
/**
* @dataProvider ReconciliationKeyProvider
*/
public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser_FurtherExtKeyForRack($bIsRackReconKey){
$this->deleteAllRacks();
$this->createRackObjects(
[
$this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4']
]
);
$aCsvData = [["UnexistingRackDescription"]];
$aExtKeys = ["org_id" => ["name" => 0], "rack_id" => ["name" => 1, "description" => 3]];
$sSearchLinkUrl = 'UI.php?operation=search&filter=%5B%22SELECT+%60Rack%60+FROM+Rack+AS+%60Rack%60+WHERE+%28%28%60Rack%60.%60name%60+%3D+%3Aname%29+AND+%28%60Rack%60.%60description%60+%3D+%3Adescription%29%29%22%2C%7B%22name%22%3A%22UnexistingRack%22%2C%22description%22%3A%22UnexistingRackDescription%22%7D%2C%5B%5D%5D'
;
$this->performBulkChangeTest(
"No match for value 'UnexistingRack UnexistingRackDescription'",
"Some possible 'Rack' value(s): RackTest1 RackTest1Desc, RackTest2 RackTest2Desc, RackTest3 RackTest3Desc...",
null,
$bIsRackReconKey,
$aCsvData,
$aExtKeys,
$sSearchLinkUrl
);
}
private function GetUid(){
if (is_null($this->sUid)){
$this->sUid = date('dmYHis');
}
return $this->sUid;
}
/** *
* @param $aInitData
* @param $aCsvData
* @param $aAttributes
* @param $aExtKeys
* @param $aReconcilKeys
*/
public function performBulkChangeTest($sExpectedDisplayableValue, $sExpectedDescription, $oOrg, $bIsRackReconKey,
$aAdditionalCsvData=null, $aExtKeys=null, $sSearchLinkUrl=null, $sError="Object not found") {
if ($sSearchLinkUrl===null){
$sSearchLinkUrl = 'UI.php?operation=search&filter=%5B%22SELECT+%60Rack%60+FROM+Rack+AS+%60Rack%60+WHERE+%28%60Rack%60.%60name%60+%3D+%3Aname%29%22%2C%7B%22name%22%3A%22UnexistingRack%22%7D%2C%5B%5D%5D';
}
if (is_null($oOrg)){
$iOrgId = $this->getTestOrgId();
$sOrgName = "UnitTestOrganization";
}else{
$iOrgId = $oOrg->GetKey();
$sOrgName = $oOrg->Get('name');
}
$sUid = $this->GetUid();
$aCsvData = [[$sOrgName, "UnexistingRack", "$sUid"]];
if ($aAdditionalCsvData !== null){
foreach ($aAdditionalCsvData as $i => $aData){
foreach ($aData as $sData){
$aCsvData[$i][] = $sData;
}
}
}
$aAttributes = ["name" => 2];
if ($aExtKeys == null){
$aExtKeys = ["org_id" => ["name" => 0], "rack_id" => ["name" => 1]];
}
$aReconcilKeys = [ "name" ];
$aResult = [
0 => $sOrgName,
"org_id" => $iOrgId,
1 => "UnexistingRack",
2 => "\"$sUid\"",
"rack_id" => [
$sExpectedDisplayableValue,
$sExpectedDescription
],
"__STATUS__" => "Issue: Unexpected attribute value(s)",
"__ERRORS__" => $sError,
];
if ($bIsRackReconKey){
$aReconcilKeys[] = "rack_id";
$aResult[2] = $sUid;
$aResult["__STATUS__"] = "Issue: failed to reconcile";
}
CMDBSource::Query('START TRANSACTION');
//change value during the test
$db_core_transactions_enabled=MetaModel::GetConfig()->Get('db_core_transactions_enabled');
MetaModel::GetConfig()->Set('db_core_transactions_enabled',false);
$this->debug("aCsvData:".json_encode($aCsvData[0]));
$this->debug("aReconcilKeys:". var_export($aReconcilKeys));
$oBulk = new \BulkChange(
"Server",
$aCsvData,
$aAttributes,
$aExtKeys,
$aReconcilKeys,
null,
null,
"Y-m-d H:i:s", // date format
true // localize
);
$this->debug("BulkChange:");
$oChange = \CMDBObject::GetCurrentChange();
$this->debug("GetCurrentChange:");
$aRes = $oBulk->Process($oChange);
$this->debug("Process:");
static::assertNotNull($aRes);
$this->debug("assertNotNull:");
var_dump($aRes);
foreach ($aRes as $aRow) {
if (array_key_exists('__STATUS__', $aRow)) {
$sStatus = $aRow['__STATUS__'];
$this->debug("sStatus:".$sStatus->GetDescription());
$this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription());
foreach ($aRow as $i => $oCell) {
if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") {
$this->debug("i:".$i);
$this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue());
if (array_key_exists($i,$aResult)) {
$this->debug("aResult:".var_export($aResult[$i]));
if ($oCell instanceof \CellStatus_SearchIssue ||
$oCell instanceof \CellStatus_Ambiguous) {
$this->assertEquals($aResult[$i][0], $oCell->GetDisplayableValue(),
"failure on ".get_class($oCell).' cell type');
$this->assertEquals($sSearchLinkUrl, $oCell->GetSearchLinkUrl(),
"failure on ".get_class($oCell).' cell type');
$this->assertEquals($aResult[$i][1], $oCell->GetDescription(),
"failure on ".get_class($oCell).' cell type');
}
}
} else if ($i === "__ERRORS__") {
$sErrors = array_key_exists("__ERRORS__", $aResult) ? $aResult["__ERRORS__"] : "";
$this->assertEquals( $sErrors, $oCell->GetDescription());
}
}
$this->assertEquals( $aResult[0], $aRow[0]->GetDisplayableValue());
}
}
MetaModel::GetConfig()->Set('db_core_transactions_enabled',$db_core_transactions_enabled);
}
}

View File

@@ -104,13 +104,13 @@ class BulkChangeTest extends ItopDataTestCase {
if (array_key_exists('__STATUS__', $aRow)) {
$sStatus = $aRow['__STATUS__'];
//$this->debug("sStatus:".$sStatus->GetDescription());
$this->assertEquals($sStatus->GetDescription(), $aResult["__STATUS__"]);
$this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription());
foreach ($aRow as $i => $oCell) {
if ($i != "finalclass" && $i != "__STATUS__") {
if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") {
$this->debug("i:".$i);
$this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue());
$this->debug("aResult:".$aResult[$i]);
$this->assertEquals($oCell->GetDisplayableValue(), $aResult[$i]);
$this->assertEquals($aResult[$i], $oCell->GetDisplayableValue());
}
}
}
@@ -131,28 +131,37 @@ class BulkChangeTest extends ItopDataTestCase {
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
["org_id" => ["name" => 0]],
["id"],
[ 0 => "Demo", "org_id" => "n/a", 1 => "Server1", 2 => "1", 3 => "production", 4 => "date", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
[ 0 => "Demo", "org_id" => "n/a", 1 => "Server1", 2 => "1", 3 => "production", 4 => "'date' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
],
"Case 1 : no match" => [
[["Bad", "Server1", "1", "production", ""]],
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
["org_id" => ["name" => 0]],
["id"],
["org_id" => "",1 => "Server1",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
["org_id" => "No match for value 'Bad'",1 => "Server1",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
],
"Case 10 : Missing mandatory value" => [
[["", "Server1", "1", "production", ""]],
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
["org_id" => ["name" => 0]],
["id"],
[ "org_id" => "", 1 => "Server1", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
[ "org_id" => "invalid value for attribute", 1 => "Server1", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
],
"Case 6 : Unexpected value" => [
[["Demo", "Server1", "1", "<svg onclick\"alert(1)\">", ""]],
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
["org_id" => ["name" => 0]],
["id"],
[0 => "Demo", "org_id" => "3", 1 => "Server1", 2 => "1", 3 => "&lt;svg onclick&quot;alert(1)&quot;&gt;", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
[
0 => "Demo",
"org_id" => "3",
1 => "Server1",
2 => "1",
3 => "'&lt;svg onclick&quot;alert(1)&quot;&gt;' is an invalid value",
4 => "",
"id" => 1,
"__STATUS__" => "Issue: Unexpected attribute value(s)",
"__ERRORS__" => "Unexpected value for attribute 'status': no match found, check spelling"],
],
];
}
@@ -172,17 +181,20 @@ class BulkChangeTest extends ItopDataTestCase {
//change value during the test
$db_core_transactions_enabled=MetaModel::GetConfig()->Get('db_core_transactions_enabled');
MetaModel::GetConfig()->Set('db_core_transactions_enabled',false);
/** @var Server $oServer */
$oServer = $this->createObject('Server', array(
'name' => $aInitData[1],
'status' => $aInitData[2],
'org_id' => $aInitData[0],
'purchase_date' => $aInitData[3],
));
$aCsvData[0][2]=$oServer->GetKey();
$aResult[2]=$oServer->GetKey();
$aResult["id"]=$oServer->GetKey();
$this->debug("oServer->GetKey():".$oServer->GetKey());
if (is_array($aInitData) && sizeof($aInitData) != 0) {
/** @var Server $oServer */
$oServer = $this->createObject('Server', array(
'name' => $aInitData[1],
'status' => $aInitData[2],
'org_id' => $aInitData[0],
'purchase_date' => $aInitData[3],
));
$aCsvData[0][2]=$oServer->GetKey();
$aResult[2]=$oServer->GetKey();
$aResult["id"]=$oServer->GetKey();
$this->debug("oServer->GetKey():".$oServer->GetKey());
}
$this->debug("aCsvData:".json_encode($aCsvData[0]));
$this->debug("aReconcilKeys:".$aReconcilKeys[0]);
$oBulk = new \BulkChange(
@@ -207,13 +219,16 @@ class BulkChangeTest extends ItopDataTestCase {
if (array_key_exists('__STATUS__', $aRow)) {
$sStatus = $aRow['__STATUS__'];
$this->debug("sStatus:".$sStatus->GetDescription());
$this->assertEquals($sStatus->GetDescription(), $aResult["__STATUS__"]);
$this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription());
foreach ($aRow as $i => $oCell) {
if ($i != "finalclass" && $i != "__STATUS__") {
if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") {
$this->debug("i:".$i);
$this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue());
$this->debug("aResult:".$aResult[$i]);
$this->assertEquals( $aResult[$i], $oCell->GetDisplayableValue());
$this->assertEquals( $aResult[$i], $oCell->GetDisplayableValue(), "failure on " . get_class($oCell) . ' cell type');
} else if ($i === "__ERRORS__") {
$sErrors = array_key_exists("__ERRORS__", $aResult) ? $aResult["__ERRORS__"] : "";
$this->assertEquals( $sErrors, $oCell->GetDescription());
}
}
$this->assertEquals( $aResult[0], $aRow[0]->GetDisplayableValue());
@@ -225,21 +240,58 @@ class BulkChangeTest extends ItopDataTestCase {
public function CSVImportProvider() {
return [
"Case 6 - 1 : Unexpected value" => [
"Case 6 - 1 : Unexpected value (update)" => [
["1", "ServerTest", "production", ""],
[["Demo", "ServerTest", "key", "BadValue", ""]],
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
["org_id" => ["name" => 0]],
["id"],
[0 => "Demo", "org_id" => "3", 1 => "ServerTest", 2 => "1", 3 => "BadValue", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
[
0 => "Demo",
"org_id" => "3",
1 => "ServerTest",
2 => "1",
3 => "'BadValue' is an invalid value",
4 => "",
"id" => 1,
"__STATUS__" => "Issue: Unexpected attribute value(s)",
"__ERRORS__" => "Allowed 'status' value(s): implementation,obsolete,production,stock",
],
],
"Case 6 - 2 : Unexpected value" => [
"Case 6 - 2 : Unexpected value (update)" => [
["1", "ServerTest", "production", ""],
[["Demo", "ServerTest", "key", "<svg onclick\"alert(1)\">", ""]],
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
["org_id" => ["name" => 0]],
["id"],
[0 => "Demo", "org_id" => "3", 1 => "ServerTest", 2 => "1", 3 => "&lt;svg onclick&quot;alert(1)&quot;&gt;", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
[
0 => "Demo",
"org_id" => "3",
1 => "ServerTest",
2 => "1",
3 => "'&lt;svg onclick&quot;alert(1)&quot;&gt;' is an invalid value",
4 => "",
"id" => 1,
"__STATUS__" => "Issue: Unexpected attribute value(s)",
"__ERRORS__" => "Allowed 'status' value(s): implementation,obsolete,production,stock",
],
],
"Case 6 - 3 : Unexpected value (creation)" => [
[],
[["Demo", "ServerTest", "<svg onclick\"alert(1)\">", ""]],
["name" => 1, "status" => 2, "purchase_date" => 3],
["org_id" => ["name" => 0]],
["name"],
[
0 => "Demo",
"org_id" => "3",
1 => "\"ServerTest\"",
2 => "'&lt;svg onclick&quot;alert(1)&quot;&gt;' is an invalid value",
3 => "",
"id" => 1,
"__STATUS__" => "Issue: Unexpected attribute value(s)",
"__ERRORS__" => "Allowed 'status' value(s): implementation,obsolete,production,stock",
],
],
"Case 8 : unchanged name" => [
["1", "<svg onclick\"alert(1)\">", "production", ""],
@@ -263,7 +315,7 @@ class BulkChangeTest extends ItopDataTestCase {
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
["org_id" => ["name" => 0]],
["id"],
[ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "date", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
[ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "'date' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
],
"Case 9 - 2: wrong date format" => [
["1", "ServerTest", "production", ""],
@@ -271,7 +323,7 @@ class BulkChangeTest extends ItopDataTestCase {
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
["org_id" => ["name" => 0]],
["id"],
[ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "&lt;svg onclick&quot;alert(1)&quot;&gt;", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
[ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "'&lt;svg onclick&quot;alert(1)&quot;&gt;' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
],
"Case 1 - 1 : no match" => [
["1", "ServerTest", "production", ""],
@@ -279,7 +331,9 @@ class BulkChangeTest extends ItopDataTestCase {
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
["org_id" => ["name" => 0]],
["id"],
[ 0 => "Bad", "org_id" => "",1 => "ServerTest",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
[ 0 => "Bad", "org_id" => "No match for value 'Bad'",1 => "ServerTest",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)",
"__ERRORS__" => "Object not found",
],
],
"Case 1 - 2 : no match" => [
["1", "ServerTest", "production", ""],
@@ -287,7 +341,9 @@ class BulkChangeTest extends ItopDataTestCase {
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
["org_id" => ["name" => 0]],
["id"],
[ 0 => "&lt;svg fonclick&quot;alert(1)&quot;&gt;", "org_id" => "",1 => "ServerTest",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
[ 0 => "&lt;svg fonclick&quot;alert(1)&quot;&gt;", "org_id" => "No match for value '<svg fonclick\"alert(1)\">'",1 => "ServerTest",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)",
"__ERRORS__" => "Object not found",
],
],
"Case 10 : Missing mandatory value" => [
["1", "ServerTest", "production", ""],
@@ -295,7 +351,9 @@ class BulkChangeTest extends ItopDataTestCase {
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
["org_id" => ["name" => 0]],
["id"],
[ 0 => "", "org_id" => "", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
[ 0 => "", "org_id" => "invalid value for attribute", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)",
"__ERRORS__" => "Null not allowed",
],
],
"Case 0 : Date format" => [
@@ -304,7 +362,7 @@ class BulkChangeTest extends ItopDataTestCase {
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
["org_id" => ["name" => 0]],
["id"],
[ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "2020-20-03", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
[ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "'2020-20-03' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
],
];
}
@@ -326,20 +384,22 @@ class BulkChangeTest extends ItopDataTestCase {
//change value during the test
$db_core_transactions_enabled=MetaModel::GetConfig()->Get('db_core_transactions_enabled');
MetaModel::GetConfig()->Set('db_core_transactions_enabled',false);
/** @var Server $oServer */
$oOrganisation = $this->createObject('Organization', array(
'name' =>$aInitData[0]
));
$aResult["org_id"]=$oOrganisation->GetKey();
$oServer = $this->createObject('Server', array(
'name' => $aInitData[1],
'status' => $aInitData[2],
'org_id' => $oOrganisation->GetKey(),
'purchase_date' => $aInitData[3],
));
$aCsvData[0][2]=$oServer->GetKey();
$aResult[2]=$oServer->GetKey();
$aResult["id"]=$oServer->GetKey();
if (is_array($aInitData) && sizeof($aInitData) != 0) {
/** @var Server $oServer */
$oOrganisation = $this->createObject('Organization', array(
'name' => $aInitData[0]
));
$aResult["org_id"] = $oOrganisation->GetKey();
$oServer = $this->createObject('Server', array(
'name' => $aInitData[1],
'status' => $aInitData[2],
'org_id' => $oOrganisation->GetKey(),
'purchase_date' => $aInitData[3],
));
$aCsvData[0][2]=$oServer->GetKey();
$aResult[2]=$oServer->GetKey();
$aResult["id"]=$oServer->GetKey();
}
$oBulk = new \BulkChange(
"Server",
$aCsvData,
@@ -356,15 +416,17 @@ class BulkChangeTest extends ItopDataTestCase {
static::assertNotNull($aRes);
foreach ($aRes as $aRow) {
foreach ($aRow as $i => $oCell) {
if ($i != "finalclass" && $i != "__STATUS__") {
if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") {
$this->debug("i:".$i);
$this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue());
$this->debug("aResult:".$aResult[$i]);
$this->assertEquals($aResult[$i], $oCell->GetDisplayableValue());
}
elseif ($i == "__STATUS__") {
} elseif ($i == "__STATUS__") {
$sStatus = $aRow['__STATUS__'];
$this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription());
} else if ($i === "__ERRORS__") {
$sErrors = array_key_exists("__ERRORS__", $aResult) ? $aResult["__ERRORS__"] : "";
$this->assertEquals( $sErrors, $oCell->GetDescription());
}
}
$this->assertEquals($aResult[0], $aRow[0]->GetDisplayableValue());
@@ -402,4 +464,4 @@ class BulkChangeTest extends ItopDataTestCase {
];
}
}
}

View File

@@ -11,6 +11,7 @@ use Combodo\iTop\Core\DbConnectionWrapper;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use Exception;
use MetaModel;
use MySQLTransactionNotClosedException;
/**
* @runTestsInSeparateProcesses
@@ -231,22 +232,56 @@ class TransactionsTest extends ItopTestCase
public function DBUpdateProvider()
{
return [
"Normal case" => ['iFailAt' => -1, 'bIsModified' => false],
"ticket_request" => ['iFailAt' => 1, 'bIsModified' => true],
"Normal case" => ['iFailAt' => -1, 'bIsModified' => false],
"ticket_request" => ['iFailAt' => 1, 'bIsModified' => true],
"lnkcontacttoticket" => ['iFailAt' => 2, 'bIsModified' => true],
"History 1" => ['iFailAt' => 3, 'bIsModified' => true],
"History 2" => ['iFailAt' => 4, 'bIsModified' => true],
"History 3" => ['iFailAt' => 5, 'bIsModified' => true],
"History 4" => ['iFailAt' => 6, 'bIsModified' => true],
"History 5" => ['iFailAt' => 7, 'bIsModified' => true],
"History 6" => ['iFailAt' => 8, 'bIsModified' => true],
"History 7" => ['iFailAt' => 9, 'bIsModified' => true],
"History 8" => ['iFailAt' => 10, 'bIsModified' => true],
"History 9" => ['iFailAt' => 11, 'bIsModified' => true],
"History 10" => ['iFailAt' => 12, 'bIsModified' => true],
"History 11" => ['iFailAt' => 13, 'bIsModified' => true],
"History 12" => ['iFailAt' => 14, 'bIsModified' => true],
"History 13" => ['iFailAt' => 15, 'bIsModified' => true],
"History 1" => ['iFailAt' => 3, 'bIsModified' => true],
"History 2" => ['iFailAt' => 4, 'bIsModified' => true],
"History 3" => ['iFailAt' => 5, 'bIsModified' => true],
"History 4" => ['iFailAt' => 6, 'bIsModified' => true],
"History 5" => ['iFailAt' => 7, 'bIsModified' => true],
"History 6" => ['iFailAt' => 8, 'bIsModified' => true],
"History 7" => ['iFailAt' => 9, 'bIsModified' => true],
"History 8" => ['iFailAt' => 10, 'bIsModified' => true],
"History 9" => ['iFailAt' => 11, 'bIsModified' => true],
"History 10" => ['iFailAt' => 12, 'bIsModified' => true],
"History 11" => ['iFailAt' => 13, 'bIsModified' => true],
"History 12" => ['iFailAt' => 14, 'bIsModified' => true],
"History 13" => ['iFailAt' => 15, 'bIsModified' => true],
];
}
}
/**
* @return void
* @doesNotPerformAssertions
*/
public function testTransactionOpenedThenClosed()
{
CMDBSource::Query('START TRANSACTION;');
CMDBSource::Query('COMMIT;');
}
/**
* This will throw an exception in the tearDown method.
* This cannot be detected nor by `@expectedException` nor `expectException` method, so we have a specific tearDown impl
*
* @return void
* @doesNotPerformAssertions
*/
public function testTransactionOpenedNotClosed()
{
CMDBSource::Query('START TRANSACTION;');
}
protected function tearDown(): void
{
try {
parent::tearDown();
}
catch (MySQLTransactionNotClosedException $e) {
if ($this->getName() === 'testTransactionOpenedNotClosed') {
$this->debug('Executing the testTransactionOpenNoClose method throws a '.MySQLTransactionNotClosedException::class.' exception in tearDown');
}
}
}
}

View File

@@ -0,0 +1,219 @@
<?php
/*
* @copyright Copyright (C) 2010-2022 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Core\CRUD;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use lnkContactToFunctionalCI;
use MetaModel;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class DBObjectTest extends ItopDataTestCase
{
const USE_TRANSACTION = true;
const CREATE_TEST_ORG = true;
public function testReloadNotNecessaryForInsert()
{
$oPerson = $this->CreatePersonInstance();
// Insert without Reload
$oPerson->DBInsert();
$aValues1 = [];
foreach (MetaModel::GetAttributesList('Person') as $sAttCode) {
if (MetaModel::GetAttributeDef('Person', $sAttCode) instanceof \AttributeLinkedSet) {
continue;
}
$aValues1[$sAttCode] = $oPerson->Get($sAttCode);
}
$sOrgName1 = $oPerson->Get('org_name');
/** @var \ormLinkSet $oCIList1 */
$oCIList1 = $oPerson->Get('cis_list');
$oTeamList1 = $oPerson->Get('team_list');
$sPerson1 = print_r($oPerson, true);
// 1st Reload
$oPerson->Reload(true);
$sPerson2 = print_r($oPerson, true);
$this->assertNotEquals($sPerson1, $sPerson2);
$aValues2 = [];
foreach (MetaModel::GetAttributesList('Person') as $sAttCode) {
if (MetaModel::GetAttributeDef('Person', $sAttCode) instanceof \AttributeLinkedSet) {
continue;
}
$aValues2[$sAttCode] = $oPerson->Get($sAttCode);
}
$sOrgName2 = $oPerson->Get('org_name');
/** @var \ormLinkSet $oCIList2 */
$oCIList2 = $oPerson->Get('cis_list');
$oTeamList2 = $oPerson->Get('team_list');
$this->assertEquals($sOrgName1, $sOrgName2);
$this->assertTrue($oCIList1->Equals($oCIList2));
$this->assertTrue($oTeamList1->Equals($oTeamList2));
$this->assertEquals($aValues1, $aValues2);
// 2nd Reload
$oPerson->Reload(true);
$sPerson3 = print_r($oPerson, true);
$this->assertEquals($sPerson2, $sPerson3);
}
public function testFriendlynameResetOnExtKeyReset()
{
$oPerson = $this->CreatePersonInstance();
$oManager = $this->CreatePersonInstance();
$oManager->DBInsert();
$oPerson->Set('manager_id', $oManager->GetKey());
$this->assertNotEmpty($oPerson->Get('manager_id_friendlyname'));
$oPerson->Set('manager_id', 0);
$this->assertEmpty($oPerson->Get('manager_id_friendlyname'));
}
public function testReloadNotNecessaryForUpdate()
{
$oPerson = $this->CreatePersonInstance();
$oPerson->DBInsert();
$oManager = $this->CreatePersonInstance();
$oManager->DBInsert();
$oPerson->Set('manager_id', $oManager->GetKey());
$oPerson->DBUpdate();
$sManagerFriendlyname1 = $oPerson->Get('manager_id_friendlyname');
$oCIList1 = $oPerson->Get('cis_list');
$oTeamList1 = $oPerson->Get('team_list');
$aValues1 = [];
foreach (MetaModel::GetAttributesList('Person') as $sAttCode) {
if (MetaModel::GetAttributeDef('Person', $sAttCode) instanceof \AttributeLinkedSet) {
continue;
}
$aValues1[$sAttCode] = $oPerson->Get($sAttCode);
}
$sPerson1 = print_r($oPerson, true);
// 1st Reload
$oPerson->Reload(true);
$sPerson2 = print_r($oPerson, true);
$this->assertNotEquals($sPerson1, $sPerson2);
$sManagerFriendlyname2 = $oPerson->Get('manager_id_friendlyname');
$oCIList2 = $oPerson->Get('cis_list');
$oTeamList2 = $oPerson->Get('team_list');
$aValues2 = [];
foreach (MetaModel::GetAttributesList('Person') as $sAttCode) {
if (MetaModel::GetAttributeDef('Person', $sAttCode) instanceof \AttributeLinkedSet) {
continue;
}
$aValues2[$sAttCode] = $oPerson->Get($sAttCode);
}
$this->assertEquals($sManagerFriendlyname1, $sManagerFriendlyname2);
$this->assertTrue($oCIList1->Equals($oCIList2));
$this->assertTrue($oTeamList1->Equals($oTeamList2));
$this->assertEquals($aValues1, $aValues2);
// 2nd Reload
$oPerson->Reload(true);
$sPerson3 = print_r($oPerson, true);
$this->assertEquals($sPerson2, $sPerson3);
}
public function testGetObjectUpdateUnderReentryProtection()
{
$oPerson = $this->CreatePersonInstance();
$oPerson->DBInsert();
$oPerson->Set('email', 'test@combodo.com');
$oPerson->DBUpdate();
$this->assertFalse($oPerson->IsModified());
$oNewPerson = MetaModel::GetObject('Person', $oPerson->GetKey());
$this->assertNotEquals($oPerson->GetObjectUniqId(), $oNewPerson->GetObjectUniqId());
MetaModel::StartReentranceProtection(Metamodel::REENTRANCE_TYPE_UPDATE, $oPerson);
$oPerson->Set('email', 'test1@combodo.com');
$oPerson->DBUpdate();
$this->assertTrue($oPerson->IsModified());
$oNewPerson = MetaModel::GetObject('Person', $oPerson->GetKey());
$this->assertEquals($oPerson->GetObjectUniqId(), $oNewPerson->GetObjectUniqId());
MetaModel::StopReentranceProtection(Metamodel::REENTRANCE_TYPE_UPDATE, $oPerson);
}
public function testObjectIsReadOnly()
{
$oPerson = $this->CreatePersonInstance();
$sMessage = 'Not allowed to write to this object !';
$oPerson->SetReadOnly($sMessage);
try {
$oPerson->Set('email', 'test1@combodo.com');
$this->assertTrue(false, 'Set() should have raised a CoreException');
}
catch (\CoreException $e) {
$this->assertEquals($sMessage, $e->getMessage());
}
$oPerson->SetReadWrite();
$oPerson->Set('email', 'test1@combodo.com');
}
/**
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
*/
private function CreatePersonInstance()
{
$oServer1 = $this->CreateServer(1);
$oServer2 = $this->CreateServer(2);
$sClass = 'Person';
$aParams = [
'name' => 'Person_'.rand(10000, 99999),
'first_name' => 'Test',
'org_id' => $this->getTestOrgId(),
];
$oPerson = MetaModel::NewObject($sClass);
foreach ($aParams as $sAttCode => $oValue) {
$oPerson->Set($sAttCode, $oValue);
}
$oNewLink1 = new lnkContactToFunctionalCI();
$oNewLink1->Set('functionalci_id', $oServer1->GetKey());
$oNewLink2 = new lnkContactToFunctionalCI();
$oNewLink2->Set('functionalci_id', $oServer2->GetKey());
$oCIs = $oPerson->Get('cis_list');
$oCIs->AddItem($oNewLink1);
$oCIs->AddItem($oNewLink2);
$oPerson->Set('cis_list', $oCIs);
return $oPerson;
}
}

Some files were not shown because too many files have changed in this diff Show More