Compare commits

...

111 Commits

Author SHA1 Message Date
Eric Espie
cc4af0a027 N°6667 - Ignore trigger on state entering with auto-dispatch 2023-08-22 14:30:18 +02:00
Eric Espie
3366bae0ab N°6061 - Add tests on Expression evaluation 2023-08-18 15:34:06 +02:00
Romain Quetiez
03b484c349 Tests: fix test not working on MariaDB (unexpected coma tolerated by MySQL) 2023-08-18 12:13:11 +02:00
Molkobain
70081ecf33 N°6436 - Add unit test for API introduced in 3.1 (\iFieldRendererMappingsExtension) 2023-08-18 10:31:05 +02:00
Molkobain
575ba1cd7b Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts:
#	core/metamodel.class.php
2023-08-18 10:24:50 +02:00
Molkobain
d130959692 Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	core/metamodel.class.php
2023-08-18 10:14:51 +02:00
Molkobain
a8c689c6c0 N°6436 - Add unit test to ensure that we don't lose an API during merge between branches 2023-08-18 09:55:45 +02:00
Molkobain
1990ccb5d8 N°6436 - Move interfaces enumeration from 1 line to 1 line / interface (and re-ordered them) for easier merges in newer branches 2023-08-18 09:52:55 +02:00
Molkobain
e107be56e4 N°6097 - Tests: Fix missing hook entry in PHPUnit XML file that led to compiled environment being re-build for each test case 2023-08-18 09:51:15 +02:00
Romain Quetiez
0f8e87e001 Tests: allow execution of RouterTest alone, fix tool to execute each test class separately 2023-08-18 08:44:54 +02:00
Molkobain
d92d2b5e9e Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts:
#	core/metamodel.class.php
2023-08-17 21:36:19 +02:00
Romain Quetiez
ebd0136773 Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts:
#	tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php
2023-08-17 18:36:34 +02:00
Molkobain
f6653e1594 N°6436 - Restore 3.0 APIs lost during 6433678d merge 2023-08-17 17:47:46 +02:00
Romain Quetiez
65bb76b9e3 N°6658 - Boost PHPUnit tests execution 2023-08-17 17:27:55 +02:00
Molkobain
f238593966 Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts:
#	tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php
#	tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php
2023-08-11 09:19:49 +02:00
Molkobain
d951d3b872 💚 Fix typo in extended class name 2023-08-11 09:05:30 +02:00
Molkobain
ccceb870e3 Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php
#	tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php
2023-08-10 15:53:05 +02:00
Molkobain
ed6df77cbb N°6097 - Tests: Optimize performances by creating custom env. only once and re-using it across test classes 2023-08-10 15:45:39 +02:00
Molkobain
1ad28312ec N°6097 - Tests: Introduce autoloader for "utility" classes and move them to a sub-folder for better organization as folder was still messy
Note that unittestautoload.php is now useless. We just keep for now until everything is migrated (projects / branches / modules)
2023-08-10 15:45:39 +02:00
Molkobain
f002aa04cd N°6097 - Tests: Enable PHP unit tests on a custom DataModel 2023-08-10 15:45:39 +02:00
Molkobain
b86d70623e N°6097 - Tests: Temporarily add test case for the new ItopCustomDatamodelTestCase class 2023-08-10 15:45:39 +02:00
Molkobain
fe3467309d N°6097 - Tests: Refactor base test classes for better extensibility 2023-08-10 15:45:39 +02:00
Molkobain
851ab9c356 N°6097 - Add \utils::GetDataPath() method to avoid duplicating manual path build 2023-08-10 15:45:39 +02:00
Molkobain
aef3c2e609 N°6097 - Fix \CMDBSource::DropDB() not resetting cache like \CMDBSource::DropTable() which can lead to errors when trying to re-create it afterwards 2023-08-10 15:45:39 +02:00
Pierre Goiffon
5212e15cc4 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-08-10 14:41:05 +02:00
Pierre Goiffon
f04fc546b5 N°6643 Fix TypeError in \CMDBSource::LogDeadLock 2023-08-10 14:34:09 +02:00
Lars Kaltefleiter
caf3076b12 N°3441 - Portal: Fix failure to open an object containing a link to an archived object (#523)
* N°3441 - Portal : cannot open an object containing a link to an archived object

* N°3441 - Display fa-archive icon in portal

* Update sources/Renderer/Bootstrap/FieldRenderer/BsSelectObjectFieldRenderer.php

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

* Update sources/Renderer/Bootstrap/FieldRenderer/BsSelectObjectFieldRenderer.php

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

* Update sources/Renderer/Bootstrap/FieldRenderer/BsSelectObjectFieldRenderer.php

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

---------

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2023-08-10 09:52:22 +02:00
Pierre Goiffon
c4c400d852 N°6638 💡 More explanations on CompiledDictionariesConsistencyTest::testImportCsvMessageStillOk 2023-08-09 14:54:19 +02:00
Pierre Goiffon
6cc4cc4fb6 📝 Version history : add 3.1.0-2 2023-08-09 10:20:27 +02:00
Pierre Goiffon
d7495af207 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-08-08 15:42:39 +02:00
Pierre Goiffon
13ad98b9b3 Add other integration tests in the beforeSetup group
All of those tests can be ran without a running iTop instance, and are blocking
2023-08-08 15:34:27 +02:00
Pierre Goiffon
4be54fdd65 Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-08-08 15:33:36 +02:00
Pierre Goiffon
6d13397ba1 Add other integration tests in the beforeSetup group
All of those tests can be ran without a running iTop instance, and are blocking
2023-08-08 15:33:09 +02:00
Pierre Goiffon
48e7e0309a N°6638 Fix DictionariesConsistencyTest::testImportCsvMessageStillOk not run on Jenkins
Was contained in a class with a beforeSetup group annotation, whereas it tries to read files in env-production (!)
Plus the dataprovider was using APPROOT const + utils class, which aren't available by default :(
=> Fixed by moving in a dedicated class (CompiledDictionariesConsistencyTest) and removing the dataprovider
2023-08-08 15:30:01 +02:00
Pierre Goiffon
2ce9b2afaf Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-08-04 14:58:38 +02:00
Pierre Goiffon
d64a91d4ce Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	core/metamodel.class.php
2023-08-04 14:58:22 +02:00
Pierre Goiffon
c0c8a13864 💡 \MetaModel::GetObject : remove documented throw Exception 2023-08-04 14:55:38 +02:00
Pierre Goiffon
5ffa41bc16 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-08-03 11:09:14 +02:00
Pierre Goiffon
d2eef06276 AttributeURLTest : remove useless separateProcess annotations 2023-08-03 11:08:47 +02:00
Pierre Goiffon
77b14c516e Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-08-03 09:41:22 +02:00
Pierre Goiffon
880a824f2f N°6562 Replace new DOMText() by \DOMDocument::createTextNode
Because init using constructor outputs a read only node, see https://www.php.net/manual/en/domelement.construct.php
Thanks @Hipska
See conversation in 734a788
2023-08-03 09:40:39 +02:00
Molkobain
f7f1b5f399 Merge remote-tracking branch 'origin/support/3.1.0' into support/3.1 2023-08-02 15:27:17 +02:00
Molkobain
85f66f5e0c N°6618 - Router: Add protection against invalid routes cache 2023-08-02 11:44:20 +02:00
Molkobain
a5c980113b N°6618 - Router: Fix available routes cache being re-generated at each call 2023-08-02 11:44:20 +02:00
Pierre Goiffon
18efbfa803 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-08-02 10:39:51 +02:00
Pierre Goiffon
7aa478d6ff N°6562 💡 Fix comment
Thanks @Molkobain !
2023-08-02 10:35:30 +02:00
Pierre Goiffon
97700dbf15 N°6562 Re-enable failing tests
Conditional disabling was made in ea8e7c5
2023-08-01 14:27:57 +02:00
Pierre Goiffon
c25c69d746 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-08-01 14:27:41 +02:00
Pierre Goiffon
734a788340 N°6562 Fix DOMNode->textContent write
This attribute is read only
Causes layout issues on PHP 8.1.21 and 8.2.8
2023-08-01 14:22:56 +02:00
Eric Espie
eb1eb15791 N°6061 - Allow services to implement interfaces
N°6061 - allow local path from an arbitrary path

(cherry picked from commit 19e7fc9cb9)
(cherry picked from commit fb23bddeb2)
(cherry picked from commit 750ecd4804)
2023-07-28 10:46:06 +02:00
Pierre Goiffon
a84077782d N°4354 N°6587 Remove duplicated ItopDataTestCase::CreateContactlessUser method
Regression introduced by 26048150
2023-07-27 16:48:49 +02:00
Pierre Goiffon
26048150d3 Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts:
#	tests/php-unit-tests/ItopDataTestCase.php
2023-07-27 16:44:02 +02:00
Pierre Goiffon
e5b6e2eb8c N°4354 N°6587 Add test to cover $oUser->Get('profile_list') VS security.hide_administrators config param 2023-07-27 16:42:56 +02:00
Pierre Goiffon
87b6ea4def 📝 Version history : add missing 3.1.0-beta 2023-07-27 14:55:55 +02:00
purplegrape
72873a3343 🌐 Improve zh-cn dict
Manual merge for #516
Translation by @purplegrape, many thanks !
2023-07-27 11:17:09 +02:00
Pierre Goiffon
5ef25ccb77 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-07-26 12:07:50 +02:00
Pierre Goiffon
1682a85cc0 Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-07-26 12:07:35 +02:00
Pierre Goiffon
cd9beec313 Merge remote-tracking branch 'origin/support/2.6' into support/2.7 2023-07-26 12:07:09 +02:00
Pierre Goiffon
8295eaed90 Merge remote-tracking branch 'origin/support/2.5' into support/2.6 2023-07-26 12:06:32 +02:00
Eric Espie
86a7cefa68 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-07-25 17:56:12 +02:00
Eric Espie
829b648dd2 Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-07-25 17:55:45 +02:00
Eric Espie
5475b9fbbe N°3454 - MoveToProd in 2 steps - fix utils::GetCurrentModuleName() 2023-07-25 17:44:43 +02:00
Eric Espie
6f8e7c7002 N°3454 - MoveToProd in 2 steps - fix utils::GetCurrentModuleUrl() 2023-07-25 17:20:37 +02:00
Pierre Goiffon
67ca554261 📝 Version history : add 3.1.0-1 2023-07-25 17:07:30 +02:00
Pierre Goiffon
f89953f39e Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-07-24 15:39:09 +02:00
Pierre Goiffon
772368ef8a 💡 PHPDoc for object list panels 2023-07-24 15:38:57 +02:00
Pierre Goiffon
2e049aa244 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-07-24 11:59:57 +02:00
Pierre Goiffon
a57b6471c9 Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-07-24 11:59:40 +02:00
Pierre Goiffon
bc7c1b4744 N°6590 Fix DictionariesConsistencyTest for PL dict files 2023-07-24 11:14:37 +02:00
Eric Espie
12c78697f4 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-07-19 15:20:15 +02:00
Eric Espie
046e857768 Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	core/config.class.inc.php
2023-07-19 15:19:06 +02:00
Eric Espie
4d8246c4d8 N°6436 - Integrate Performance Audit pre requisite in iTop Pro 2.7.9 (changed config variable name) 2023-07-19 15:13:43 +02:00
Eric Espie
5c61d725e1 N°6436 - Integrate Performance Audit pre requisite in iTop Pro 2.7.9 (changed config variable name) 2023-07-19 15:06:00 +02:00
Eric Espie
0c7195f1a3 Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts:
#	core/kpi.class.inc.php
2023-07-19 10:53:09 +02:00
Eric Espie
00b070b3cf Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	core/kpi.class.inc.php
2023-07-19 10:44:22 +02:00
Eric Espie
2c4cad4dac N°6436 - Integrate Performance Audit pre requisite in iTop Pro 2.7.9 (avoid unnecessary calls) 2023-07-19 10:37:41 +02:00
Eric Espie
9c37d5c23e Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts:
#	application/applicationextension.inc.php
#	application/cmdbabstract.class.inc.php
#	core/dbobject.class.php
#	core/kpi.class.inc.php
#	core/metamodel.class.php
#	lib/composer/autoload_classmap.php
#	lib/composer/autoload_static.php
#	tests/php-unit-tests/unitary-tests/setup/DBBackupTest.php
2023-07-19 09:26:46 +02:00
Stephen Abello
89145593ef N°6552 - Security hardening 2023-07-19 09:25:48 +02:00
Eric Espie
b2e80d37dd N°6436 - typo 2023-07-18 14:48:32 +02:00
Eric Espie
6432678de9 Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	application/cmdbabstract.class.inc.php
#	application/utils.inc.php
#	bootstrap.inc.php
#	composer.json
#	core/MyHelpers.class.inc.php
#	core/cmdbsource.class.inc.php
#	core/config.class.inc.php
#	core/dbobject.class.php
#	core/kpi.class.inc.php
#	core/metamodel.class.php
#	lib/composer/autoload_classmap.php
#	lib/composer/autoload_real.php
#	lib/composer/autoload_static.php
2023-07-18 14:36:58 +02:00
Pierre Goiffon
952194b385 N°6570 Fix BulkChangeExtKeyTest errors
- transaction started but never stopped
- invalid value label typo
- urlencode on search url
2023-07-18 14:12:29 +02:00
Pierre Goiffon
bfb452dd69 N°6570 Rename BulkChangeExtKeyTest file so it is run in Jenkins
Was *Test.inc.php instead of default *Test.php
2023-07-18 14:12:29 +02:00
Pierre Goiffon
64baeba1c7 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-07-18 09:49:03 +02:00
Molkobain
71ed784c60 N°6532 - Fix missing "/" in path
(cherry picked from commit 32fd75bc4b)
2023-07-18 09:40:34 +02:00
Eric Espie
da45651121 Merge branch 'feature/6548_Hide_DBHost_and_DBUser_in_log' into support/2.7 2023-07-18 09:34:48 +02:00
Eric Espie
d388ce9a06 Merge branch 'feature/6548_Hide_DBHost_and_DBUser_in_log' into support/2.7 2023-07-18 09:17:40 +02:00
Eric Espie
47e71d8838 Merge branch 'feature/6436-Integrate_Performance_Audit_extensibility' into support/2.7 2023-07-18 09:17:05 +02:00
Stephen Abello
2b5973ec67 N°6436 - Integrate Performance Audit pre requisite in iTop Pro 2.7.9 2023-07-18 09:15:37 +02:00
Benjamin Dalsass
e58918f53e N°6546 - AttributeLinkedSetIndirect filter dosen't work 2023-07-18 08:53:26 +02:00
Molkobain
125715af3f N°6562 - Temporarily disable XML conversion unit tests failing in PHP 8.2.8 2023-07-14 21:09:37 +02:00
Molkobain
ea8e7c5131 N°6562 - Temporarily disable XML conversion unit tests failing in PHP 8.1.21 2023-07-13 10:11:56 +02:00
Eric Espie
06e5e0b102 Merge branch 'support/3.1.0' into support/3.1 2023-07-12 13:31:34 +02:00
Eric Espie
df1cb0b6e3 N°6408 - CRUD : Fix log consuming too much memory 2023-07-12 11:21:32 +02:00
Pierre Goiffon
5247f5b3ea 🔖 Prepare version 3.1.1 2023-07-11 09:56:59 +02:00
Eric Espie
78396d8e4a 6548 - [ER] Hide DBHost and DBUser in log 2023-07-10 17:37:27 +02:00
Pierre Goiffon
f1ee22cbed Merge branch 'release/3.1.0' into develop 2023-07-10 16:57:23 +02:00
Molkobain
39305468f8 N°6043 - Booking: Move \TemporaryObjectDescriptor to /core to keep compatibility with legacy/custom packages 2023-07-10 12:25:32 +02:00
Molkobain
32fd75bc4b N°6532 - Fix missing "/" in path 2023-07-10 09:43:28 +02:00
Pierre Goiffon
efadf2cc79 Merge remote-tracking branch 'origin/support/3.0' into develop 2023-07-07 10:24:57 +02:00
Pierre Goiffon
40d63a2fa4 N°3663 💡 Fix depreciation comment in core/coreexception.class.inc.php 2023-07-07 10:24:15 +02:00
Pierre Goiffon
baa6dedbcf Merge remote-tracking branch 'origin/support/3.0' into develop 2023-07-07 09:32:14 +02:00
Pierre Goiffon
556b9ad89a N°6532 Fix "failed to open stream" error on require_once approot in coreexception.class.inc.php
Was occurring in TemplateFieldValueTest templates-base phpunit test
2023-07-07 09:31:34 +02:00
Stephen Abello
9afc22bd8f N°6123 - Add tests and comments 2023-07-07 09:29:15 +02:00
Pierre Goiffon
ef0b0f88c9 Merge remote-tracking branch 'origin/support/3.0' into develop
# Conflicts:
#	sources/Core/Email/EmailSwiftMailer.php
#	tests/php-unit-tests/integration-tests/DictionariesConsistencyTest.php
#	tests/php-unit-tests/postbuild_integration.xml.dist
2023-07-06 17:11:10 +02:00
Pierre Goiffon
a010239efb Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-07-06 15:48:42 +02:00
Pierre Goiffon
264a8cd70a N°6494 - Some tests are run twice, some never
(cherry picked from commit a2a0b2cd0b)

(cherry picked from commit 4c9ea0c9d4)

# Conflicts:
#	tests/php-unit-tests/integration-tests/DictionariesConsistencyTest.php
2023-07-06 15:45:09 +02:00
Stephen Abello
aa1834170b N°6427 - Fix SwiftMailer not retrieving sendmail path 2023-07-06 14:31:54 +02:00
Stephen Abello
f94d67ab35 N°6340 - Fix permission refused when sending an email and renewing OAuth token in synchronous mode 2023-07-06 10:28:10 +02:00
Stephen Abello
3048c8c41f N°5560 - Display an error when trying to regenerate an expired OAuth token 2023-07-06 09:52:00 +02:00
Stephen Abello
246e4a9f50 N°6123 - Fix warnings when launching a backup on MariaDB > v10.6.1 with localhost dbhost 2023-07-06 09:28:01 +02:00
odain
0001e8ffc4 💚 use new ci validation 2020-10-09 10:13:51 +02:00
145 changed files with 4137 additions and 1945 deletions

View File

@@ -62,6 +62,12 @@ gitGraph
commit id: "2022-12-28" tag: "2.7.8"
checkout support/3.0
commit id: "2023-04-12" tag: "3.0.3"
checkout develop
commit id: "2023-06-19" tag: "3.1.0-beta" type: REVERSE
commit id: "2023-07-26" tag: "3.1.0-1" type: HIGHLIGHT
branch support/3.1 order: 840
checkout support/3.1
commit id: "2023-08-09" tag: "3.1.0-2"
```
To learn more, check the [iTop community versions history on the official wiki](https://www.itophub.io/wiki/page?id=latest:release:start).
To learn more, check the [iTop community versions history on the official wiki](https://www.itophub.io/wiki/page?id=latest:release:start).

View File

@@ -2246,3 +2246,27 @@ interface iModuleExtension
*/
public function __construct();
}
/**
* KPI logging extensibility point
*
* KPI Logger extension
*/
interface iKPILoggerExtension
{
/**
* Init the statistics collected
*
* @return void
*/
public function InitStats();
/**
* Add a new KPI to the stats
*
* @param \Combodo\iTop\Core\Kpi\KpiLogData $oKpiLogData
*
* @return mixed
*/
public function LogOperation($oKpiLogData);
}

View File

@@ -1166,7 +1166,7 @@ HTML
/**
* @param \WebPage $oPage
* @param \CMDBObjectSet $oSet
* @param array $aExtraParams
* @param array $aExtraParams See possible values in {@see DataTableUIBlockFactory::RenderDataTable()}
*
* @throws \ApplicationException
* @throws \CoreException
@@ -4547,7 +4547,9 @@ HTML;
foreach (MetaModel::EnumPlugins(iApplicationObjectExtension::class) as $oExtensionInstance) {
$sExtensionClass = get_class($oExtensionInstance);
$this->LogCRUDDebug(__METHOD__, "Calling $sExtensionClass::OnDBInsert()");
$oKPI = new ExecutionKPI();
$oExtensionInstance->OnDBInsert($this, self::GetCurrentChange());
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBInsert');
}
}
@@ -4562,13 +4564,16 @@ HTML;
protected function DBCloneTracked_Internal($newKey = null)
{
$oNewObj = parent::DBCloneTracked_Internal($newKey);
/** @var cmdbAbstractObject $oNewObj */
$oNewObj = MetaModel::GetObject(get_class($this), parent::DBCloneTracked_Internal($newKey));
// Invoke extensions after insertion (the object must exist, have an id, etc.)
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$oKPI = new ExecutionKPI();
$oExtensionInstance->OnDBInsert($oNewObj, self::GetCurrentChange());
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBInsert');
}
return $oNewObj;
@@ -4605,7 +4610,9 @@ HTML;
foreach (MetaModel::EnumPlugins(iApplicationObjectExtension::class) as $oExtensionInstance) {
$sExtensionClass = get_class($oExtensionInstance);
$this->LogCRUDDebug(__METHOD__, "Calling $sExtensionClass::OnDBUpdate()");
$oKPI = new ExecutionKPI();
$oExtensionInstance->OnDBUpdate($this, self::GetCurrentChange());
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBUpdate');
}
}
@@ -4649,7 +4656,9 @@ HTML;
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$oKPI = new ExecutionKPI();
$oExtensionInstance->OnDBDelete($this, self::GetCurrentChange());
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBDelete');
}
return parent::DBDeleteTracked_Internal($oDeletionPlan);
@@ -4668,7 +4677,10 @@ HTML;
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$sExtensionClass = get_class($oExtensionInstance);
if ($oExtensionInstance->OnIsModified($this)) {
$oKPI = new ExecutionKPI();
$bIsModified = $oExtensionInstance->OnIsModified($this);
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnIsModified');
if ($bIsModified) {
$this->LogCRUDDebug(__METHOD__, "Calling $sExtensionClass::OnIsModified() -> true");
return true;
} else {
@@ -4724,7 +4736,9 @@ HTML;
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$oKPI = new ExecutionKPI();
$aNewIssues = $oExtensionInstance->OnCheckToWrite($this);
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnCheckToWrite');
if (is_array($aNewIssues) && (count($aNewIssues) > 0)) // Some extensions return null instead of an empty array
{
$this->m_aCheckIssues = array_merge($this->m_aCheckIssues, $aNewIssues);
@@ -4772,7 +4786,9 @@ HTML;
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$oKPI = new ExecutionKPI();
$aNewIssues = $oExtensionInstance->OnCheckToDelete($this);
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnCheckToDelete');
if (is_array($aNewIssues) && count($aNewIssues) > 0)
{
$this->m_aDeleteIssues = array_merge($this->m_aDeleteIssues, $aNewIssues);

View File

@@ -918,6 +918,11 @@ class RuntimeDashboard extends Dashboard
{
$bCustomized = false;
$sDashboardFileSanitized = utils::RealPath($sDashboardFile, APPROOT);
if (false === $sDashboardFileSanitized) {
throw new SecurityException('Invalid dashboard file !');
}
// Search for an eventual user defined dashboard
$oUDSearch = new DBObjectSearch('UserDashboard');
$oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
@@ -929,7 +934,7 @@ class RuntimeDashboard extends Dashboard
$sDashboardDefinition = $oUserDashboard->Get('contents');
$bCustomized = true;
} else {
$sDashboardDefinition = @file_get_contents($sDashboardFile);
$sDashboardDefinition = @file_get_contents($sDashboardFileSanitized);
}
@@ -937,7 +942,7 @@ class RuntimeDashboard extends Dashboard
$oDashboard = new RuntimeDashboard($sDashBoardId);
$oDashboard->FromXml($sDashboardDefinition);
$oDashboard->SetCustomFlag($bCustomized);
$oDashboard->SetDefinitionFile($sDashboardFile);
$oDashboard->SetDefinitionFile($sDashboardFileSanitized);
} else {
$oDashboard = null;
}

View File

@@ -99,4 +99,10 @@ else
Session::Set('itop_env', ITOP_DEFAULT_ENV);
}
$sConfigFile = APPCONF.$sEnv.'/'.ITOP_CONFIG_FILE;
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, $bAllowCache, false /* $bTraceSourceFiles */, $sEnv);
try {
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, $bAllowCache, false /* $bTraceSourceFiles */, $sEnv);
}
catch (MySQLException $e) {
IssueLog::Debug($e->getMessage());
throw new MySQLException('Could not connect to the DB server', []);
}

View File

@@ -20,6 +20,7 @@
use Combodo\iTop\Application\Helper\Session;
use Combodo\iTop\Application\UI\Base\iUIBlock;
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
use Combodo\iTop\Service\Module\ModuleService;
use ScssPhp\ScssPhp\Compiler;
use ScssPhp\ScssPhp\OutputStyle;
use ScssPhp\ScssPhp\ValueConverter;
@@ -1396,13 +1397,23 @@ class utils
return APPROOT . 'env-' . MetaModel::GetEnvironment() . '/';
}
/**
* @return string A path to the folder into which data can be written
* @internal
* @since N°6097 2.7.10 3.0.4 3.1.1
*/
public static function GetDataPath(): string
{
return APPROOT.'data/';
}
/**
* @return string A path to a folder into which any module can store cache data
* The corresponding folder is created or cleaned upon code compilation
*/
public static function GetCachePath()
{
return APPROOT.'data/cache-'.MetaModel::GetEnvironment().'/';
return static::GetDataPath().'cache-'.MetaModel::GetEnvironment().'/';
}
/**
@@ -2265,24 +2276,7 @@ SQL;
*/
public static function GetCurrentModuleName($iCallDepth = 0)
{
$sCurrentModuleName = '';
$aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$sCallerFile = realpath($aCallStack[$iCallDepth]['file']);
foreach(GetModulesInfo() as $sModuleName => $aInfo)
{
if ($aInfo['root_dir'] !== '')
{
$sRootDir = realpath(APPROOT.$aInfo['root_dir']);
if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir)
{
$sCurrentModuleName = $sModuleName;
break;
}
}
}
return $sCurrentModuleName;
return ModuleService::GetInstance()->GetCurrentModuleName($iCallDepth + 1);
}
/**
@@ -2304,24 +2298,7 @@ SQL;
*/
public static function GetCurrentModuleDir($iCallDepth)
{
$sCurrentModuleDir = '';
$aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$sCallerFile = realpath($aCallStack[$iCallDepth]['file']);
foreach(GetModulesInfo() as $sModuleName => $aInfo)
{
if ($aInfo['root_dir'] !== '')
{
$sRootDir = realpath(APPROOT.$aInfo['root_dir']);
if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir)
{
$sCurrentModuleDir = basename($sRootDir);
break;
}
}
}
return $sCurrentModuleDir;
return ModuleService::GetInstance()->GetCurrentModuleDir($iCallDepth);
}
/**
@@ -2336,12 +2313,7 @@ SQL;
*/
public static function GetCurrentModuleUrl()
{
$sDir = static::GetCurrentModuleDir(1);
if ( $sDir !== '')
{
return static::GetAbsoluteUrlModulesRoot().'/'.$sDir;
}
return '';
return ModuleService::GetInstance()->GetCurrentModuleUrl(1);
}
/**
@@ -2351,8 +2323,7 @@ SQL;
*/
public static function GetCurrentModuleSetting($sProperty, $defaultvalue = null)
{
$sModuleName = static::GetCurrentModuleName(1);
return MetaModel::GetModuleSetting($sModuleName, $sProperty, $defaultvalue);
return ModuleService::GetInstance()->GetCurrentModuleSetting($sProperty, $defaultvalue);
}
/**
@@ -2361,12 +2332,7 @@ SQL;
*/
public static function GetCompiledModuleVersion($sModuleName)
{
$aModulesInfo = GetModulesInfo();
if (array_key_exists($sModuleName, $aModulesInfo))
{
return $aModulesInfo[$sModuleName]['version'];
}
return null;
return ModuleService::GetInstance()->GetCompiledModuleVersion($sModuleName);
}
/**
@@ -2691,24 +2657,26 @@ SQL;
}
/**
* Returns the local path relative to the iTop installation of an existing file
* Returns the local path relative to the iTop installation (APPROOT or the given base path)
* Dir separator is changed to '/' for consistency among the different OS
*
* @param string $sAbsolutePath absolute path
* @param string $sBasePath Base path for the resulting local path (default APPROOT)
*
* @return false|string
* @return false|string The generated local path or false if absolute path is not under the base path
* @since 3.1.1 Added base path defaulted to previous version APPROOT
*/
final public static function LocalPath($sAbsolutePath)
final public static function LocalPath($sAbsolutePath, string $sBasePath = APPROOT)
{
$sRootPath = realpath(APPROOT);
$sRootPath = realpath($sBasePath);
$sFullPath = realpath($sAbsolutePath);
if (($sFullPath === false) || !self::StartsWith($sFullPath, $sRootPath))
{
return false;
}
$sLocalPath = substr($sFullPath, strlen($sRootPath.DIRECTORY_SEPARATOR));
$sLocalPath = str_replace(DIRECTORY_SEPARATOR, '/', $sLocalPath);
return $sLocalPath;
return str_replace(DIRECTORY_SEPARATOR, '/', $sLocalPath);
}
/**
@@ -2900,7 +2868,7 @@ HTML;
// Add already loaded classes
$aCurrentClasses = array_fill_keys(get_declared_classes(), '');
$aClassMap = array_merge($aClassMap, $aCurrentClasses);
$aClassMap = array_merge($aCurrentClasses, $aClassMap);
foreach ($aClassMap as $sPHPClass => $sPHPFile) {
$bSkipped = false;
@@ -2925,11 +2893,12 @@ HTML;
$bSkipped = true; // file not found
}
}
if(!$bSkipped){
try {
$oRefClass = new ReflectionClass($sPHPClass);
if ($oRefClass->implementsInterface($sInterface) && $oRefClass->isInstantiable()) {
if ($oRefClass->implementsInterface($sInterface) &&
!$oRefClass->isInterface() && !$oRefClass->isAbstract() && !$oRefClass->isTrait()) {
$aMatchingClasses[] = $sPHPClass;
}
} catch (Exception $e) {

View File

@@ -45,6 +45,7 @@ define('MAINTENANCE_MODE_FILE', APPROOT.'data/.maintenance');
define('READONLY_MODE_FILE', APPROOT.'data/.readonly');
$fItopStarted = microtime(true);
$iItopInitialMemory = memory_get_usage(true);
if (!isset($GLOBALS['bBypassAutoload']) || $GLOBALS['bBypassAutoload'] == false) {
require_once APPROOT.'/lib/autoload.php';

View File

@@ -59,9 +59,16 @@ class DbConnectionWrapper
* Use this to register a mock that will handle {@see mysqli::query()}
*
* @param \mysqli|null $oMysqli
* @since 3.0.4 3.1.1 3.2.0 Param $oMysqli becomes nullable
*/
public static function SetDbConnectionMockForQuery(?mysqli $oMysqli): void
public static function SetDbConnectionMockForQuery(?mysqli $oMysqli = null): void
{
static::$oDbCnxMockableForQuery = $oMysqli;
if (is_null($oMysqli)) {
// Reset to standard connection
static::$oDbCnxMockableForQuery = static::$oDbCnxStandard;
}
else {
static::$oDbCnxMockableForQuery = $oMysqli;
}
}
}

View File

@@ -419,6 +419,7 @@ class MyHelpers
//}
return $sOutput;
}
}
/**
@@ -523,5 +524,3 @@ class Str
return (strtolower($sString) == $sString);
}
}
?>

View File

@@ -431,6 +431,7 @@ class CMDBSource
{
self::$m_sDBName = '';
}
self::_TablesInfoCacheReset(); // reset the table info cache!
}
public static function CreateTable($sQuery)
@@ -607,8 +608,9 @@ class CMDBSource
{
self::LogDeadLock($e, true);
throw new MySQLException('Failed to issue SQL query', array('query' => $sSql, $e));
}
$oKPI->ComputeStats('Query exec (mySQL)', $sSql);
} finally {
$oKPI->ComputeStats('Query exec (mySQL)', $sSql);
}
if ($oResult === false) {
$aContext = array('query' => $sSql);
@@ -626,18 +628,24 @@ class CMDBSource
}
/**
* @param \Exception $e
* @param Exception $e
* @param bool $bForQuery to get the proper DB connection
* @param bool $bCheckMysqliErrno if false won't try to check for mysqli::errno value
*
* @since 2.7.1
* @since 3.0.0 N°4325 add new optional parameter to use the correct DB connection
* @since 3.0.4 3.1.1 3.2.0 N°6643 new bCheckMysqliErrno parameter as a workaround for mysqli::errno cannot be mocked
*/
private static function LogDeadLock(Exception $e, $bForQuery = false)
private static function LogDeadLock(Exception $e, $bForQuery = false, $bCheckMysqliErrno = true)
{
// checks MySQL error code
$iMySqlErrorNo = DbConnectionWrapper::GetDbConnection($bForQuery)->errno;
if (!in_array($iMySqlErrorNo, array(self::MYSQL_ERRNO_WAIT_TIMEOUT, self::MYSQL_ERRNO_DEADLOCK))) {
return;
if ($bCheckMysqliErrno) {
$iMySqlErrorNo = DbConnectionWrapper::GetDbConnection($bForQuery)->errno;
if (!in_array($iMySqlErrorNo, array(self::MYSQL_ERRNO_WAIT_TIMEOUT, self::MYSQL_ERRNO_DEADLOCK))) {
return;
}
} else {
$iMySqlErrorNo = "N/A";
}
// Get error info
@@ -664,7 +672,10 @@ class CMDBSource
);
DeadLockLog::Info($sMessage, $iMySqlErrorNo, $aLogContext);
IssueLog::Error($sMessage, LogChannels::DEADLOCK, $e->getMessage());
IssueLog::Error($sMessage, LogChannels::DEADLOCK, [
'exception.class' => get_class($e),
'exception.message' => $e->getMessage(),
]);
}
/**

View File

@@ -29,7 +29,7 @@ define('ITOP_APPLICATION_SHORT', 'iTop');
*
* @see ITOP_CORE_VERSION to get iTop core version
*/
define('ITOP_VERSION', '3.1.0-dev');
define('ITOP_VERSION', '3.1.1-dev');
define('ITOP_VERSION_NAME', 'Fullmoon');
define('ITOP_REVISION', 'svn');
@@ -656,22 +656,22 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'email_transport_smtp.allow_self_signed' => array(
'email_transport_smtp.allow_self_signed' => [
'type' => 'bool',
'description' => 'Allow self signed peer certificates',
'default' => false,
'value' => false,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'email_transport_smtp.verify_peer' => array(
],
'email_transport_smtp.verify_peer' => [
'type' => 'bool',
'description' => 'Verify peer certificate',
'default' => true,
'value' => true,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
],
'email_css' => [
'type' => 'string',
'description' => 'CSS that will override the standard stylesheet used for the notifications',
@@ -1069,6 +1069,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'log_kpi_generate_legacy_report' => [
'type' => 'bool',
'description' => 'Generate the legacy KPI report (kpi.html)',
'default' => true,
'value' => '',
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'max_linkset_output' => [
'type' => 'integer',
'description' => 'Maximum number of items shown when getting a list of related items in an email, using the form $this->some_list$. 0 means no limit.',

View File

@@ -1,8 +1,11 @@
<?php
/**
* This file is only here for compatibility issues. Will be removed in iTop 3.1.0 (N°3664)
* This file is only here for compatibility reasons.
* It will be removed in future iTop versions (N°6533)
*
* @deprecated 3.0.0 N°3663 Exception classes were moved to `/application/exceptions`, use autoloader instead of require !
*/
require_once '../approot.inc.php';
DeprecatedCallsLog::NotifyDeprecatedFile('Classes were moved to /application/exceptions');
require_once __DIR__ . '/../approot.inc.php';
DeprecatedCallsLog::NotifyDeprecatedFile('Classes were moved to /application/exceptions and can be used directly with the autoloader');

View File

@@ -188,8 +188,8 @@ final class ItopCounter
if (!$hDBLink)
{
throw new Exception("Could not connect to the DB server (host=$sDBHost, user=$sDBUser): ".mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno().')');
}
throw new MySQLException('Could not connect to the DB server '.mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno(), array('host' => $sDBHost, 'user' => $sDBUser));
}
return $hDBLink;
}

View File

@@ -612,8 +612,11 @@ abstract class DBObject implements iDisplay
public function Set($sAttCode, $value)
{
if (!utils::StartsWith(get_class($this), 'CMDBChange') && $this->GetKey() > 0) {
// not all the values have __to_string() so print_r is sed and preferred over var_export for the handling or circular references
$this->LogCRUDEnter(__METHOD__, "$sAttCode => ".print_r($value, true));
if (is_object($value) || is_array($value)) {
$this->LogCRUDEnter(__METHOD__, "$sAttCode => object or array");
} else {
$this->LogCRUDEnter(__METHOD__, "$sAttCode => ".print_r($value, true));
}
}
$sMessage = $this->IsReadOnly();
@@ -1141,7 +1144,9 @@ abstract class DBObject implements iDisplay
return; //skip!
}
$this->FireEventComputeValues();
$oKPI = new ExecutionKPI();
$this->ComputeValues();
$oKPI->ComputeStatsForExtension($this, 'ComputeValues');
}
/**
@@ -2477,7 +2482,6 @@ abstract class DBObject implements iDisplay
{
$this->m_aCheckIssues = array();
$oKPI = new ExecutionKPI();
if ($bDoComputeValues) {
$this->DoComputeValues();
}
@@ -2487,8 +2491,9 @@ abstract class DBObject implements iDisplay
$this->FireEventCheckToWrite();
$this->SetReadWrite();
$oKPI = new ExecutionKPI();
$this->DoCheckToWrite();
$oKPI->ComputeStats('CheckToWrite', get_class($this));
$oKPI->ComputeStatsForExtension($this, 'DoCheckToWrite');
if (count($this->m_aCheckIssues) == 0)
{
$this->m_bCheckStatus = true;
@@ -3111,7 +3116,9 @@ abstract class DBObject implements iDisplay
// Ensure the update of the values (we are accessing the data directly)
$this->DoComputeValues();
$oKPI = new ExecutionKPI();
$this->OnInsert();
$oKPI->ComputeStatsForExtension($this, 'OnInsert');
$this->FireEventBeforeWrite();
@@ -3167,7 +3174,9 @@ abstract class DBObject implements iDisplay
$this->DBInsertSingleTable($sParentClass);
}
$oKPI = new ExecutionKPI();
$this->OnObjectKeyReady();
$oKPI->ComputeStatsForExtension($this, 'OnObjectKeyReady');
$this->UpdateCurrentObjectInCrudStack();
$this->DBWriteLinks();
@@ -3244,7 +3253,9 @@ abstract class DBObject implements iDisplay
public function PostInsertActions(): void
{
$this->FireEventAfterWrite([], true);
$oKPI = new ExecutionKPI();
$this->AfterInsert();
$oKPI->ComputeStatsForExtension($this, 'AfterInsert');
// Activate any existing trigger
$sClass = get_class($this);
@@ -3342,7 +3353,9 @@ abstract class DBObject implements iDisplay
try {
$this->DoComputeValues();
$this->ComputeStopWatchesDeadline(false);
$oKPI = new ExecutionKPI();
$this->OnUpdate();
$oKPI->ComputeStatsForExtension($this, 'OnUpdate');
$this->FireEventBeforeWrite();
@@ -3556,10 +3569,13 @@ abstract class DBObject implements iDisplay
public function PostUpdateActions(array $aChanges): void
{
$this->FireEventAfterWrite($aChanges, false);
$oKPI = new ExecutionKPI();
$this->AfterUpdate();
$oKPI->ComputeStatsForExtension($this, 'AfterUpdate');
// - TriggerOnObjectUpdate
$aParams = array('class_list' => MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL));
$aClassList = MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL);
$aParams = array('class_list' => $aClassList);
$oSet = new DBObjectSet(DBObjectSearch::FromOQL('SELECT TriggerOnObjectUpdate AS t WHERE t.target_class IN (:class_list)'),
array(), $aParams);
while ($oTrigger = $oSet->Fetch()) {
@@ -3573,6 +3589,44 @@ abstract class DBObject implements iDisplay
}
}
$sClass = get_class($this);
if (MetaModel::HasLifecycle($sClass))
{
$sStateAttCode = MetaModel::GetStateAttributeCode($sClass);
if (isset($this->m_aPreviousValuesForUpdatedAttributes[$sStateAttCode])) {
$sPreviousState = $this->m_aPreviousValuesForUpdatedAttributes[$sStateAttCode];
// Change state triggers...
$aParams = array(
'class_list' => MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL),
'previous_state' => $sPreviousState,
'new_state' => $this->Get($sStateAttCode),
);
$oSet = new DBObjectSet(DBObjectSearch::FromOQL('SELECT TriggerOnStateLeave AS t WHERE t.target_class IN (:class_list) AND t.state=:previous_state'), array(), $aParams);
while ($oTrigger = $oSet->Fetch()) {
/** @var \TriggerOnStateLeave $oTrigger */
try {
$oTrigger->DoActivate($this->ToArgs('this'));
}
catch (Exception $e) {
$oTrigger->LogException($e, $this);
utils::EnrichRaisedException($oTrigger, $e);
}
}
$oSet = new DBObjectSet(DBObjectSearch::FromOQL('SELECT TriggerOnStateEnter AS t WHERE t.target_class IN (:class_list) AND t.state=:new_state'), array(), $aParams);
while ($oTrigger = $oSet->Fetch()) {
/** @var \TriggerOnStateEnter $oTrigger */
try {
$oTrigger->DoActivate($this->ToArgs('this'));
}
catch (Exception $e) {
$oTrigger->LogException($e, $this);
utils::EnrichRaisedException($oTrigger, $e);
}
}
}
}
// Activate any existing trigger
// - TriggerOnObjectMention
// Forgotten by the fix of N°3245
@@ -3783,7 +3837,9 @@ abstract class DBObject implements iDisplay
return;
}
$oKPI = new ExecutionKPI();
$this->OnDelete();
$oKPI->ComputeStatsForExtension($this, 'OnDelete');
// Activate any existing trigger
$sClass = get_class($this);
@@ -3891,7 +3947,9 @@ abstract class DBObject implements iDisplay
}
$this->FireEventAfterDelete();
$oKPI = new ExecutionKPI();
$this->AfterDelete();
$oKPI->ComputeStatsForExtension($this, 'AfterDelete');
$this->m_bIsInDB = false;
@@ -4264,36 +4322,6 @@ abstract class DBObject implements iDisplay
$this->DBWrite();
}
// Change state triggers...
$aParams = array(
'class_list' => MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL),
'previous_state' => $sPreviousState,
'new_state' => $sNewState,
);
$oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnStateLeave AS t WHERE t.target_class IN (:class_list) AND t.state=:previous_state"), array(), $aParams);
while ($oTrigger = $oSet->Fetch()) {
/** @var \TriggerOnStateLeave $oTrigger */
try {
$oTrigger->DoActivate($this->ToArgs('this'));
}
catch (Exception $e) {
$oTrigger->LogException($e, $this);
utils::EnrichRaisedException($oTrigger, $e);
}
}
$oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnStateEnter AS t WHERE t.target_class IN (:class_list) AND t.state=:new_state"), array(), $aParams);
while ($oTrigger = $oSet->Fetch()) {
/** @var \TriggerOnStateEnter $oTrigger */
try {
$oTrigger->DoActivate($this->ToArgs('this'));
}
catch (Exception $e) {
$oTrigger->LogException($e, $this);
utils::EnrichRaisedException($oTrigger, $e);
}
}
$this->FireEvent(EVENT_DB_AFTER_APPLY_STIMULUS, $aEventData);
}
else

View File

@@ -767,7 +767,10 @@ class DBObjectSet implements iDBObjectSetIterator
try
{
$oKPI = new ExecutionKPI();
$this->m_oSQLResult = CMDBSource::Query($sSQL);
$sOQL = $this->GetPseudoOQL($this->m_oFilter, $this->GetRealSortOrder(), $this->m_iLimitCount, $this->m_iLimitStart, false);
$oKPI->ComputeStats('OQL Query Exec', $sOQL);
} catch (MySQLException $e)
{
// 1116 = ER_TOO_MANY_TABLES
@@ -847,8 +850,11 @@ class DBObjectSet implements iDBObjectSetIterator
{
if (is_null($this->m_iNumTotalDBRows))
{
$oKPI = new ExecutionKPI();
$sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, 0, 0, true);
$resQuery = CMDBSource::Query($sSQL);
$sOQL = $this->GetPseudoOQL($this->m_oFilter, array(), 0, 0, true);
$oKPI->ComputeStats('OQL Query Exec', $sOQL);
if (!$resQuery) return 0;
$aRow = CMDBSource::FetchArray($resQuery);
@@ -859,6 +865,42 @@ class DBObjectSet implements iDBObjectSetIterator
return $this->m_iNumTotalDBRows + count($this->m_aAddedObjects); // Does it fix Trac #887 ??
}
/**
* @param \DBSearch $oFilter
* @param array $aOrder
* @param int $iLimitCount
* @param int $iLimitStart
* @param bool $bCount
*
* @return string
*/
private function GetPseudoOQL($oFilter, $aOrder, $iLimitCount, $iLimitStart, $bCount)
{
$sOQL = '';
if ($bCount) {
$sOQL .= 'COUNT ';
}
$sOQL .= $oFilter->ToOQL();
if ($iLimitCount > 0) {
$sOQL .= ' LIMIT ';
if ($iLimitStart > 0) {
$sOQL .= "$iLimitStart, ";
}
$sOQL .= "$iLimitCount";
}
if (count($aOrder) > 0) {
$sOQL .= ' ORDER BY ';
$aOrderBy = [];
foreach ($aOrder as $sAttCode => $bAsc) {
$aOrderBy[] = $sAttCode.' '.($bAsc ? 'ASC' : 'DESC');
}
$sOQL .= implode(', ', $aOrderBy);
}
return $sOQL;
}
/**
* Check if the count exceeds a given limit
*
@@ -875,8 +917,11 @@ class DBObjectSet implements iDBObjectSetIterator
{
if (is_null($this->m_iNumTotalDBRows))
{
$oKPI = new ExecutionKPI();
$sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, $iLimit + 2, 0, true);
$resQuery = CMDBSource::Query($sSQL);
$sOQL = $this->GetPseudoOQL($this->m_oFilter, array(), $iLimit + 2, 0, true);
$oKPI->ComputeStats('OQL Query Exec', $sOQL);
if ($resQuery)
{
$aRow = CMDBSource::FetchArray($resQuery);
@@ -887,7 +932,7 @@ class DBObjectSet implements iDBObjectSetIterator
{
$iCount = 0;
}
}
}
else
{
$iCount = $this->m_iNumTotalDBRows;
@@ -912,8 +957,11 @@ class DBObjectSet implements iDBObjectSetIterator
{
if (is_null($this->m_iNumTotalDBRows))
{
$oKPI = new ExecutionKPI();
$sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, $iLimit + 2, 0, true);
$resQuery = CMDBSource::Query($sSQL);
$sOQL = $this->GetPseudoOQL($this->m_oFilter, array(), $iLimit + 2, 0, true);
$oKPI->ComputeStats('OQL Query Exec', $sOQL);
if ($resQuery)
{
$aRow = CMDBSource::FetchArray($resQuery);
@@ -924,7 +972,7 @@ class DBObjectSet implements iDBObjectSetIterator
{
$iCount = 0;
}
}
}
else
{
$iCount = $this->m_iNumTotalDBRows;

View File

@@ -56,10 +56,11 @@ class Dict
* @param $sLanguageCode
*
* @throws \DictExceptionUnknownLanguage
* @since 3.0.4 3.1.1 3.2.0 Param $sLanguageCode becomes nullable
*/
public static function SetUserLanguage($sLanguageCode)
public static function SetUserLanguage($sLanguageCode = null)
{
if (!array_key_exists($sLanguageCode, self::$m_aLanguages))
if (!is_null($sLanguageCode) && !array_key_exists($sLanguageCode, self::$m_aLanguages))
{
throw new DictExceptionUnknownLanguage($sLanguageCode);
}

View File

@@ -1,27 +1,14 @@
<?php
// Copyright (C) 2010-2023 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
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Core\Kpi\KpiLogData;
use Combodo\iTop\Service\Module\ModuleService;
/**
* Measures operations duration, memory usage, etc. (and some other KPIs)
*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
class ExecutionKPI
@@ -30,6 +17,8 @@ class ExecutionKPI
static protected $m_bEnabled_Memory = false;
static protected $m_bBlameCaller = false;
static protected $m_sAllowedUser = '*';
static protected $m_bGenerateLegacyReport = true;
static protected $m_fSlowQueries = 0;
static protected $m_aStats = []; // Recurrent operations
static protected $m_aExecData = []; // One shot operations
@@ -86,14 +75,39 @@ class ExecutionKPI
return false;
}
static public function SetGenerateLegacyReport($bReportExtensionsOnly)
{
self::$m_bGenerateLegacyReport = $bReportExtensionsOnly;
}
static public function SetSlowQueries($fSlowQueries)
{
self::$m_fSlowQueries = $fSlowQueries;
}
static public function GetDescription()
{
$aFeatures = array();
if (self::$m_bEnabled_Duration) $aFeatures[] = 'Duration';
if (self::$m_bEnabled_Memory) $aFeatures[] = 'Memory usage';
$sFeatures = implode(', ', $aFeatures);
$sFeatures = 'Measures: '.implode(', ', $aFeatures);
$sFor = self::$m_sAllowedUser == '*' ? 'EVERYBODY' : "'".trim(self::$m_sAllowedUser)."'";
return "KPI logging is active for $sFor. Measures: $sFeatures";
$sSlowQueries = '';
if (self::$m_fSlowQueries > 0) {
$sSlowQueries = ". Slow Queries: ".self::$m_fSlowQueries."s";
}
$aExtensions = [];
/** @var \iKPILoggerExtension $oExtensionInstance */
foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) {
$aExtensions[] = ModuleService::GetInstance()->GetModuleNameFromObject($oExtensionInstance);
}
$sExtensions = '';
if (count($aExtensions) > 0) {
$sExtensions = '. KPI Extensions: ['.implode(', ', $aExtensions).']';
}
return "KPI logging is active for $sFor. $sFeatures$sSlowQueries$sExtensions";
}
static public function ReportStats()
@@ -101,7 +115,28 @@ class ExecutionKPI
if (!self::IsEnabled()) return;
global $fItopStarted;
global $iItopInitialMemory;
$sExecId = microtime(); // id to differentiate the hrefs!
$sRequest = $_SERVER['REQUEST_URI'].' ('.$_SERVER['REQUEST_METHOD'].')';
if (isset($_POST['operation'])) {
$sRequest .= ' operation: '.$_POST['operation'];
}
$fStop = MyHelpers::getmicrotime();
if (($fStop - $fItopStarted) > self::$m_fSlowQueries) {
// Invoke extensions to log the KPI operation
/** @var \iKPILoggerExtension $oExtensionInstance */
$iCurrentMemory = self::memory_get_usage();
$iPeakMemory = self::memory_get_peak_usage();
foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) {
$oKPILogData = new KpiLogData(KpiLogData::TYPE_REQUEST, 'Page', $sRequest, $fItopStarted, $fStop, '', $iItopInitialMemory, $iCurrentMemory, $iPeakMemory);
$oExtensionInstance->LogOperation($oKPILogData);
}
}
if (!self::$m_bGenerateLegacyReport) {
return;
}
$aBeginTimes = array();
foreach (self::$m_aExecData as $aOpStats)
@@ -114,9 +149,9 @@ class ExecutionKPI
$sHtml = "<hr/>";
$sHtml .= "<div style=\"background-color: grey; padding: 10px;\">";
$sHtml .= "<h3><a name=\"".md5($sExecId)."\">KPIs</a> - ".$_SERVER['REQUEST_URI']." (".$_SERVER['REQUEST_METHOD'].")</h3>";
$sHtml .= "<h3><a name=\"".md5($sExecId)."\">KPIs</a> - $sRequest</h3>";
$oStarted = DateTime::createFromFormat('U.u', $fItopStarted);
$sHtml .= "<p>".$oStarted->format('Y-m-d H:i:s.u')."</p>";
$sHtml .= '<p>'.$oStarted->format('Y-m-d H:i:s.u').'</p>';
$sHtml .= "<p>log_kpi_user_id: ".UserRights::GetUserId()."</p>";
$sHtml .= "<div>";
$sHtml .= "<table border=\"1\" style=\"$sTableStyle\">";
@@ -257,7 +292,7 @@ class ExecutionKPI
$sTotalInter = round($fTotalInter, 3);
$sMinInter = round($fMinInter, 3);
$sMaxInter = round($fMaxInter, 3);
if (($fTotalInter >= $fSlowQueries))
if (($fTotalInter >= self::$m_fSlowQueries))
{
if ($bDisplayHeader)
{
@@ -285,37 +320,19 @@ class ExecutionKPI
self::Report($sHtml);
}
public static function InitStats()
{
// Invoke extensions to initialize the KPI statistics
/** @var \iKPILoggerExtension $oExtensionInstance */
foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) {
$oExtensionInstance->InitStats();
}
}
public function __construct()
{
$this->ResetCounters();
self::Push($this);
}
/**
* Stack executions to remove children duration from stats
*
* @param \ExecutionKPI $oExecutionKPI
*/
private static function Push(ExecutionKPI $oExecutionKPI)
{
self::$m_aExecutionStack[] = $oExecutionKPI;
}
/**
* Pop current child and count its duration in its parent
*
* @param float|int $fChildDuration
*/
private static function Pop(float $fChildDuration = 0)
{
array_pop(self::$m_aExecutionStack);
// Update the parent's children duration
$oPrevExecutionKPI = end(self::$m_aExecutionStack);
if ($oPrevExecutionKPI) {
$oPrevExecutionKPI->m_fChildrenDuration += $fChildDuration;
}
}
}
// Get the duration since startup, and reset the counter for the next measure
//
@@ -323,9 +340,15 @@ class ExecutionKPI
{
global $fItopStarted;
if (!self::IsEnabled()) {
return;
}
$aNewEntry = null;
if (self::$m_bEnabled_Duration) {
$fStarted = $this->m_fStarted;
$fStopped = $this->m_fStarted;
if (self::$m_bEnabled_Duration) {
$fStopped = MyHelpers::getmicrotime();
$aNewEntry = array(
'op' => $sOperationDesc,
@@ -336,6 +359,9 @@ class ExecutionKPI
$this->m_fStarted = $fStopped;
}
$iInitialMemory = is_null($this->m_iInitialMemory) ? 0 : $this->m_iInitialMemory;
$iCurrentMemory = 0;
$iPeakMemory = 0;
if (self::$m_bEnabled_Memory)
{
$iCurrentMemory = self::memory_get_usage();
@@ -345,40 +371,103 @@ class ExecutionKPI
}
$aNewEntry['mem_begin'] = $this->m_iInitialMemory;
$aNewEntry['mem_end'] = $iCurrentMemory;
if (function_exists('memory_get_peak_usage'))
{
$aNewEntry['mem_peak'] = memory_get_peak_usage();
}
$iPeakMemory = self::memory_get_peak_usage();
$aNewEntry['mem_peak'] = $iPeakMemory;
// Reset for the next operation (if the object is recycled)
$this->m_iInitialMemory = $iCurrentMemory;
}
if (!is_null($aNewEntry))
if (self::$m_bEnabled_Duration || self::$m_bEnabled_Memory) {
// Invoke extensions to log the KPI operation
/** @var \iKPILoggerExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance)
{
$sExtension = ModuleService::GetInstance()->GetModuleNameFromCallStack(1);
$oKPILogData = new KpiLogData(
KpiLogData::TYPE_REPORT,
'Step',
$sOperationDesc,
$fStarted,
$fStopped,
$sExtension,
$iInitialMemory,
$iCurrentMemory,
$iPeakMemory);
$oExtensionInstance->LogOperation($oKPILogData);
}
}
if (!is_null($aNewEntry) && self::$m_bGenerateLegacyReport)
{
self::$m_aExecData[] = $aNewEntry;
}
$this->ResetCounters();
}
public function ComputeStatsForExtension($object, $sMethod)
{
if (!self::IsEnabled()) {
return;
}
$sSignature = ModuleService::GetInstance()->GetModuleMethodSignature($object, $sMethod);
if (utils::StartsWith($sSignature, '[')) {
$this->ComputeStats('Extension', $sSignature);
}
}
public function ComputeStats($sOperation, $sArguments)
{
if (!self::IsEnabled()) {
return;
}
$fDuration = 0;
if (self::$m_bEnabled_Duration) {
$fStopped = MyHelpers::getmicrotime();
$fDuration = $fStopped - $this->m_fStarted;
$fSelfDuration = $fDuration - $this->m_fChildrenDuration;
if (self::$m_bBlameCaller) {
self::$m_aStats[$sOperation][$sArguments][] = array(
'time' => $fSelfDuration,
'callers' => MyHelpers::get_callstack(1),
);
} else {
self::$m_aStats[$sOperation][$sArguments][] = array(
'time' => $fSelfDuration,
);
}
}
self::Pop($fDuration);
$aCallstack = [];
if (self::$m_bGenerateLegacyReport) {
if (self::$m_bBlameCaller) {
$aCallstack = MyHelpers::get_callstack(1);
self::$m_aStats[$sOperation][$sArguments][] = [
'time' => $fDuration,
'callers' => $aCallstack,
];
} else {
self::$m_aStats[$sOperation][$sArguments][] = [
'time' => $fDuration
];
}
}
$iInitialMemory = is_null($this->m_iInitialMemory) ? 0 : $this->m_iInitialMemory;
$iCurrentMemory = 0;
$iPeakMemory = 0;
if (self::$m_bEnabled_Memory)
{
$iCurrentMemory = self::memory_get_usage();
$iPeakMemory = self::memory_get_peak_usage();
}
// Invoke extensions to log the KPI operation
/** @var \iKPILoggerExtension $oExtensionInstance */
foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) {
$sExtension = ModuleService::GetInstance()->GetModuleNameFromCallStack(1);
$oKPILogData = new KpiLogData(
KpiLogData::TYPE_STATS,
$sOperation,
$sArguments,
$this->m_fStarted,
$fStopped,
$sExtension,
$iInitialMemory,
$iCurrentMemory,
$iPeakMemory,
$aCallstack);
$oExtensionInstance->LogOperation($oKPILogData);
}
}
}
protected function ResetCounters()
@@ -408,35 +497,7 @@ class ExecutionKPI
static protected function memory_get_usage()
{
if (function_exists('memory_get_usage'))
{
return memory_get_usage(true);
}
// Copied from the PHP manual
//
//If its Windows
//Tested on Win XP Pro SP2. Should work on Win 2003 Server too
//Doesn't work for 2000
//If you need it to work for 2000 look at http://us2.php.net/manual/en/function.memory-get-usage.php#54642
if (substr(PHP_OS,0,3) == 'WIN')
{
$output = array();
exec('tasklist /FI "PID eq ' . getmypid() . '" /FO LIST', $output);
return preg_replace( '/[\D]/', '', $output[5] ) * 1024;
}
else
{
//We now assume the OS is UNIX
//Tested on Mac OS X 10.4.6 and Linux Red Hat Enterprise 4
//This should work on most UNIX systems
$pid = getmypid();
exec("ps -eo%mem,rss,pid | grep $pid", $output);
$output = explode(" ", $output[0]);
//rss is given in 1024 byte units
return $output[1] * 1024;
}
return memory_get_usage(true);
}
static public function memory_get_peak_usage($bRealUsage = false)

View File

@@ -1138,7 +1138,7 @@ class DeprecatedCallsLog extends LogAPI
parent::Enable($sTargetFile);
if (
(false === defined(ITOP_PHPUNIT_RUNNING_CONSTANT_NAME))
(false === defined('ITOP_PHPUNIT_RUNNING_CONSTANT_NAME'))
&& static::IsLogLevelEnabledSafe(self::LEVEL_WARNING, self::ENUM_CHANNEL_PHP_LIBMETHOD)
) {
set_error_handler([static::class, 'DeprecatedNoticesErrorHandler'], E_DEPRECATED | E_USER_DEPRECATED);

View File

@@ -6298,6 +6298,13 @@ abstract class MetaModel
*/
public static function Startup($config, $bModelOnly = false, $bAllowCache = true, $bTraceSourceFiles = false, $sEnvironment = 'production')
{
// Startup on a new environment is not supported
static $bStarted = false;
if ($bStarted) {
return;
}
$bStarted = true;
self::$m_sEnvironment = $sEnvironment;
try {
@@ -6376,7 +6383,9 @@ abstract class MetaModel
ExecutionKPI::EnableDuration(self::$m_oConfig->Get('log_kpi_duration'));
ExecutionKPI::EnableMemory(self::$m_oConfig->Get('log_kpi_memory'));
ExecutionKPI::SetAllowedUser(self::$m_oConfig->Get('log_kpi_user_id'));
ExecutionKPI::SetAllowedUser(self::$m_oConfig->Get('log_kpi_user_id'));
ExecutionKPI::SetGenerateLegacyReport(self::$m_oConfig->Get('log_kpi_generate_legacy_report'));
ExecutionKPI::SetSlowQueries(self::$m_oConfig->Get('log_kpi_slow_queries'));
self::$m_bSkipCheckToWrite = self::$m_oConfig->Get('skip_check_to_write');
self::$m_bSkipCheckExtKeys = self::$m_oConfig->Get('skip_check_ext_keys');
@@ -6495,6 +6504,7 @@ abstract class MetaModel
CMDBSource::InitFromConfig(self::$m_oConfig);
// Later when timezone implementation is correctly done: CMDBSource::SetTimezone($sDBTimezone);
ExecutionKPI::InitStats();
}
/**
@@ -6526,6 +6536,19 @@ abstract class MetaModel
return $value;
}
/**
* @internal Used for resetting the configuration during automated tests
* @param \Config $oConfiguration
*
* @return void
* @since 3.0.4 3.1.1 3.2.0
*/
public static function SetConfig(Config $oConfiguration)
{
self::$m_oConfig = $oConfiguration;
}
/**
* @return Config
*/
@@ -6807,25 +6830,21 @@ abstract class MetaModel
* $bMustBeFound=false)
* @throws CoreException if no result found and $bMustBeFound=true
* @throws ArchivedObjectException if archive mode disabled and result is archived and $bMustBeFound=true
* @throws \Exception
*
*/
public static function GetObject($sClass, $iKey, $bMustBeFound = true, $bAllowAllData = false, $aModifierProperties = null)
{
$oObject = self::GetObjectWithArchive($sClass, $iKey, $bMustBeFound, $bAllowAllData, $aModifierProperties);
if (empty($oObject))
{
if (empty($oObject)) {
return null;
}
if (!utils::IsArchiveMode() && $oObject->IsArchived())
{
if (!utils::IsArchiveMode() && $oObject->IsArchived()) {
if ($bMustBeFound) {
throw new ArchivedObjectException("The object $sClass::$iKey is archived");
} else {
return null;
}
return null;
}
return $oObject;
@@ -7608,14 +7627,12 @@ abstract class MetaModel
// Build the list of available extensions
//
$aInterfaces = [
'iApplicationUIExtension',
'iPreferencesExtension',
'iApplicationObjectExtension',
'iLoginFSMExtension',
'iLoginUIExtension',
'iLogoutExtension',
'iQueryModifier',
'iOnClassInitialization',
'iLoginUIExtension',
'iPreferencesExtension',
'iApplicationUIExtension',
'iApplicationObjectExtension',
'iPopupMenuExtension',
'iPageUIExtension',
'iPageUIBlockExtension',
@@ -7629,9 +7646,12 @@ abstract class MetaModel
'iBackofficeDictEntriesExtension',
'iBackofficeDictEntriesPrefixesExtension',
'iPortalUIExtension',
'iQueryModifier',
'iOnClassInitialization',
'iModuleExtension',
'iKPILoggerExtension',
'ModuleHandlerApiInterface',
'iNewsroomProvider',
'iModuleExtension',
];
foreach ($aInterfaces as $sInterface) {
self::$m_aExtensionClassNames[$sInterface] = array();

View File

@@ -257,7 +257,7 @@ class iTopMutex
$this->hDBLink = CMDBSource::GetMysqliInstance($sServer, $sUser, $sPwd, $sSource, $bTlsEnabled, $sTlsCA, false);
if (!$this->hDBLink) {
throw new Exception("Could not connect to the DB server (host=$sServer, user=$sUser): ".mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno().')');
throw new MySQLException('Could not connect to the DB server '.mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno(), array('host' => $sDBHost, 'user' => $sDBUser));
}
// Make sure that the server variable `wait_timeout` is at least 86400 seconds for this connection,

View File

@@ -121,7 +121,9 @@ abstract class Trigger extends cmdbAbstractObject
$oAction = MetaModel::GetObject('Action', $iActionId);
if ($oAction->IsActive())
{
$oKPI = new ExecutionKPI();
$oAction->DoExecute($this, $aContextArgs);
$oKPI->ComputeStatsForExtension($oAction, 'DoExecute');
}
}
}

View File

@@ -761,14 +761,25 @@ class UserRights
protected static $m_aCacheContactPictureAbsUrl = [];
/** @var UserRightsAddOnAPI $m_oAddOn */
protected static $m_oAddOn;
protected static $m_oUser;
protected static $m_oRealUser;
protected static $m_oUser = null;
protected static $m_oRealUser = null;
protected static $m_sSelfRegisterAddOn = null;
protected static $m_aAdmins = array();
protected static $m_aPortalUsers = array();
/** @var array array('sName' => $sName, 'bSuccess' => $bSuccess); */
private static $m_sLastLoginStatus = null;
/**
* @return void
* @since 3.0.4 3.1.1 3.2.0
*/
protected static function ResetCurrentUserData()
{
self::$m_oUser = null;
self::$m_oRealUser = null;
self::$m_sLastLoginStatus = null;
}
/**
* @param string $sModuleName
*
@@ -787,8 +798,7 @@ class UserRights
}
self::$m_oAddOn = new $sModuleName;
self::$m_oAddOn->Init();
self::$m_oUser = null;
self::$m_oRealUser = null;
self::ResetCurrentUserData();
}
/**
@@ -855,6 +865,8 @@ class UserRights
*/
public static function Login($sLogin, $sAuthentication = 'any')
{
static::Logoff();
$oUser = self::FindUser($sLogin, $sAuthentication);
if (is_null($oUser))
{
@@ -872,6 +884,17 @@ class UserRights
return true;
}
/**
* @return void
* @since 3.0.4 3.1.1 3.2.0
*/
public static function Logoff()
{
self::ResetCurrentUserData();
Dict::SetUserLanguage(null);
self::_ResetSessionCache();
}
/**
* @param string $sLogin Login of the user to check the credentials for
* @param string $sPassword

View File

@@ -235,13 +235,16 @@ class DBRestore extends DBBackup
if (in_array($oFileInfo->getFilename(), $aStandardFiles)) {
continue;
}
if (strncmp($oFileInfo->getPathname(), $sDataDir.'/production-modules', strlen($sDataDir.'/production-modules')) == 0) {
// Normalize filenames to cope with Windows backslashes
$sPath = str_replace('\\', '/', $oFileInfo->getPathname());
$sRefPath = str_replace('\\', '/', $sDataDir.'/production-modules');
if (strncmp($sPath, $sRefPath, strlen($sRefPath)) == 0) {
continue;
}
$aExtraFiles[$oFileInfo->getPathname()] = APPROOT.substr($oFileInfo->getPathname(), strlen($sDataDir));
}
return $aExtraFiles;
}
}

View File

@@ -58,7 +58,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:FAQ/Attribute:category_id+' => '',
'Class:FAQ/Attribute:category_name' => '类别名称',
'Class:FAQ/Attribute:category_name+' => '',
'Class:FAQ/Attribute:error_code' => '错误码',
'Class:FAQ/Attribute:error_code' => '错误码',
'Class:FAQ/Attribute:error_code+' => '',
'Class:FAQ/Attribute:key_words' => '关键字',
'Class:FAQ/Attribute:key_words+' => '',

View File

@@ -66,11 +66,11 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:Incident/Attribute:status+' => '',
'Class:Incident/Attribute:status/Value:new' => '新建',
'Class:Incident/Attribute:status/Value:new+' => '',
'Class:Incident/Attribute:status/Value:escalated_tto' => '已升级响应时间',
'Class:Incident/Attribute:status/Value:escalated_tto' => '已升级TTO',
'Class:Incident/Attribute:status/Value:escalated_tto+' => '',
'Class:Incident/Attribute:status/Value:assigned' => '已分配',
'Class:Incident/Attribute:status/Value:assigned+' => '',
'Class:Incident/Attribute:status/Value:escalated_ttr' => '已升级解决时间',
'Class:Incident/Attribute:status/Value:escalated_ttr' => '已升级TTR',
'Class:Incident/Attribute:status/Value:escalated_ttr+' => '',
'Class:Incident/Attribute:status/Value:waiting_for_approval' => '等待批准',
'Class:Incident/Attribute:status/Value:waiting_for_approval+' => '',
@@ -90,8 +90,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:Incident/Attribute:impact/Value:3+' => '',
'Class:Incident/Attribute:priority' => '优先级',
'Class:Incident/Attribute:priority+' => '',
'Class:Incident/Attribute:priority/Value:1' => '非常高',
'Class:Incident/Attribute:priority/Value:1+' => '非常高',
'Class:Incident/Attribute:priority/Value:1' => '紧急',
'Class:Incident/Attribute:priority/Value:1+' => '紧急',
'Class:Incident/Attribute:priority/Value:2' => '高',
'Class:Incident/Attribute:priority/Value:2+' => '高',
'Class:Incident/Attribute:priority/Value:3' => '中',
@@ -100,8 +100,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:Incident/Attribute:priority/Value:4+' => '低',
'Class:Incident/Attribute:urgency' => '紧急度',
'Class:Incident/Attribute:urgency+' => '',
'Class:Incident/Attribute:urgency/Value:1' => '非常高',
'Class:Incident/Attribute:urgency/Value:1+' => '非常高',
'Class:Incident/Attribute:urgency/Value:1' => '紧急',
'Class:Incident/Attribute:urgency/Value:1+' => '紧急',
'Class:Incident/Attribute:urgency/Value:2' => '高',
'Class:Incident/Attribute:urgency/Value:2+' => '高',
'Class:Incident/Attribute:urgency/Value:3' => '中',
@@ -136,7 +136,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:Incident/Attribute:escalation_flag/Value:no+' => '否',
'Class:Incident/Attribute:escalation_flag/Value:yes' => '是',
'Class:Incident/Attribute:escalation_flag/Value:yes+' => '是',
'Class:Incident/Attribute:escalation_reason' => '热门',
'Class:Incident/Attribute:escalation_reason' => '升级原因',
'Class:Incident/Attribute:escalation_reason+' => '',
'Class:Incident/Attribute:assignment_date' => '分配日期',
'Class:Incident/Attribute:assignment_date+' => '',
@@ -146,21 +146,21 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:Incident/Attribute:last_pending_date+' => '',
'Class:Incident/Attribute:cumulatedpending' => '累计待定',
'Class:Incident/Attribute:cumulatedpending+' => '',
'Class:Incident/Attribute:tto' => '响应时间',
'Class:Incident/Attribute:tto+' => '',
'Class:Incident/Attribute:ttr' => '解决时间',
'Class:Incident/Attribute:ttr+' => '',
'Class:Incident/Attribute:tto_escalation_deadline' => '响应时间截止',
'Class:Incident/Attribute:tto' => 'TTO',
'Class:Incident/Attribute:tto+' => '响应时间',
'Class:Incident/Attribute:ttr' => 'TTR',
'Class:Incident/Attribute:ttr+' => '解决时限',
'Class:Incident/Attribute:tto_escalation_deadline' => 'TTO截止日期',
'Class:Incident/Attribute:tto_escalation_deadline+' => '',
'Class:Incident/Attribute:sla_tto_passed' => '超过SLA响应时间',
'Class:Incident/Attribute:sla_tto_passed' => 'SLA TTO 合格',
'Class:Incident/Attribute:sla_tto_passed+' => '',
'Class:Incident/Attribute:sla_tto_over' => 'SLA响应时间结束',
'Class:Incident/Attribute:sla_tto_over' => 'SLA TTO 超时',
'Class:Incident/Attribute:sla_tto_over+' => '',
'Class:Incident/Attribute:ttr_escalation_deadline' => '解决时间截止',
'Class:Incident/Attribute:ttr_escalation_deadline' => 'TTR截止日期',
'Class:Incident/Attribute:ttr_escalation_deadline+' => '',
'Class:Incident/Attribute:sla_ttr_passed' => '超过SLA解决时间',
'Class:Incident/Attribute:sla_ttr_passed' => 'SLA TTR 合格',
'Class:Incident/Attribute:sla_ttr_passed+' => '',
'Class:Incident/Attribute:sla_ttr_over' => 'SLA解决时间结束',
'Class:Incident/Attribute:sla_ttr_over' => 'SLA TTR 超时',
'Class:Incident/Attribute:sla_ttr_over+' => '',
'Class:Incident/Attribute:time_spent' => '耗时',
'Class:Incident/Attribute:time_spent+' => '',
@@ -182,7 +182,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:Incident/Attribute:resolution_code/Value:training+' => '培训',
'Class:Incident/Attribute:solution' => '解决方案',
'Class:Incident/Attribute:solution+' => '',
'Class:Incident/Attribute:pending_reason' => '待定原因',
'Class:Incident/Attribute:pending_reason' => '待定原因',
'Class:Incident/Attribute:pending_reason+' => '',
'Class:Incident/Attribute:parent_incident_id' => '父级事件',
'Class:Incident/Attribute:parent_incident_id+' => '',

View File

@@ -66,7 +66,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:KnownError/Attribute:workaround+' => '',
'Class:KnownError/Attribute:solution' => '解决方案',
'Class:KnownError/Attribute:solution+' => '',
'Class:KnownError/Attribute:error_code' => '错误码',
'Class:KnownError/Attribute:error_code' => '错误码',
'Class:KnownError/Attribute:error_code+' => '',
'Class:KnownError/Attribute:domain' => '类型',
'Class:KnownError/Attribute:domain+' => '',

View File

@@ -230,6 +230,7 @@ HTML
$this->Set('refresh_token', $oAccessToken->getRefreshToken());
}
$this->Set('status', 'active');
$this->AllowWrite();
$this->DBUpdate();
}
]]></code>

View File

@@ -11,6 +11,7 @@ use Combodo\iTop\Application\TwigBase\Controller\Controller;
use Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderFactory;
use Dict;
use IssueLog;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use MetaModel;
use utils;
use WebPage;
@@ -65,13 +66,15 @@ class AjaxOauthClientController extends Controller
}
if (isset($aQuery['code'])) {
$sCode = $aQuery['code'];
$oAccessToken = OAuthClientProviderFactory::GetAccessTokenFromCode($oOAuthClient, $sCode);
$oOAuthClient->SetAccessToken($oAccessToken);
$aResult['status'] = 'success';
try {
$oAccessToken = OAuthClientProviderFactory::GetAccessTokenFromCode($oOAuthClient, $sCode);
$oOAuthClient->SetAccessToken($oAccessToken);
$aResult['status'] = 'success';
}
catch (IdentityProviderException $e) {
$aResult['status'] = 'error';
$aResult['error_description'] = $e->getMessage();
}
}
} else {
$aResult['status'] = 'error';

View File

@@ -103,8 +103,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:Problem/Attribute:impact/Value:3+' => '',
'Class:Problem/Attribute:urgency' => '紧急度',
'Class:Problem/Attribute:urgency+' => '',
'Class:Problem/Attribute:urgency/Value:1' => '非常高',
'Class:Problem/Attribute:urgency/Value:1+' => '非常高',
'Class:Problem/Attribute:urgency/Value:1' => '紧急',
'Class:Problem/Attribute:urgency/Value:1+' => '紧急',
'Class:Problem/Attribute:urgency/Value:2' => '高',
'Class:Problem/Attribute:urgency/Value:2+' => '高',
'Class:Problem/Attribute:urgency/Value:3' => '中',
@@ -113,8 +113,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:Problem/Attribute:urgency/Value:4+' => '低',
'Class:Problem/Attribute:priority' => '优先级',
'Class:Problem/Attribute:priority+' => '',
'Class:Problem/Attribute:priority/Value:1' => '非常高',
'Class:Problem/Attribute:priority/Value:1+' => '非常高',
'Class:Problem/Attribute:priority/Value:1' => '紧急',
'Class:Problem/Attribute:priority/Value:1+' => '紧急',
'Class:Problem/Attribute:priority/Value:2' => '高',
'Class:Problem/Attribute:priority/Value:2+' => '高',
'Class:Problem/Attribute:priority/Value:3' => '中',

View File

@@ -58,11 +58,11 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:UserRequest/Attribute:status+' => '',
'Class:UserRequest/Attribute:status/Value:new' => '新建',
'Class:UserRequest/Attribute:status/Value:new+' => '',
'Class:UserRequest/Attribute:status/Value:escalated_tto' => '已升级响应时间',
'Class:UserRequest/Attribute:status/Value:escalated_tto' => '已升级TTO',
'Class:UserRequest/Attribute:status/Value:escalated_tto+' => '',
'Class:UserRequest/Attribute:status/Value:assigned' => '已分配',
'Class:UserRequest/Attribute:status/Value:assigned+' => '',
'Class:UserRequest/Attribute:status/Value:escalated_ttr' => '已升级解决时间',
'Class:UserRequest/Attribute:status/Value:escalated_ttr' => '已升级TTR',
'Class:UserRequest/Attribute:status/Value:escalated_ttr+' => '',
'Class:UserRequest/Attribute:status/Value:waiting_for_approval' => '等待批准',
'Class:UserRequest/Attribute:status/Value:waiting_for_approval+' => '',
@@ -90,8 +90,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:UserRequest/Attribute:impact/Value:3+' => '',
'Class:UserRequest/Attribute:priority' => '优先级',
'Class:UserRequest/Attribute:priority+' => '',
'Class:UserRequest/Attribute:priority/Value:1' => '非常高',
'Class:UserRequest/Attribute:priority/Value:1+' => '非常高',
'Class:UserRequest/Attribute:priority/Value:1' => '紧急',
'Class:UserRequest/Attribute:priority/Value:1+' => '紧急',
'Class:UserRequest/Attribute:priority/Value:2' => '高',
'Class:UserRequest/Attribute:priority/Value:2+' => '高',
'Class:UserRequest/Attribute:priority/Value:3' => '中',
@@ -100,8 +100,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:UserRequest/Attribute:priority/Value:4+' => '低',
'Class:UserRequest/Attribute:urgency' => '紧急度',
'Class:UserRequest/Attribute:urgency+' => '',
'Class:UserRequest/Attribute:urgency/Value:1' => '非常高',
'Class:UserRequest/Attribute:urgency/Value:1+' => '非常高',
'Class:UserRequest/Attribute:urgency/Value:1' => '紧急',
'Class:UserRequest/Attribute:urgency/Value:1+' => '紧急',
'Class:UserRequest/Attribute:urgency/Value:2' => '高',
'Class:UserRequest/Attribute:urgency/Value:2+' => '高',
'Class:UserRequest/Attribute:urgency/Value:3' => '中',
@@ -134,7 +134,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:UserRequest/Attribute:servicesubcategory_id+' => '',
'Class:UserRequest/Attribute:servicesubcategory_name' => '子服务名称',
'Class:UserRequest/Attribute:servicesubcategory_name+' => '',
'Class:UserRequest/Attribute:escalation_flag' => '是否升级',
'Class:UserRequest/Attribute:escalation_flag' => '升级标签',
'Class:UserRequest/Attribute:escalation_flag+' => '',
'Class:UserRequest/Attribute:escalation_flag/Value:no' => '否',
'Class:UserRequest/Attribute:escalation_flag/Value:no+' => '否',
@@ -150,30 +150,30 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:UserRequest/Attribute:last_pending_date+' => '',
'Class:UserRequest/Attribute:cumulatedpending' => '累计待定',
'Class:UserRequest/Attribute:cumulatedpending+' => '',
'Class:UserRequest/Attribute:tto' => '响应时间',
'Class:UserRequest/Attribute:tto' => 'TTO',
'Class:UserRequest/Attribute:tto+' => '',
'Class:UserRequest/Attribute:ttr' => '解决时间',
'Class:UserRequest/Attribute:ttr' => 'TTR',
'Class:UserRequest/Attribute:ttr+' => '',
'Class:UserRequest/Attribute:tto_escalation_deadline' => '响应时间截止',
'Class:UserRequest/Attribute:tto_escalation_deadline' => 'TTO截止日期',
'Class:UserRequest/Attribute:tto_escalation_deadline+' => '',
'Class:UserRequest/Attribute:sla_tto_passed' => '超过SLA响应时间',
'Class:UserRequest/Attribute:sla_tto_passed' => 'SLA TTO 合格',
'Class:UserRequest/Attribute:sla_tto_passed+' => '',
'Class:UserRequest/Attribute:sla_tto_over' => 'SLA响应时间超过',
'Class:UserRequest/Attribute:sla_tto_over' => 'SLA TTO 超时',
'Class:UserRequest/Attribute:sla_tto_over+' => '',
'Class:UserRequest/Attribute:ttr_escalation_deadline' => '解决时间截止',
'Class:UserRequest/Attribute:ttr_escalation_deadline' => 'TTR截止日期',
'Class:UserRequest/Attribute:ttr_escalation_deadline+' => '',
'Class:UserRequest/Attribute:sla_ttr_passed' => '超过SLA解决时间',
'Class:UserRequest/Attribute:sla_ttr_passed' => 'SLA TTR 合格',
'Class:UserRequest/Attribute:sla_ttr_passed+' => '',
'Class:UserRequest/Attribute:sla_ttr_over' => 'SLA解决时间超过',
'Class:UserRequest/Attribute:sla_ttr_over' => 'SLA TTR 超时',
'Class:UserRequest/Attribute:sla_ttr_over+' => '',
'Class:UserRequest/Attribute:time_spent' => '耗时',
'Class:UserRequest/Attribute:time_spent+' => '',
'Class:UserRequest/Attribute:resolution_code' => '解决码',
'Class:UserRequest/Attribute:resolution_code' => '解决码',
'Class:UserRequest/Attribute:resolution_code+' => '',
'Class:UserRequest/Attribute:resolution_code/Value:assistance' => '帮助',
'Class:UserRequest/Attribute:resolution_code/Value:assistance+' => '帮助',
'Class:UserRequest/Attribute:resolution_code/Value:bug fixed' => '缺陷修复',
'Class:UserRequest/Attribute:resolution_code/Value:bug fixed+' => '缺陷修复',
'Class:UserRequest/Attribute:resolution_code/Value:bug fixed' => 'bug修复',
'Class:UserRequest/Attribute:resolution_code/Value:bug fixed+' => 'bug修复',
'Class:UserRequest/Attribute:resolution_code/Value:hardware repair' => '硬件维修',
'Class:UserRequest/Attribute:resolution_code/Value:hardware repair+' => '硬件维修',
'Class:UserRequest/Attribute:resolution_code/Value:other' => '其它',

View File

@@ -62,11 +62,11 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:UserRequest/Attribute:status+' => '',
'Class:UserRequest/Attribute:status/Value:new' => '新建',
'Class:UserRequest/Attribute:status/Value:new+' => '',
'Class:UserRequest/Attribute:status/Value:escalated_tto' => '已升级响应时间',
'Class:UserRequest/Attribute:status/Value:escalated_tto' => '已升级TTO',
'Class:UserRequest/Attribute:status/Value:escalated_tto+' => '',
'Class:UserRequest/Attribute:status/Value:assigned' => '已分配',
'Class:UserRequest/Attribute:status/Value:assigned+' => '',
'Class:UserRequest/Attribute:status/Value:escalated_ttr' => '已升级解决时间',
'Class:UserRequest/Attribute:status/Value:escalated_ttr' => '已升级TTR',
'Class:UserRequest/Attribute:status/Value:escalated_ttr+' => '',
'Class:UserRequest/Attribute:status/Value:waiting_for_approval' => '等待批准',
'Class:UserRequest/Attribute:status/Value:waiting_for_approval+' => '',
@@ -156,21 +156,21 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:UserRequest/Attribute:last_pending_date+' => '',
'Class:UserRequest/Attribute:cumulatedpending' => '累计待定',
'Class:UserRequest/Attribute:cumulatedpending+' => '',
'Class:UserRequest/Attribute:tto' => '响应时间',
'Class:UserRequest/Attribute:tto+' => '',
'Class:UserRequest/Attribute:ttr' => '解决时间',
'Class:UserRequest/Attribute:ttr+' => '',
'Class:UserRequest/Attribute:tto_escalation_deadline' => '响应时间期限',
'Class:UserRequest/Attribute:tto' => 'TTO',
'Class:UserRequest/Attribute:tto+' => '响应时间',
'Class:UserRequest/Attribute:ttr' => 'TTR',
'Class:UserRequest/Attribute:ttr+' => '解决时限',
'Class:UserRequest/Attribute:tto_escalation_deadline' => 'TTO截止日期',
'Class:UserRequest/Attribute:tto_escalation_deadline+' => '',
'Class:UserRequest/Attribute:sla_tto_passed' => '超过SLA响应时间',
'Class:UserRequest/Attribute:sla_tto_passed' => 'SLA TTO 合格',
'Class:UserRequest/Attribute:sla_tto_passed+' => '',
'Class:UserRequest/Attribute:sla_tto_over' => 'SLA响应时间结束',
'Class:UserRequest/Attribute:sla_tto_over' => 'SLA TTO 超时',
'Class:UserRequest/Attribute:sla_tto_over+' => '',
'Class:UserRequest/Attribute:ttr_escalation_deadline' => '解决时间期限',
'Class:UserRequest/Attribute:ttr_escalation_deadline' => 'TTR截止日期',
'Class:UserRequest/Attribute:ttr_escalation_deadline+' => '',
'Class:UserRequest/Attribute:sla_ttr_passed' => '超过SLA解决时间',
'Class:UserRequest/Attribute:sla_ttr_passed' => 'SLA TTR 合格',
'Class:UserRequest/Attribute:sla_ttr_passed+' => '',
'Class:UserRequest/Attribute:sla_ttr_over' => 'SLA解决时间结束',
'Class:UserRequest/Attribute:sla_ttr_over' => 'SLA TTR 超时',
'Class:UserRequest/Attribute:sla_ttr_over+' => '',
'Class:UserRequest/Attribute:time_spent' => '耗时',
'Class:UserRequest/Attribute:time_spent+' => '',
@@ -192,7 +192,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:UserRequest/Attribute:resolution_code/Value:training+' => '培训',
'Class:UserRequest/Attribute:solution' => '解决方案',
'Class:UserRequest/Attribute:solution+' => '',
'Class:UserRequest/Attribute:pending_reason' => '待定原因',
'Class:UserRequest/Attribute:pending_reason' => '待定原因',
'Class:UserRequest/Attribute:pending_reason+' => '',
'Class:UserRequest/Attribute:parent_request_id' => '父级需求',
'Class:UserRequest/Attribute:parent_request_id+' => '',

View File

@@ -389,8 +389,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:SLT/Attribute:name+' => '',
'Class:SLT/Attribute:priority' => '优先级',
'Class:SLT/Attribute:priority+' => '',
'Class:SLT/Attribute:priority/Value:1' => '非常高',
'Class:SLT/Attribute:priority/Value:1+' => '非常高',
'Class:SLT/Attribute:priority/Value:1' => '紧急',
'Class:SLT/Attribute:priority/Value:1+' => '紧急',
'Class:SLT/Attribute:priority/Value:2' => '高',
'Class:SLT/Attribute:priority/Value:2+' => '高',
'Class:SLT/Attribute:priority/Value:3' => '中',
@@ -403,15 +403,15 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:SLT/Attribute:request_type/Value:incident+' => '事件',
'Class:SLT/Attribute:request_type/Value:service_request' => '服务需求',
'Class:SLT/Attribute:request_type/Value:service_request+' => '服务需求',
'Class:SLT/Attribute:metric' => '指标',
'Class:SLT/Attribute:metric' => '衡量指标',
'Class:SLT/Attribute:metric+' => '',
'Class:SLT/Attribute:metric/Value:tto' => '响应时间',
'Class:SLT/Attribute:metric/Value:tto' => 'TTO',
'Class:SLT/Attribute:metric/Value:tto+' => '响应时间',
'Class:SLT/Attribute:metric/Value:ttr' => '解决时间',
'Class:SLT/Attribute:metric/Value:ttr+' => '解决时',
'Class:SLT/Attribute:metric/Value:ttr' => 'TTR',
'Class:SLT/Attribute:metric/Value:ttr+' => '解决时',
'Class:SLT/Attribute:value' => '值',
'Class:SLT/Attribute:value+' => '',
'Class:SLT/Attribute:unit' => '单位',
'Class:SLT/Attribute:unit' => '度量单位',
'Class:SLT/Attribute:unit+' => '',
'Class:SLT/Attribute:unit/Value:hours' => '小时',
'Class:SLT/Attribute:unit/Value:hours+' => '小时',

View File

@@ -362,8 +362,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:SLT/Attribute:name+' => '',
'Class:SLT/Attribute:priority' => '优先级',
'Class:SLT/Attribute:priority+' => '',
'Class:SLT/Attribute:priority/Value:1' => '非常高',
'Class:SLT/Attribute:priority/Value:1+' => '非常高',
'Class:SLT/Attribute:priority/Value:1' => '紧急',
'Class:SLT/Attribute:priority/Value:1+' => '紧急',
'Class:SLT/Attribute:priority/Value:2' => '高',
'Class:SLT/Attribute:priority/Value:2+' => '高',
'Class:SLT/Attribute:priority/Value:3' => '中',
@@ -376,15 +376,15 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:SLT/Attribute:request_type/Value:incident+' => '事件',
'Class:SLT/Attribute:request_type/Value:service_request' => '服务需求',
'Class:SLT/Attribute:request_type/Value:service_request+' => '服务需求',
'Class:SLT/Attribute:metric' => '指标',
'Class:SLT/Attribute:metric' => '衡量指标',
'Class:SLT/Attribute:metric+' => '',
'Class:SLT/Attribute:metric/Value:tto' => '响应时间',
'Class:SLT/Attribute:metric/Value:tto' => 'TTO',
'Class:SLT/Attribute:metric/Value:tto+' => '响应时间',
'Class:SLT/Attribute:metric/Value:ttr' => '解决时间',
'Class:SLT/Attribute:metric/Value:ttr+' => '解决时',
'Class:SLT/Attribute:metric/Value:ttr' => 'TTR',
'Class:SLT/Attribute:metric/Value:ttr+' => '解决时',
'Class:SLT/Attribute:value' => '值',
'Class:SLT/Attribute:value+' => '',
'Class:SLT/Attribute:unit' => '单位',
'Class:SLT/Attribute:unit' => '度量单位',
'Class:SLT/Attribute:unit+' => '',
'Class:SLT/Attribute:unit/Value:hours' => '小时',
'Class:SLT/Attribute:unit/Value:hours+' => '小时',

View File

@@ -251,7 +251,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:DocumentNote' => '文档笔记',
'Class:DocumentNote+' => '',
'Class:DocumentNote/Attribute:text' => '文',
'Class:DocumentNote/Attribute:text' => '文',
'Class:DocumentNote/Attribute:text+' => '',
));

View File

@@ -240,9 +240,9 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:cmdbAbstractObject/Method:ApplyStimulus+' => 'Apply the specified stimulus to the current object',
'Class:cmdbAbstractObject/Method:ApplyStimulus/Param:1' => 'Stimulus code',
'Class:cmdbAbstractObject/Method:ApplyStimulus/Param:1+' => 'A valid stimulus code for the current class',
'Class:ResponseTicketTTO/Interface:iMetricComputer' => '响应时间',
'Class:ResponseTicketTTO/Interface:iMetricComputer' => 'TTO',
'Class:ResponseTicketTTO/Interface:iMetricComputer+' => 'SLT 的响应时间',
'Class:ResponseTicketTTR/Interface:iMetricComputer' => '解决时间',
'Class:ResponseTicketTTR/Interface:iMetricComputer+' => 'SLT 的解决时',
'Class:ResponseTicketTTR/Interface:iMetricComputer' => 'TTR',
'Class:ResponseTicketTTR/Interface:iMetricComputer+' => 'SLT 的解决时',
));

View File

@@ -19,9 +19,9 @@
// Display DataTable
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'UI:Datatables:Language:Processing' => '请稍候...',
'UI:Datatables:Language:LengthMenu' => '_MENU_ 每页',
'UI:Datatables:Language:LengthMenu' => '每页 _MENU_ ',
'UI:Datatables:Language:ZeroRecords' => '未找到相关结果',
'UI:Datatables:Language:Info' => '_TOTAL_ 项',
'UI:Datatables:Language:Info' => '_TOTAL_ 项',
'UI:Datatables:Language:InfoEmpty' => '未找到相关信息',
'UI:Datatables:Language:EmptyTable' => '表格中暂无数据',
'UI:Datatables:Language:Error' => '运行查询时出错',

View File

@@ -16,6 +16,7 @@
*
* You should have received a copy of the GNU Affero General Public License
*/
// Navigation menu
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'UI:Layout:NavigationMenu:CompanyLogo:AltText' => '公司logo',

View File

@@ -20,44 +20,60 @@
* You should have received a copy of the GNU Affero General Public License
* along with iTop. If not, see <http://www.gnu.org/licenses/>
*/
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Core:DeletedObjectLabel' => '%1s (已删除)',
'Core:DeletedObjectTip' => '对象已被删除于 %1$s (%2$s)',
'Core:UnknownObjectLabel' => '对象找不到 (class: %1$s, id: %2$d)',
'Core:UnknownObjectTip' => 'The object could not be found. It may have been deleted some time ago and the log has been purged since.~~',
'Core:UniquenessDefaultError' => 'Uniqueness rule \'%1$s\' in error~~',
'Core:CheckConsistencyError' => 'Consistency rules not followed: %1$s~~',
'Core:CheckValueError' => 'Unexpected value for attribute \'%1$s\' (%2$s) : %3$s~~',
'Core:AttributeLinkedSet' => '对象数组',
'Core:AttributeLinkedSet+' => 'Any kind of objects of the same class or subclass~~',
'Core:AttributeLinkedSetDuplicatesFound' => 'Duplicates in the \'%1$s\' field : %2$s~~',
'Core:AttributeDashboard' => '仪表盘',
'Core:AttributeDashboard+' => '',
'Core:AttributePhoneNumber' => '电话号码',
'Core:AttributePhoneNumber+' => '',
'Core:AttributeObsolescenceDate' => '报废日期',
'Core:AttributeObsolescenceDate+' => '',
'Core:AttributeTagSet' => '清单',
'Core:AttributeTagSet+' => '',
'Core:AttributeSet:placeholder' => '请点击这里添加',
'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromClass' => '%1$s (%2$s)~~',
'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromOneChildClass' => '%1$s (%2$s from %3$s)~~',
'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromSeveralChildClasses' => '%1$s (%2$s from child classes)~~',
'Core:AttributeCaseLog' => '日志',
'Core:AttributeCaseLog+' => '',
'Core:AttributeMetaEnum' => 'Computed enum~~',
'Core:AttributeMetaEnum+' => '~~',
'Core:AttributeLinkedSetIndirect' => '对象数组(N-N)',
'Core:AttributeLinkedSetIndirect+' => 'Any kind of objects [subclass] of the same class~~',
'Core:AttributeInteger' => '整数',
'Core:AttributeInteger+' => '整数值(可以为负)',
'Core:AttributeDecimal' => '小数',
'Core:AttributeDecimal+' => '小数(可以为负)',
'Core:AttributeBoolean' => '布尔',
'Core:AttributeBoolean+' => '布尔',
'Core:AttributeBoolean+' => '',
'Core:AttributeBoolean/Value:null' => '',
'Core:AttributeBoolean/Value:yes' => '是',
'Core:AttributeBoolean/Value:no' => '否',
'Core:AttributeArchiveFlag' => '是否归档',
'Core:AttributeArchiveFlag/Value:yes' => '是',
'Core:AttributeArchiveFlag/Value:yes+' => '此对象仅在归档模式可见',
@@ -66,6 +82,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Core:AttributeArchiveFlag/Label+' => '',
'Core:AttributeArchiveDate/Label' => '归档日期',
'Core:AttributeArchiveDate/Label+' => '',
'Core:AttributeObsolescenceFlag' => '是否废弃',
'Core:AttributeObsolescenceFlag/Value:yes' => '是',
'Core:AttributeObsolescenceFlag/Value:yes+' => 'This object is excluded from the impact analysis, and hidden from search results~~',
@@ -74,38 +91,54 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Core:AttributeObsolescenceFlag/Label+' => 'Computed dynamically on other attributes~~',
'Core:AttributeObsolescenceDate/Label' => '废弃时间',
'Core:AttributeObsolescenceDate/Label+' => 'Approximative date at which the object has been considered obsolete~~',
'Core:AttributeString' => '字符串',
'Core:AttributeString+' => '字符串',
'Core:AttributeClass' => '类',
'Core:AttributeClass+' => '类别',
'Core:AttributeClass+' => '',
'Core:AttributeApplicationLanguage' => '用户语言',
'Core:AttributeApplicationLanguage+' => '语言和国家地区(EN US)',
'Core:AttributeFinalClass' => '类 (auto)',
'Core:AttributeFinalClass+' => 'Real class of the object (automatically created by the core)',
'Core:AttributePassword' => '密码',
'Core:AttributePassword+' => '外部设备的密码',
'Core:AttributeEncryptedString' => '加密字符串',
'Core:AttributeEncryptedString+' => 'String encrypted with a local key~~',
'Core:AttributeEncryptUnknownLibrary' => '未知的加密库 (%1$s)',
'Core:AttributeEncryptFailedToDecrypt' => '** 解密错误 **',
'Core:AttributeText' => '文本',
'Core:AttributeText+' => '多行字符串',
'Core:AttributeHTML' => 'HTML',
'Core:AttributeHTML+' => 'HTML字符串',
'Core:AttributeEmailAddress' => '邮箱地址',
'Core:AttributeEmailAddress+' => '邮箱地址',
'Core:AttributeIPAddress' => 'IP地址',
'Core:AttributeIPAddress+' => 'IP地址',
'Core:AttributeOQL' => 'OQL',
'Core:AttributeOQL+' => 'Object Query Langage expression~~',
'Core:AttributeEnum' => 'Enum~~',
'Core:AttributeEnum+' => 'List of predefined alphanumeric strings~~',
'Core:AttributeTemplateString' => '字符模板',
'Core:AttributeTemplateString+' => '包含占位符的字符串',
'Core:AttributeTemplateText' => '文字模板',
'Core:AttributeTemplateText+' => '包含占位符的文本',
'Core:AttributeTemplateHTML' => 'HTML模板',
'Core:AttributeTemplateHTML+' => 'HTML containing placeholders~~',
'Core:AttributeDateTime' => '日期/时间',
'Core:AttributeDateTime+' => 'Date and time (年-月-日 时:分:秒)',
'Core:AttributeDateTime?SmartSearch' => '
@@ -123,6 +156,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
<p>
如果不写具体时间,则默认00:00:00
</p>',
'Core:AttributeDate' => '日期',
'Core:AttributeDate+' => '日期 (年-月-日)',
'Core:AttributeDate?SmartSearch' => '
@@ -137,30 +171,43 @@ Operators:<br/>
<b>&lt;</b><em>日期</em><br/>
<b>[</b><em>日期</em>,<em>日期</em><b>]</b>
</p>',
'Core:AttributeDeadline' => '截止日期',
'Core:AttributeDeadline+' => '日期, 显示与当前的相对时间',
'Core:AttributeExternalKey' => '外键',
'Core:AttributeExternalKey+' => 'External (or foreign) key~~',
'Core:AttributeHierarchicalKey' => 'Hierarchical Key~~',
'Core:AttributeHierarchicalKey+' => 'External (or foreign) key to the parent~~',
'Core:AttributeExternalField' => '外部字段',
'Core:AttributeExternalField+' => 'Field mapped to an external key~~',
'Core:AttributeURL' => 'URL',
'Core:AttributeURL+' => 'Absolute or relative URL as a text string~~',
'Core:AttributeBlob' => 'Blob',
'Core:AttributeBlob+' => '任何二进制内容(文档)',
'Core:AttributeOneWayPassword' => '单向密码',
'Core:AttributeOneWayPassword+' => '单向加密(或哈希) 的密码',
'Core:AttributeTable' => '表',
'Core:AttributeTable+' => '带索引的二维数组',
'Core:AttributePropertySet' => '属性',
'Core:AttributePropertySet+' => 'List of untyped properties (name and value)~~',
'Core:AttributeFriendlyName' => '通用名称',
'Core:AttributeFriendlyName+' => 'Attribute created automatically ; the friendly name is computed after several attributes~~',
'Core:FriendlyName-Label' => '全称',
'Core:FriendlyName-Description' => '全称',
'Core:AttributeTag' => '标签',
'Core:AttributeTag+' => '标签',
'Core:Context=REST/JSON' => 'REST',
'Core:Context=Synchro' => '同步',
'Core:Context=Setup' => '安装向导',
@@ -201,10 +248,10 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
//
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:CMDBChangeOp' => '变更操作',
'Class:CMDBChangeOp+' => '变更操作跟踪',
'Class:CMDBChangeOp' => '变更操作跟踪',
'Class:CMDBChangeOp+' => '某人在某时某刻对某个对象的变更操作',
'Class:CMDBChangeOp/Attribute:change' => '变更',
'Class:CMDBChangeOp/Attribute:change+' => '变更',
'Class:CMDBChangeOp/Attribute:change+' => '',
'Class:CMDBChangeOp/Attribute:date' => '日期',
'Class:CMDBChangeOp/Attribute:date+' => '变更的日期和时间',
'Class:CMDBChangeOp/Attribute:userinfo' => '用户',
@@ -325,9 +372,9 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:EventNotification' => '通知事件',
'Class:EventNotification+' => 'Trace of a notification that has been sent~~',
'Class:EventNotification/Attribute:trigger_id' => '触发器',
'Class:EventNotification/Attribute:trigger_id+' => '用户账',
'Class:EventNotification/Attribute:trigger_id+' => '用户账',
'Class:EventNotification/Attribute:action_id' => '用户',
'Class:EventNotification/Attribute:action_id+' => '用户账',
'Class:EventNotification/Attribute:action_id+' => '用户账',
'Class:EventNotification/Attribute:object_id' => '对象id',
'Class:EventNotification/Attribute:object_id+' => 'object id (class defined by the trigger ?)~~',
));
@@ -408,8 +455,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:EventRestService/Attribute:version+' => '参数 \'版本\'',
'Class:EventRestService/Attribute:json_input' => '输入',
'Class:EventRestService/Attribute:json_input+' => 'Argument \'json_data\'~~',
'Class:EventRestService/Attribute:code' => '码',
'Class:EventRestService/Attribute:code+' => '返回码',
'Class:EventRestService/Attribute:code' => '码',
'Class:EventRestService/Attribute:code+' => '返回码',
'Class:EventRestService/Attribute:json_output' => '响应',
'Class:EventRestService/Attribute:json_output+' => 'HTTP 响应 (json)',
'Class:EventRestService/Attribute:provider' => '提供者',
@@ -660,7 +707,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:TriggerOnAttributeBlobDownload' => 'Trigger (on object\'s document download)~~',
'Class:TriggerOnAttributeBlobDownload+' => 'Trigger on object\'s document field download of [a child class of] the given class~~',
'Class:TriggerOnAttributeBlobDownload/Attribute:target_attcodes' => 'Target fields~~',
'Class:TriggerOnAttributeBlobDownload/Attribute:target_attcodes' => '目标字段',
'Class:TriggerOnAttributeBlobDownload/Attribute:target_attcodes+' => '',
));
@@ -671,7 +718,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:TriggerOnThresholdReached' => '触发器 (基于阈值)',
'Class:TriggerOnThresholdReached+' => '当达到某个阈值时触发',
'Class:TriggerOnThresholdReached/Attribute:stop_watch_code' => '秒表',
'Class:TriggerOnThresholdReached/Attribute:stop_watch_code' => '计时',
'Class:TriggerOnThresholdReached/Attribute:stop_watch_code+' => '',
'Class:TriggerOnThresholdReached/Attribute:threshold_index' => '阈值',
'Class:TriggerOnThresholdReached/Attribute:threshold_index+' => '',
@@ -768,6 +815,7 @@ The hyperlink is displayed in the tooltip appearing on the “Lock” symbol of
'Class:SynchroDataSource/Attribute:user_delete_policy/Value:administrators' => 'Administrators only',
'Class:SynchroDataSource/Attribute:user_delete_policy/Value:everybody' => 'Everybody allowed to delete such objects',
'Class:SynchroDataSource/Attribute:user_delete_policy/Value:nobody' => 'Nobody',
'SynchroDataSource:Description' => '描述',
'SynchroDataSource:Reconciliation' => 'Search &amp; reconciliation~~',
'SynchroDataSource:Deletion' => 'Deletion rules~~',
@@ -1005,8 +1053,9 @@ The hyperlink is displayed in the tooltip appearing on the “Lock” symbol of
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:TagSetFieldData' => '%2$s for class %1$s~~',
'Class:TagSetFieldData+' => '~~',
'Class:TagSetFieldData/Attribute:code' => '代码',
'Class:TagSetFieldData/Attribute:code+' => '内部代码. 必须至少包含3个数字或字母',
'Class:TagSetFieldData/Attribute:code' => '编码',
'Class:TagSetFieldData/Attribute:code+' => '内部编码. 必须至少包含3个数字或字母',
'Class:TagSetFieldData/Attribute:label' => '标签',
'Class:TagSetFieldData/Attribute:label+' => '显示的标签',
'Class:TagSetFieldData/Attribute:description' => '描述',
@@ -1014,6 +1063,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:TagSetFieldData/Attribute:finalclass' => 'Tag class~~~~',
'Class:TagSetFieldData/Attribute:obj_class' => 'Object class~~~~',
'Class:TagSetFieldData/Attribute:obj_attcode' => 'Field code~~~~',
'Core:TagSetFieldData:ErrorDeleteUsedTag' => '已使用的标签无法删除',
'Core:TagSetFieldData:ErrorDuplicateTagCodeOrLabel' => 'Tags codes or labels must be unique~~',
'Core:TagSetFieldData:ErrorTagCodeSyntax' => 'Tags code must contain between 3 and %1$d alphanumeric characters, starting with a letter.~~',
@@ -1139,6 +1189,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:ResourceSystemMenu' => 'Resource System Menu~~',
'Class:ResourceSystemMenu+' => '',
));
// Additional language entries not present in English dict
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'INTERNAL:JQuery-DatePicker:LangCode' => 'zh-CN'

File diff suppressed because it is too large Load Diff

View File

@@ -383,6 +383,7 @@ return array(
'Combodo\\iTop\\Core\\Email\\EmailFactory' => $baseDir . '/sources/Core/Email/EmailFactory.php',
'Combodo\\iTop\\Core\\Email\\iEMail' => $baseDir . '/sources/Core/Email/iEMail.php',
'Combodo\\iTop\\Core\\EventListener\\AttributeBlobEventListener' => $baseDir . '/sources/Core/EventListener/AttributeBlobEventListener.php',
'Combodo\\iTop\\Core\\Kpi\\KpiLogData' => $baseDir . '/sources/Core/Kpi/KpiLogData.php',
'Combodo\\iTop\\Core\\MetaModel\\FriendlyNameType' => $baseDir . '/sources/Core/MetaModel/FriendlyNameType.php',
'Combodo\\iTop\\Core\\MetaModel\\HierarchicalKey' => $baseDir . '/sources/Core/MetaModel/HierarchicalKey.php',
'Combodo\\iTop\\DesignDocument' => $baseDir . '/core/designdocument.class.inc.php',
@@ -461,6 +462,7 @@ return array(
'Combodo\\iTop\\Service\\Links\\LinkSetModel' => $baseDir . '/sources/Service/Links/LinkSetModel.php',
'Combodo\\iTop\\Service\\Links\\LinkSetRepository' => $baseDir . '/sources/Service/Links/LinkSetRepository.php',
'Combodo\\iTop\\Service\\Links\\LinksBulkDataPostProcessor' => $baseDir . '/sources/Service/Links/LinksBulkDataPostProcessor.php',
'Combodo\\iTop\\Service\\Module\\ModuleService' => $baseDir . '/sources/Service/Module/ModuleService.php',
'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => $baseDir . '/sources/Service/Router/Exception/RouteNotFoundException.php',
'Combodo\\iTop\\Service\\Router\\Exception\\RouterException' => $baseDir . '/sources/Service/Router/Exception/RouterException.php',
'Combodo\\iTop\\Service\\Router\\Router' => $baseDir . '/sources/Service/Router/Router.php',
@@ -2952,6 +2954,7 @@ return array(
'iDBObjectURLMaker' => $baseDir . '/application/applicationcontext.class.inc.php',
'iDisplay' => $baseDir . '/core/dbobject.class.php',
'iFieldRendererMappingsExtension' => $baseDir . '/application/applicationextension.inc.php',
'iKPILoggerExtension' => $baseDir . '/application/applicationextension.inc.php',
'iKeyboardShortcut' => $baseDir . '/sources/Application/UI/Hook/iKeyboardShortcut.php',
'iLogFileNameBuilder' => $baseDir . '/core/log.class.inc.php',
'iLoginExtension' => $baseDir . '/application/applicationextension.inc.php',

View File

@@ -747,6 +747,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Core\\Email\\EmailFactory' => __DIR__ . '/../..' . '/sources/Core/Email/EmailFactory.php',
'Combodo\\iTop\\Core\\Email\\iEMail' => __DIR__ . '/../..' . '/sources/Core/Email/iEMail.php',
'Combodo\\iTop\\Core\\EventListener\\AttributeBlobEventListener' => __DIR__ . '/../..' . '/sources/Core/EventListener/AttributeBlobEventListener.php',
'Combodo\\iTop\\Core\\Kpi\\KpiLogData' => __DIR__ . '/../..' . '/sources/Core/Kpi/KpiLogData.php',
'Combodo\\iTop\\Core\\MetaModel\\FriendlyNameType' => __DIR__ . '/../..' . '/sources/Core/MetaModel/FriendlyNameType.php',
'Combodo\\iTop\\Core\\MetaModel\\HierarchicalKey' => __DIR__ . '/../..' . '/sources/Core/MetaModel/HierarchicalKey.php',
'Combodo\\iTop\\DesignDocument' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php',
@@ -825,6 +826,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Service\\Links\\LinkSetModel' => __DIR__ . '/../..' . '/sources/Service/Links/LinkSetModel.php',
'Combodo\\iTop\\Service\\Links\\LinkSetRepository' => __DIR__ . '/../..' . '/sources/Service/Links/LinkSetRepository.php',
'Combodo\\iTop\\Service\\Links\\LinksBulkDataPostProcessor' => __DIR__ . '/../..' . '/sources/Service/Links/LinksBulkDataPostProcessor.php',
'Combodo\\iTop\\Service\\Module\\ModuleService' => __DIR__ . '/../..' . '/sources/Service/Module/ModuleService.php',
'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => __DIR__ . '/../..' . '/sources/Service/Router/Exception/RouteNotFoundException.php',
'Combodo\\iTop\\Service\\Router\\Exception\\RouterException' => __DIR__ . '/../..' . '/sources/Service/Router/Exception/RouterException.php',
'Combodo\\iTop\\Service\\Router\\Router' => __DIR__ . '/../..' . '/sources/Service/Router/Router.php',
@@ -3316,6 +3318,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'iDBObjectURLMaker' => __DIR__ . '/../..' . '/application/applicationcontext.class.inc.php',
'iDisplay' => __DIR__ . '/../..' . '/core/dbobject.class.php',
'iFieldRendererMappingsExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
'iKPILoggerExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
'iKeyboardShortcut' => __DIR__ . '/../..' . '/sources/Application/UI/Hook/iKeyboardShortcut.php',
'iLogFileNameBuilder' => __DIR__ . '/../..' . '/core/log.class.inc.php',
'iLoginExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',

View File

@@ -1,13 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.webServer>
<security>
<requestFiltering>
<fileExtensions applyToWebDAV="false" allowUnlisted="false"></fileExtensions>
</requestFiltering>
<authorization>
<deny users="*" /> <!-- Denies all users -->
</authorization>
</security>
</system.webServer>
<system.web>
<authorization>
<deny users="*" /> <!-- Denies all users -->
</authorization>
</system.web>
</configuration>

View File

@@ -358,6 +358,7 @@ class DBBackup
$sPortOption = self::GetMysqliCliSingleOption('port', $this->iDBPort);
$sTlsOptions = self::GetMysqlCliTlsOptions($this->oConfig);
$sProtocolOption = self::GetMysqlCliTransportOption($this->sDBHost);
$sMysqlVersion = CMDBSource::GetDBVersion();
$bIsMysqlSupportUtf8mb4 = (version_compare($sMysqlVersion, self::MYSQL_VERSION_WITH_UTF8MB4_IN_PROGRAMS) === -1);
@@ -378,8 +379,8 @@ EOF;
// Note: opt implicitely sets lock-tables... which cancels the benefit of single-transaction!
// skip-lock-tables compensates and allows for writes during a backup
$sCommand = "$sMySQLDump --defaults-extra-file=\"$sMySQLDumpCnfFile\" --opt --skip-lock-tables --default-character-set=".$sMysqldumpCharset." --add-drop-database --single-transaction --host=$sHost $sPortOption --user=$sUser $sTlsOptions --result-file=$sTmpFileName $sDBName $sTables 2>&1";
$sCommandDisplay = "$sMySQLDump --defaults-extra-file=\"$sMySQLDumpCnfFile\" --opt --skip-lock-tables --default-character-set=".$sMysqldumpCharset." --add-drop-database --single-transaction --host=$sHost $sPortOption --user=xxxxx $sTlsOptions --result-file=$sTmpFileName $sDBName $sTables";
$sCommand = "$sMySQLDump --defaults-extra-file=\"$sMySQLDumpCnfFile\" --opt --skip-lock-tables --default-character-set=".$sMysqldumpCharset." --add-drop-database --single-transaction --host=$sHost $sPortOption $sProtocolOption --user=$sUser $sTlsOptions --result-file=$sTmpFileName $sDBName $sTables 2>&1";
$sCommandDisplay = "$sMySQLDump --defaults-extra-file=\"$sMySQLDumpCnfFile\" --opt --skip-lock-tables --default-character-set=".$sMysqldumpCharset." --add-drop-database --single-transaction --host=$sHost $sPortOption $sProtocolOption --user=xxxxx $sTlsOptions --result-file=$sTmpFileName $sDBName $sTables";
// Now run the command for real
$this->LogInfo("backup: generate data file with command: $sCommandDisplay");
@@ -467,8 +468,8 @@ EOF;
if ($oMysqli->connect_errno)
{
$sHost = is_null($this->iDBPort) ? $this->sDBHost : $this->sDBHost.' on port '.$this->iDBPort;
throw new BackupException("Cannot connect to the MySQL server '$sHost' (".$oMysqli->connect_errno.") ".$oMysqli->connect_error);
}
throw new MySQLException('Could not connect to the DB server '.$oMysqli->connect_errno.' (mysql errno: '.$oMysqli->connect_error, array('host' => $sHost, 'user' => $sUser));
}
if (!$oMysqli->select_db($this->sDBName))
{
throw new BackupException("The database '$this->sDBName' does not seem to exist");
@@ -576,6 +577,28 @@ EOF;
return ' --'.$sCliArgName.'='.self::EscapeShellArg($sData);
}
/**
* Define if we should force a transport option
*
* @param string $sHost
*
* @return string .
* @since 2.7.9 3.0.4 3.1.1 N°6123
*/
public static function GetMysqlCliTransportOption(string $sHost)
{
$sTransportOptions = '';
/** N°6123 As we're using a --port option, if we use localhost as host,
* MariaDB > 10.6 will implicitly change its protocol from socket to tcp and throw a warning **/
if($sHost === 'localhost'){
$sTransportOptions = '--protocol=tcp';
}
return $sTransportOptions;
}
/**
* @return string the command to launch mysqldump (without its params)
*/

View File

@@ -2060,7 +2060,6 @@ EOF
$this->CompileCommonProperty('display_style', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('edit_mode', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('filter', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('allowed_values', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('with_php_constraint', $oField, $aParameters, $sModuleRelativeDir, false);
$aParameters['depends_on'] = $sDependencies;
} elseif ($sAttType == 'AttributeLinkedSet') {

View File

@@ -892,7 +892,10 @@ class iTopDesignFormat
$oNodeList = $oXPath->query("/itop_design/classes//class/fields/field/values/value");
foreach ($oNodeList as $oNode) {
$sCode = $oNode->textContent;
$oNode->textContent = '';
// N°6562 textContent is readonly, see https://www.php.net/manual/en/class.domnode.php#95545
// $oNode->textContent = '';
// N°6562 to update text node content we must use the node methods !
$oNode->removeChild($oNode->firstChild);
$oCodeNode = $oNode->ownerDocument->createElement("code", $sCode);
$oNode->appendChild($oCodeNode);
}
@@ -982,7 +985,14 @@ class iTopDesignFormat
if ($oStyleNode) {
$this->DeleteNode($oStyleNode);
}
$oNode->textContent = $sCode;
// N°6562 textContent is readonly, see https://www.php.net/manual/en/class.domnode.php#95545
// $oNode->textContent = $sCode;
// N°6562 to update text node content we must use the node methods !
// we are using DOMDocument::createTextNode instead of new DOMText because elements created using the constructor are read only
// see https://www.php.net/manual/en/domelement.construct.php
$oTextContentNode = $this->oDocument->createTextNode($sCode);
$oNode->appendChild($oTextContentNode);
}
}
// - Style

View File

@@ -55,11 +55,13 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
public const UI_BLOCK_CLASS_NAME = DataTable::class;
/**
* If inside an iTop object, you can use {@see cmdbAbstractObject::DisplaySet()}
*
* @api
* @param \WebPage $oPage
* @param string $sListId
* @param \DBObjectSet $oSet
* @param array $aExtraParams
* @param array $aExtraParams See possible values in {@see self::RenderDataTable()}
*
* @return \Combodo\iTop\Application\UI\Base\Layout\UIContentBlock
* @throws \ApplicationException
@@ -84,11 +86,13 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
}
/**
* If inside an iTop object, you can use {@see cmdbAbstractObject::DisplaySet()}
*
* @api
* @param \WebPage $oPage
* @param string $sListId
* @param DBObjectSet $oSet
* @param array $aExtraParams
* @param array $aExtraParams See possible values in {@see self::RenderDataTable()}
*
* @return \Combodo\iTop\Application\UI\Base\Layout\UIContentBlock
* @throws \ArchivedObjectException
@@ -117,7 +121,12 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
* @param \WebPage $oPage
* @param string $sListId
* @param \DBObjectSet $oSet
* @param array $aExtraParams
* @param array $aExtraParams example keys used in this method :
* - toolkit_menu = boolean
* - surround_with_panel = boolean : if true adds the standard class panel (icon, title, ...)
* - panel_title = string
* - panel_title_is_html = boolean
* - panel_icon = string : class icon (for example from {@see MetaModel::GetClassIcon()})
*
* @return \Combodo\iTop\Application\UI\Base\Layout\UIContentBlock
* @throws \ArchivedObjectException

View File

@@ -74,6 +74,7 @@ class OAuthClientProviderFactory
* @return AccessTokenInterface
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \League\OAuth2\Client\Provider\Exception\IdentityProviderException
*/
public static function GetAccessTokenFromCode(OAuthClient $oOAuthClient, $sCode)
{
@@ -109,7 +110,7 @@ class OAuthClientProviderFactory
/**
* @param \DBObject $oOAuthClient
*
* @return mixed
* @return OAuthClientProviderAbstract
* @throws \ArchivedObjectException
* @throws \CoreException
*/

View File

@@ -0,0 +1,125 @@
<?php
/**
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Core\Kpi;
class KpiLogData
{
const TYPE_REPORT = 'report';
const TYPE_STATS = 'stats';
const TYPE_REQUEST = 'request';
/** @var string */
public $sType;
/** @var string */
public $sOperation;
/** @var string */
public $sArguments;
/** @var float */
public $fStartTime;
/** @var float */
public $fStopTime;
/** @var string */
public $sExtension;
/** @var int */
public $iInitialMemory;
/** @var int */
public $iCurrentMemory;
/** @var int */
public $iPeakMemory;
/** @var array */
public $aData;
/**
* @param string $sType
* @param string $sOperation
* @param string $sArguments
* @param float $fStartTime
* @param float $fStopTime
* @param string $sExtension
* @param int $iInitialMemory
* @param int $iCurrentMemory
* @param array $aData
*/
public function __construct($sType, $sOperation, $sArguments, $fStartTime, $fStopTime, $sExtension, $iInitialMemory = 0, $iCurrentMemory = 0, $iPeakMemory = 0, $aData = [])
{
$this->sType = $sType;
$this->sOperation = $sOperation;
$this->sArguments = @iconv(mb_detect_encoding($sArguments, mb_detect_order(), true), 'UTF-8', $sArguments);
$this->fStartTime = $fStartTime;
$this->fStopTime = $fStopTime;
$this->sExtension = $sExtension;
$this->iInitialMemory = $iInitialMemory;
$this->iCurrentMemory = $iCurrentMemory;
$this->iPeakMemory = $iPeakMemory;
$this->aData = $aData;
}
/**
* Return the CSV Header
*
* @return string
*/
public static function GetCSVHeader()
{
return "Type,Operation,Arguments,StartTime,StopTime,Duration,Extension,InitialMemory,CurrentMemory,PeakMemory";
}
/**
* Return the CSV line for the values
* @return string
*/
public function GetCSV()
{
$fDuration = sprintf('%01.4f', $this->fStopTime - $this->fStartTime);
$sType = $this->RemoveQuotes($this->sType);
$sOperation = $this->RemoveQuotes($this->sOperation);
$sArguments = $this->RemoveQuotes($this->sArguments);
$sExtension = $this->RemoveQuotes($this->sExtension);
return "\"$sType\",\"$sOperation\",\"$sArguments\",$this->fStartTime,$this->fStopTime,$fDuration,\"$sExtension\",$this->iInitialMemory,$this->iCurrentMemory,$this->iPeakMemory";
}
private function RemoveQuotes(string $sEntry): string
{
return str_replace('"', "'", $sEntry);
}
/**
* @param \Combodo\iTop\Core\Kpi\KpiLogData $oOther
*
* @return float
*/
public function Compare(KpiLogData $oOther): float
{
if ($oOther->fStartTime > $this->fStartTime) {
return -1;
}
return 1;
}
public function Contains(KpiLogData $oOther): bool
{
if ($oOther->fStartTime < $this->fStartTime) {
return false;
}
if ($oOther->fStartTime > $this->fStopTime) {
return false;
}
return true;
}
public function __toString()
{
return "$this->sType:$this->sOperation:$this->sArguments";
}
public function GetUUID(): string
{
return sha1($this->__toString());
}
}

View File

@@ -320,12 +320,19 @@ EOF
if ($this->oField->GetCurrentValue() !== null && $this->oField->GetCurrentValue() !== 0 && $this->oField->GetCurrentValue() !== '')
{
// Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated
$oFieldValue = MetaModel::GetObject($sFieldValueClass, $this->oField->GetCurrentValue(), true, true);
$oFieldValue = MetaModel::GetObjectWithArchive($sFieldValueClass, $this->oField->GetCurrentValue(), true, true);
$sFieldHtmlValue = $oFieldValue->GetName();
$sFieldUrl = ApplicationContext::MakeObjectUrl($sFieldValueClass, $this->oField->GetCurrentValue());
if(!empty($sFieldUrl))
if($oFieldValue->IsArchived())
{
$sFieldHtmlValue = '<a href="'.$sFieldUrl.'" data-toggle="itop-portal-modal">'.$sFieldHtmlValue.'</a>';
$sFieldHtmlValue = '<span class="text_decoration"><span class="fas fa-archive"></span></span>' . $sFieldHtmlValue;
}
else
{
$sFieldUrl = ApplicationContext::MakeObjectUrl($sFieldValueClass, $this->oField->GetCurrentValue());
if (!empty($sFieldUrl))
{
$sFieldHtmlValue = '<a href="' . $sFieldUrl . '" data-toggle="itop-portal-modal">' . $sFieldHtmlValue . '</a>';
}
}
}
else

View File

@@ -0,0 +1,214 @@
<?php
/**
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Service\Module;
use MetaModel;
use ReflectionClass;
use ReflectionMethod;
use utils;
class ModuleService
{
/** @var ModuleService */
private static $oInstance;
private function __construct()
{
}
public static function GetInstance(): ModuleService
{
if (!isset(static::$oInstance)) {
static::$oInstance = new ModuleService();
}
return static::$oInstance;
}
/**
* Get a "signature" of the method of an extension in the form of: "[module-name] class::method()"
*
* @param object|string $object Object or class
* @param string $sMethod
*
* @return string
* @throws \ReflectionException
*/
public function GetModuleMethodSignature($object, string $sMethod): string
{
$sSignature = '';
$oReflectionMethod = new ReflectionMethod($object, $sMethod);
$oReflectionClass = $oReflectionMethod->getDeclaringClass();
$sExtension = $this->GetModuleNameFromObject($oReflectionClass->getName());
if (strlen($sExtension) !== 0) {
$sSignature .= '['.$sExtension.'] ';
}
$sSignature .= $oReflectionClass->getShortName().'::'.$sMethod.'()';
return $sSignature;
}
/**
* Get the module name from an object or class
*
* @param object|string $object
*
* @return string
* @throws \ReflectionException
*/
public function GetModuleNameFromObject($object): string
{
$oReflectionClass = new ReflectionClass($object);
$sPath = str_replace('\\', '/', $oReflectionClass->getFileName());
$sPattern = str_replace('\\', '/', '@'.APPROOT.'env-'.utils::GetCurrentEnvironment()).'/(?<ext>.+)/@U';
if (preg_match($sPattern, $sPath, $aMatches) !== false) {
if (isset($aMatches['ext'])) {
return $aMatches['ext'];
}
}
return '';
}
/**
* **Warning** : returned result can be invalid as we're using backtrace to find the module dir name
*
* @param int $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module
*
* @return string the relative (to MODULESROOT) path of the root directory of the module containing the file where the call to
* this function is made
* or an empty string if no such module is found (or not called within a module file)
*
* @uses \debug_backtrace()
*/
public function GetCurrentModuleDir(int $iCallDepth): string
{
$sCurrentModuleDir = '';
$aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$sCallerFile = realpath($aCallStack[$iCallDepth]['file']);
foreach(GetModulesInfo() as $sModuleName => $aInfo)
{
if ($aInfo['root_dir'] !== '')
{
$sRootDir = realpath(APPROOT.$aInfo['root_dir']);
if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir)
{
$sCurrentModuleDir = basename($sRootDir);
break;
}
}
}
return $sCurrentModuleDir;
}
/**
* **Warning** : as this method uses {@see GetCurrentModuleDir} it produces hazardous results.
* You should better uses directly {@see GetAbsoluteUrlModulesRoot} and add the module dir name yourself ! See N°4573
*
* @return string the base URL for all files in the current module from which this method is called
* or an empty string if no such module is found (or not called within a module file)
* @throws \Exception
*
* @uses GetCurrentModuleDir
*/
public function GetCurrentModuleUrl(int $iCallDepth = 0): string
{
$sDir = $this->GetCurrentModuleDir(1 + $iCallDepth);
if ( $sDir !== '')
{
return utils::GetAbsoluteUrlModulesRoot().'/'.$sDir;
}
return '';
}
/**
* @param string $sProperty The name of the property to retrieve
* @param mixed $defaultValue
*
* @return mixed the value of a given setting for the current module
*/
public function GetCurrentModuleSetting(string $sProperty, $defaultValue = null)
{
$sModuleName = $this->GetCurrentModuleName(1);
return MetaModel::GetModuleSetting($sModuleName, $sProperty, $defaultValue);
}
/**
* @param string $sModuleName
*
* @return string|NULL compiled version of a given module, as it was seen by the compiler
*/
public function GetCompiledModuleVersion(string $sModuleName): ?string
{
$aModulesInfo = GetModulesInfo();
if (array_key_exists($sModuleName, $aModulesInfo))
{
return $aModulesInfo[$sModuleName]['version'];
}
return null;
}
/**
* Returns the name of the module containing the file where the call to this function is made
* or an empty string if no such module is found (or not called within a module file)
*
* @param int $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module
*
* @return string
*/
public function GetCurrentModuleName(int $iCallDepth = 0): string
{
$sCurrentModuleName = '';
$aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$sCallerFile = realpath($aCallStack[$iCallDepth]['file']);
return $this->GetModuleNameFromPath($sCallerFile);
}
private function GetModuleNameFromPath($sPath)
{
foreach (GetModulesInfo() as $sModuleName => $aInfo) {
if ($aInfo['root_dir'] !== '') {
$sRootDir = realpath(APPROOT.$aInfo['root_dir']);
if (substr($sPath, 0, strlen($sRootDir)) === $sRootDir) {
return $sModuleName;
}
}
}
return '';
}
/**
* Get the extension code from the call stack.
* Scan the call stack until a module is found.
*
* @param int $iLevelsToIgnore
*
* @return string module name
*/
public function GetModuleNameFromCallStack(int $iLevelsToIgnore = 0): string
{
$aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$aCallStack = array_slice($aCallStack, $iLevelsToIgnore);
foreach ($aCallStack as $aCallInfo) {
$sFile = realpath(empty($aCallInfo['file']) ? '' : $aCallInfo['file']);
$sModuleName = $this->GetModuleNameFromPath($sFile);
if (strlen($sModuleName) > 0) {
return $sModuleName;
}
}
return '';
}
}

View File

@@ -138,12 +138,24 @@ class Router
{
$aRoutes = [];
$bUseCache = false === utils::IsDevelopmentEnvironment();
$bMustWriteCache = false;
$sCacheFilePath = $this->GetCacheFileAbsPath();
// Try to read from cache
if ($bUseCache) {
if (is_file($sCacheFilePath)) {
$aRoutes = include $sCacheFilePath;
$aCachedRoutes = include $sCacheFilePath;
// N°6618 - Protection against corrupted cache returning `1` instead of an array of routes
if (is_array($aCachedRoutes)) {
$aRoutes = $aCachedRoutes;
} else {
// Invalid cache force re-generation
// Note that even if it is re-generated corrupted again, this protection should prevent crashes
$bMustWriteCache = true;
}
} else {
$bMustWriteCache = true;
}
}
@@ -180,11 +192,11 @@ class Router
}
}
// Save to cache
if ($bUseCache) {
// Save to cache if it doesn't exist already
if ($bMustWriteCache) {
$sCacheContent = "<?php\n\nreturn ".var_export($aRoutes, true).";";
SetupUtils::builddir(dirname($sCacheFilePath));
file_put_contents($sCacheFilePath, $sCacheContent);
file_put_contents($sCacheFilePath, $sCacheContent, LOCK_EX);
}
return $aRoutes;

View File

@@ -2162,7 +2162,9 @@ class SynchroReplica extends DBObject implements iDisplay
// it will be deleted by the mean of a trigger too
protected function DBDeleteSingleObject()
{
$oKPI = new ExecutionKPI();
$this->OnDelete();
$oKPI->ComputeStatsForExtension($this, 'OnDelete');
if (!MetaModel::DBIsReadOnly())
{

View File

@@ -1,7 +1,118 @@
# PHP unitary tests
## Where should I add my test?
- Covers an iTop PHP class or method?
- Most likely in "unitary-tests".
- Covers the consistency of some data through the app?
- Most likely in "integration-tests".
- Most likely in "integration-tests".
## How do I make sure that my tests are efficient?
### Derive from the relevant test class
Whenever possible keep it the most simple, hence you should first
attempt to derive from `TestCase`.
Then, you might need to derive from `ItopTestCase`.
Finally, as a last resort, you will use `ItopDataTestCase`.
### Determine the most relevant isolation configuration
Should you have opted for `ItopDataTestCase`, then you will have to follow these steps:
1) Build you test class until it is successfull, without process isolation.
2) Run the whole test suite [unitary-tests](unitary-tests)
3) If a false-positive appears, then you will start troubleshooting. One advise: be positive!
### Leave the place clean
To check your code against polluting coding patterns, run the test [integration-tests/DetectStaticPollutionTest.php](integration-tests/DetectStaticPollutionTest.php)
It will tell you if something is wrong, either in your code, or anywhere else in the tests.
Fortunately, it will give you an alternative.
Detected patterns:
* ContextTag::addTag()
* EventService::RegisterListener()
* Dict::Add()
By the way, some patterns do not pollute, because they are handled by the test framework:
* Configuration : automatically reset after test class execution
* UserRights : a logoff is performed after each test execution
* Dict::SetUserLanguage: the user language is reset after each test execution
See also `@beforeClass` and `@afterClass` to handle cleanup.
If you can't, then ok you will have to isolate it!
## Tips
### Memory limit
As the tests are run in the same process, memory usage
may become an issue as soon as tests are all executed at once.
Fix that in the XML configuration in the PHP section
```xml
<ini name="memory_limit" value="512M"/>
```
### Understand tests interactions
With PHPStorm, select two tests, right click to get the context menu, then `run`.
You will have both tests executed and you will be able to figure out if the first one has an impact on the second one.
### About process isolation
#### Isolation with PHPUnit
By default, tests are run in a single process launched by PHPUnit.
If process isolation is configured for some tests, then those tests
will be executed in a separate process. The main process will
continue executing non isolated tests.
#### Cost of isolation
The cost of isolating a very basic `TestCase` is approximately 4 ms.
The cost of isolating an `ItopDataTestCase` is approximately 800 ms.
### Isolation within iTop
#### At the test level (preferred)
Add annotation `@runInSeparateProcess`
Each and every test case will run in a separate
process.
#### At the test class level
Add annotation `@runTestsInSeparateProcesses`
Each and every test case in the class will run in a separate
process.
#### Globally (never do that)
Set it into [phpunit.xml.dist](phpunit.xml.dist)
### Further enhancements
The annotation [`@runClassInSeparateProcess`](https://docs.phpunit.de/en/10.0/attributes.html?highlight=runclassinseparateprocess#runclassinseparateprocess) is supposed to do the perfect job, but it is buggy [(See Issue 5230)](https://github.com/sebastianbergmann/phpunit/issues/5230) and it has
the exact same effect as `@runTestsInSeparateProcesses`.
Note : this option is documented only in the [attributes part of the documentation](https://docs.phpunit.de/en/10.0/attributes.html).
### Traps
#### When it is a matter of stars
```php
/*
* @runTestsInSeparateProcesses
```
This won't work because the comment MUST start with `/**` (two stars) to be considerer by PHPUnit.
#### SetupBeforeClass called more often than expected
`setupBeforeClass` is called once for the class **in a given process**.
Therefore, if the tests are isolated, then `setupBeforeClass` will be called as often as `setUp`.
This has been proven with [`runClassInSeparateProcessTest.php`](experiments/runClassInSeparateProcessTest.php)

View File

@@ -2,5 +2,12 @@
"require-dev": {
"phpunit/phpunit" : "^9",
"sempro/phpunit-pretty-print": "^1.4"
},
"autoload": {
"psr-4": {
"Combodo\\iTop\\Test\\UnitTest\\": "src/BaseTestCase/",
"Combodo\\iTop\\Test\\UnitTest\\Hook\\": "src/Hook/",
"Combodo\\iTop\\Test\\UnitTest\\Service\\": "src/Service/"
}
}
}

View File

@@ -0,0 +1 @@
This directory aims at providing experimental proof of the mechanics of PHPUnit

View File

@@ -0,0 +1,64 @@
<?php
namespace Combodo\iTop\Test\UnitTest;
/**
* Shows that
* 1) the option runClassInSeparateProcess is equivalent to runTestsInSeparateProcesses
* 2) setUpBeforeClass is called within each spawned process (the main one, then in eventuel subprocesses)
* 3) setUp behaves as expected, i.e. called one within the same process as the test itself
*
* @preserveGlobalState disabled
* @runClassInSeparateProcess
*/
class runClassInSeparateProcessTest extends ItopDataTestCase
{
static public function setUpBeforeClass(): void
{
parent::setUpBeforeClass(); // TODO: Change the autogenerated stub
file_put_contents(
dirname(__FILE__).'/pid.txt',
getmypid().';'.static::class.';'.__METHOD__."\n",
FILE_APPEND);
}
protected function LogPid()
{
file_put_contents(
dirname(__FILE__).'/pid.txt',
getmypid().';'.static::class.';'.$this->getName()."\n",
FILE_APPEND);
}
function testA()
{
$this->LogPid();
static::assertTrue(true);
}
function testB()
{
$this->LogPid();
static::assertTrue(true);
}
/**
* @dataProvider CProvider
*/
function testC($i)
{
$this->LogPid();
static::assertTrue(true);
}
function CProvider()
{
return [
[1],
[1],
[1],
[1],
];
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Combodo\iTop\Test\UnitTest;
use PHPUnit\Framework\TestCase;
/**
* Shows that tearDown is called after a fatal error within a test
*/
class tearDownAfterFailureTest extends TestCase
{
static $bIsCorrectlyInitialized = true;
protected function tearDown(): void
{
parent::tearDown();
static::$bIsCorrectlyInitialized = true;
}
function testIsInitializedAndChangeIt()
{
static::assertTrue(static::$bIsCorrectlyInitialized);
static::$bIsCorrectlyInitialized = false;
$this->expectException('Exception');
throw new \Exception('hello');
}
function testIsStillInitialized()
{
static::assertTrue(static::$bIsCorrectlyInitialized);
}
function testFailingDueToUnexpectedException()
{
static::$bIsCorrectlyInitialized = false;
This_Is_Not_A_Function_And_Causes_A_Fatal_Error();
}
function testIsStillInitializedAnyway()
{
static::assertTrue(static::$bIsCorrectlyInitialized);
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* Copyright (C) 2013-2023 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\Test\UnitTest\Integration;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use Dict;
use const APPROOT;
/**
* As {@see DictionariesConsistencyTest}, we are testing dict files, but the ones that are compiled, so we cannot be in the beforeSetup group !
*/
class CompiledDictionariesConsistencyTest extends ItopTestCase
{
/**
* make sure N°5305 dictionary changes (CSV import ergonomy) are still here and UI remains unbroken for any lang
*
* One of the things checked is the number of parameters in the dict value. This is for now crashing the app (N°5491)
* and we have multiple inconsistencies in our existing dict files... So it is complicated to have a generic test for all files !
* At least we are protecting those new entries...
*/
public function testImportCsvMessageStillOk()
{
$aFailedLabels = [];
$aLabelsToTest = [
'UI:CSVReport-Value-SetIssue' => [],
'UI:CSVReport-Value-ChangeIssue' => ['arg1'],
'UI:CSVReport-Value-NoMatch' => ['arg1'],
'UI:CSVReport-Value-NoMatch-PossibleValues' => ['arg1', 'arg2'],
'UI:CSVReport-Value-NoMatch-NoObject' => ['arg1'],
'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => ['arg1'],
'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => ['arg1'],
];
$sCompiledLanguagesFilePath = APPROOT . 'env-' . \utils::GetCurrentEnvironment() . '/dictionaries/languages.php';
$this->assertFileExists($sCompiledLanguagesFilePath, 'We must have an existing compiled language.php file in the current env !');
require_once($sCompiledLanguagesFilePath);
$this->assertNotEmpty(Dict::GetLanguages(), 'the languages.php file exists but didn\'t load any language');
foreach (glob(APPROOT . 'env-' . \utils::GetCurrentEnvironment() . '/dictionaries/*.dict.php') as $sDictFile) {
if (preg_match('/.*\\/(.*).dict.php/', $sDictFile, $aMatches)) {
$sLangCode = $aMatches[1];
$sLanguageCode = strtoupper(str_replace('-', ' ', $sLangCode));
Dict::SetUserLanguage($sLanguageCode);
foreach ($aLabelsToTest as $sLabelKey => $aLabelArgs) {
echo "Testing $sDictFile, label $sLabelKey with " . \var_export($aLabelArgs, true) . "\n";
try {
$sLabelValue = Dict::Format($sLabelKey, ...$aLabelArgs);
//$this->debug($sLabelValue);
} catch (\ValueError $e) {
$aFailedLabels[] = $sLabelKey;
$this->debug([
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'label_name' => $sLabelKey,
'label_args' => $aLabelArgs,
]);
}
}
$this->assertEquals([], $aFailedLabels, "$sDictFile : test fail for lang $sLangCode and labels (" . implode(", ", $aFailedLabels) . ')');
}
}
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Combodo\iTop\Test\UnitTest;
use PHPUnit\Framework\TestCase;
use GlobIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RecursiveRegexIterator;
use RegexIterator;
/**
* Performs code static analysis to detect patterns that will change the values of static data and therefor could affect other tests while running them in a single process
*
* @runClassInSeparateProcess
* @preserveGlobalState disabled
*/
class detectStaticPollutionTest extends TestCase
{
protected function FindMatches($sFile, $sFileContents, $sRegexp)
{
$aRes = [];
foreach (explode("\n", $sFileContents) as $iLine => $sLine) {
if (preg_match_all($sRegexp, $sLine, $aMatches, PREG_PATTERN_ORDER)) {
$sLine = $iLine + 1;
$aRes[] = "$sFile:$sLine";
}
}
return $aRes;
}
/**
* @dataProvider PollutingPatterns
* @param $sPattern
*
* @return void
*/
function testDetectPolluters($sPattern, $sFix)
{
$sScannedDir = dirname(__FILE__).'/../unitary-tests';
$aPolluters = [];
$oDirectory = new RecursiveDirectoryIterator($sScannedDir);
$Iterator = new RecursiveIteratorIterator($oDirectory);
foreach (new RegexIterator($Iterator, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH) as $aMatch) {
$sFile = $aMatch[0];
if(is_file($sFile)) {
$sFileContents = file_get_contents($sFile);
if (preg_match_all($sPattern, $sFileContents, $keys, PREG_PATTERN_ORDER)) {
$aPolluters = array_merge($aPolluters, $this->FindMatches($sFile, $sFileContents, $sPattern));
}
}
}
$iPolluters = count($aPolluters);
static::assertTrue($iPolluters === 0, "Found polluter(s) for pattern $sPattern, $sFix:\n".implode("\n", $aPolluters));
}
public function PollutingPatterns()
{
return [
'ContextTags' => ['/ContextTag::AddContext/i', 'Use new ContextTag() instead'],
'Dict::Add' => ['/Dict::Add/i', 'TODO: implement a facade into ItopDataTestCase'],
'EventService::RegisterListener' => ['/EventService::RegisterListener/i', 'Use ItopDataTestCase::EventService_RegisterListener instead'],
];
}
}

View File

@@ -16,9 +16,9 @@
namespace Combodo\iTop\Test\UnitTest\Integration;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use Dict;
/**
* For tests on compiled dict files, see {@see CompiledDictionariesConsistencyTest}
* @group beforeSetup
*/
class DictionariesConsistencyTest extends ItopTestCase
@@ -32,6 +32,8 @@ class DictionariesConsistencyTest extends ItopTestCase
*/
public function testDictionariesLanguage($sDictFile): void
{
// In iTop the language available list is dynamically made during setup, depending on the dict files found
// Here we are using a fixed list
$aPrefixToLanguageData = array(
'cs' => array('CS CZ', 'Czech', 'Čeština'),
'da' => array('DA DA', 'Danish', 'Dansk'),
@@ -96,10 +98,12 @@ class DictionariesConsistencyTest extends ItopTestCase
{
$this->setUp();
$sAppRoot = $this->GetAppRoot();
$aDictFiles = array_merge(
glob(APPROOT.'datamodels/2.x/*/*.dict*.php'), // legacy form in modules
glob(APPROOT.'datamodels/2.x/*/dictionaries/*.dict*.php'), // modern form in modules
glob(APPROOT.'dictionaries/*.dict*.php') // framework
glob($sAppRoot.'datamodels/2.x/*/*.dict*.php'), // legacy form in modules
glob($sAppRoot.'datamodels/2.x/*/dictionaries/*.dict*.php'), // modern form in modules
glob($sAppRoot.'dictionaries/*.dict*.php') // framework
);
$aTestCases = array();
foreach ($aDictFiles as $sDictFile) {
@@ -150,67 +154,4 @@ class DictionariesConsistencyTest extends ItopTestCase
$sMessage = "File `{$sDictFile}` syntax didn't matched expectations\nparsing results=".var_export($output, true);
self::assertEquals($bIsSyntaxValid, $bDictFileSyntaxOk, $sMessage);
}
/**
* @dataProvider ImportCsvMessageStillOkProvider
* make sure N°5305 dictionary changes are still here and UI remains unbroken for any lang
*/
public function testImportCsvMessageStillOk($sLangCode, $sDictFile)
{
$aFailedLabels = [];
$aLabelsToTest = [
'UI:CSVReport-Value-SetIssue' => [],
'UI:CSVReport-Value-ChangeIssue' => [ 'arg1' ],
'UI:CSVReport-Value-NoMatch' => [ 'arg1' ],
'UI:CSVReport-Value-NoMatch-PossibleValues' => [ 'arg1', 'arg2' ],
'UI:CSVReport-Value-NoMatch-NoObject' => [ 'arg1' ],
'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => [ 'arg1' ],
'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => [ 'arg1' ],
];
$sLanguageCode = strtoupper(str_replace('-', ' ', $sLangCode));
require_once(APPROOT.'env-'.\utils::GetCurrentEnvironment().'/dictionaries/languages.php');
Dict::SetUserLanguage($sLanguageCode);
foreach ($aLabelsToTest as $sLabelKey => $aLabelArgs){
try{
$sLabelValue = Dict::Format($sLabelKey, ...$aLabelArgs);
//$this->debug($sLabelValue);
} catch (\ValueError $e){
$aFailedLabels[] = $sLabelKey;
$this->debug([
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'label_name' => $sLabelKey,
'label_args' =>$aLabelArgs,
]);
}
}
$this->assertEquals([], $aFailedLabels, "test fail for lang $sLangCode and labels (" . implode(", ", $aFailedLabels) . ')');
}
public function ImportCsvMessageStillOkProvider(){
return $this->GetDictFiles();
}
/**
* return a map linked to *.dict.php files that are generated after setup
* each entry key is lang code (example 'en')
* each value is an array with lang code (again) and dict file path
* @return array
*/
private function GetDictFiles() : array {
$aDictFiles = [];
foreach (glob(APPROOT.'env-'.\utils::GetCurrentEnvironment().'/dictionaries/*.dict.php') as $sDictFile){
if (preg_match('/.*\\/(.*).dict.php/', $sDictFile, $aMatches)){
$sLangCode = $aMatches[1];
$aDictFiles[$sLangCode] = [
'lang' => $sLangCode,
'file' => $sDictFile
];
}
}
return $aDictFiles;
}
}

View File

@@ -22,6 +22,7 @@ use utils;
/**
* @package Combodo\iTop\Test\UnitTest\Setup
* @group beforeSetup
*/
class iTopModulesPhpVersionIntegrationTest extends ItopTestCase {
/**

View File

@@ -24,6 +24,7 @@ use iTopDesignFormat;
* @covers iTopDesignFormat
*
* @package Combodo\iTop\Test\UnitTest\Setup
* @group beforeSetup
*/
class iTopModulesXmlVersionIntegrationTest extends ItopTestCase
{
@@ -71,11 +72,13 @@ class iTopModulesXmlVersionIntegrationTest extends ItopTestCase
{
static::setUp();
$sPath = APPROOT.'datamodels/2.x/*/datamodel.*.xml';
$sAppRoot = $this->GetAppRoot();
$sPath = $sAppRoot.'datamodels/2.x/*/datamodel.*.xml';
$aXmlFiles = glob($sPath);
$aXmlFiles[] = APPROOT.'core/datamodel.core.xml';
$aXmlFiles[] = APPROOT.'application/datamodel.application.xml';
$aXmlFiles[] = $sAppRoot.'core/datamodel.core.xml';
$aXmlFiles[] = $sAppRoot.'application/datamodel.application.xml';
$aTestCases = array();
foreach ($aXmlFiles as $sXmlFile) {

View File

@@ -20,6 +20,7 @@ use Combodo\iTop\Test\UnitTest\ItopTestCase;
/**
* @package Combodo\iTop\Test\UnitTest\Setup
* @group beforeSetup
*/
class iTopXmlVersionIntegrationTest extends ItopTestCase
{

View File

@@ -19,7 +19,12 @@
printerClass="\Sempro\PHPUnitPrettyPrinter\PrettyPrinterForPhpUnit9"
>
<extensions>
<extension class="Combodo\iTop\Test\UnitTest\Hook\TestsRunStartHook" />
</extensions>
<php>
<ini name="memory_limit" value="512M"/>
<ini name="error_reporting" value="E_ALL"/>
<ini name="display_errors" value="On"/>
<ini name="log_errors" value="On"/>

View File

@@ -19,6 +19,10 @@
printerClass="\Sempro\PHPUnitPrettyPrinter\PrettyPrinterForPhpUnit9"
>
<extensions>
<extension class="Combodo\iTop\Test\UnitTest\Hook\TestsRunStartHook" />
</extensions>
<php>
<ini name="error_reporting" value="E_ALL"/>
<ini name="display_errors" value="On"/>

View File

@@ -0,0 +1,200 @@
<?php
/*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest;
use CMDBSource;
use Combodo\iTop\Test\UnitTest\Hook\TestsRunStartHook;
use Combodo\iTop\Test\UnitTest\Service\UnitTestRunTimeEnvironment;
use Config;
use Exception;
use IssueLog;
use MetaModel;
use SetupUtils;
use utils;
/**
* Class ItopCustomDatamodelTestCase
*
* Helper class to extend for tests needing a custom DataModel (eg. classes, attributes, etc conditions not available in the standard DM)
* Usage:
* - Create a test case class extending this one
* - Override the {@see ItopCustomDatamodelTestCase::GetDatamodelDeltaAbsPath()} method to define where you XML delta is
* - Implement your test case methods as usual
*
* @since N°6097 2.7.9 3.0.4 3.1.0
*/
abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
{
/**
* @var bool[]
*/
protected static $aReadyCustomEnvironments = [];
/**
* @inheritDoc
* @since N°6097 Workaround to make the "runClassInSeparateProcess" directive work
*/
public function __construct($name = null, array $data = [], $dataName = '')
{
parent::__construct($name, $data, $dataName);
// Ensure that a test class derived from this one runs in a dedicated process as it changes the MetaModel / environment on the fly and
// for now we have no way of switching environments properly in memory and it will result in other (regular) test classes to fail as they won't be on the expected environment.
//
// If we don't do this, we would have to add the `@runTestsInSeparateProcesses` on *each* test classes which we want to avoid for obvious possible mistakes.
// Note that the `@runClassInSeparateProcess` don't work in PHPUnit yet.
$this->setRunClassInSeparateProcess(true);
}
/**
* @return string Abs path to the XML delta to use for the tests of that class
*/
abstract public function GetDatamodelDeltaAbsPath(): string;
/**
* @inheritDoc
*/
protected function LoadRequiredItopFiles(): void
{
parent::LoadRequiredItopFiles();
$this->RequireOnceItopFile('setup/setuputils.class.inc.php');
$this->RequireOnceItopFile('setup/runtimeenv.class.inc.php');
}
/**
* @return string Environment used as a base (conf. file, modules, DB, ...) to prepare the test environment
*/
protected function GetSourceEnvironment(): string
{
return 'production';
}
/**
* @inheritDoc
* @warning This should ONLY be overloaded if your test case XML deltas are NOT compatible with the others, as it will create / compile another environment, increasing the global testing time.
*/
public function GetTestEnvironment(): string
{
return 'php-unit-tests';
}
/**
* @return string Absolute path to the {@see \Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase::GetTestEnvironment()} folder
*/
final private function GetTestEnvironmentFolderAbsPath(): string
{
return APPROOT.'env-'.$this->GetTestEnvironment().'/';
}
/**
* Mark {@see \Combodo\iTop\Test\UnitTest\ItopDataTestCase::GetTestEnvironment()} as ready (compiled)
*
* @return void
*/
final private function MarkEnvironmentReady(): void
{
if (false === $this->IsEnvironmentReady()) {
touch(static::GetTestEnvironmentFolderAbsPath());
}
}
/**
* @return bool True if the {@see \Combodo\iTop\Test\UnitTest\ItopDataTestCase::GetTestEnvironment()} is ready (compiled, but not started)
*
* @details Having the environment ready means that it has been compiled for this global tests run, not that it is a relic from a previous global tests run
*/
final private function IsEnvironmentReady(): bool
{
// As these test cases run in separate processes, the best way we found to let know a process if its environment was already prepared for **this run** was to compare the modification times of:
// - its own env-<ENV> folder
// - a file generated at the beginning of the global test run {@see \Combodo\iTop\Test\UnitTest\Hook\TestsRunStartHook}
$sRunStartedFilePath = TestsRunStartHook::GetRunStartedFileAbsPath();
$sEnvFolderPath = static::GetTestEnvironmentFolderAbsPath();
clearstatcache();
if (false === file_exists($sRunStartedFilePath) || false === file_exists($sEnvFolderPath)) {
return false;
}
$iRunStartedFileModificationTime = filemtime($sRunStartedFilePath);
$iEnvFolderModificationTime = filemtime($sEnvFolderPath);
return $iEnvFolderModificationTime >= $iRunStartedFileModificationTime;
}
/**
* @inheritDoc
*/
protected function PrepareEnvironment(): void
{
$sSourceEnv = $this->GetSourceEnvironment();
$sTestEnv = $this->GetTestEnvironment();
// Check if test env. is already set and only prepare it if it's not up-to-date
//
// Note: To improve performances, we compile all XML deltas from test cases derived from this class and make a single environment where everything will be ran at once.
// This requires XML deltas to be compatible, but it is a known and accepted trade-off. See PR #457
if (false === $this->IsEnvironmentReady()) {
//----------------------------------------------------
// Clear any previous "$sTestEnv" environment
//----------------------------------------------------
// - Configuration file
$sConfFile = utils::GetConfigFilePath($sTestEnv);
$sConfFolder = dirname($sConfFile);
if (is_file($sConfFile)) {
chmod($sConfFile, 0777);
SetupUtils::tidydir($sConfFolder);
}
// - Datamodel delta files
// - Cache folder
// - Compiled folder
// We don't need to clean them as they are already by the compilation
// - Drop database
// We don't do that now, it will be done before re-creating the DB, once the metamodel is started
//----------------------------------------------------
// Prepare "$sTestEnv" environment
//----------------------------------------------------
// All the following is greatly inspired by the toolkit's sandbox script
// - Prepare config file
$oSourceConf = new Config(utils::GetConfigFilePath($sSourceEnv));
if ($oSourceConf->Get('source_dir') === '') {
throw new Exception('Missing entry source_dir from the config file');
}
$oTestConfig = clone($oSourceConf);
$oTestConfig->ChangeModulesPath($sSourceEnv, $sTestEnv);
// - Switch DB name to a dedicated one so we don't mess with the original one
$sTestEnvSanitizedForDBName = preg_replace('/[^\d\w]/', '', $sTestEnv);
$oTestConfig->Set('db_name', $oTestConfig->Get('db_name').'_'.$sTestEnvSanitizedForDBName);
// - Compile env. based on the existing 'production' env.
$oEnvironment = new UnitTestRunTimeEnvironment($sTestEnv);
$oEnvironment->WriteConfigFileSafe($oTestConfig);
$oEnvironment->CompileFrom($sSourceEnv, false);
// - Force re-creating a fresh DB
CMDBSource::InitFromConfig($oTestConfig);
if (CMDBSource::IsDB($oTestConfig->Get('db_name'))) {
CMDBSource::DropDB();
}
CMDBSource::CreateDB($oTestConfig->Get('db_name'));
MetaModel::Startup($sConfFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sTestEnv);
$this->MarkEnvironmentReady();
$this->debug('Preparation of custom environment "'.$sTestEnv.'" done.');
}
parent::PrepareEnvironment();
}
}

View File

@@ -1,21 +1,8 @@
<?php
// Copyright (c) 2010-2023 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
// along with iTop. If not, see <http://www.gnu.org/licenses/>
//
/*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest;
@@ -31,6 +18,7 @@ use CMDBObject;
use CMDBSource;
use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Service\Events\EventService;
use Config;
use Contact;
use DBObject;
use DBObjectSet;
@@ -46,11 +34,13 @@ use MetaModel;
use Person;
use PluginManager;
use Server;
use SetupUtils;
use TagSetFieldData;
use Ticket;
use URP_UserProfile;
use User;
use UserRequest;
use utils;
use VirtualHost;
use VirtualMachine;
use XMLDataLoader;
@@ -61,41 +51,53 @@ define('TAG_CLASS', 'FAQ');
define('TAG_ATTCODE', 'domains');
/**
* Class ItopDataTestCase
*
* Helper class to extend for tests needing access to iTop's metamodel
*
* ** Warning** Each class extending this one needs to add the following annotations :
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*
* @since 2.7.7 3.0.1 3.1.0 N°4624 processIsolation is disabled by default and must be enabled in each test needing it (basically all tests using
* iTop datamodel)
*/
class ItopDataTestCase extends ItopTestCase
abstract class ItopDataTestCase extends ItopTestCase
{
private $iTestOrgId;
// For cleanup
private $aCreatedObjects = array();
// Counts
public $aReloadCount = [];
private $aCreatedObjects = [];
private $aEventListeners = [];
/**
* @var string Default environment to use for test cases
*/
const DEFAULT_TEST_ENVIRONMENT = 'production';
const USE_TRANSACTION = true;
const CREATE_TEST_ORG = false;
/**
* This method is called before the first test of this test class is run (in the current process).
*/
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
}
/**
* This method is called after the last test of this test class is run (in the current process).
*/
public static function tearDownAfterClass(): void
{
\UserRights::FlushPrivileges();
parent::tearDownAfterClass();
}
/**
* @throws Exception
*/
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('application/utils.inc.php');
$sEnv = 'production';
$sConfigFile = APPCONF.$sEnv.'/'.ITOP_CONFIG_FILE;
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
$this->PrepareEnvironment();
if (static::USE_TRANSACTION)
{
@@ -105,8 +107,6 @@ class ItopDataTestCase extends ItopTestCase
{
$this->CreateTestOrganization();
}
EventService::RegisterListener(EVENT_DB_OBJECT_RELOAD, [$this, 'CountObjectReload']);
}
/**
@@ -114,6 +114,9 @@ class ItopDataTestCase extends ItopTestCase
*/
protected function tearDown(): void
{
static::SetNonPublicStaticProperty(\cmdbAbstractObject::class, 'aObjectsAwaitingEventDbLinksChanged', []);
\cmdbAbstractObject::SetEventDBLinksChangedBlocked(false);
if (static::USE_TRANSACTION) {
$this->debug("ROLLBACK !!!");
CMDBSource::Query('ROLLBACK');
@@ -135,10 +138,65 @@ class ItopDataTestCase extends ItopTestCase
}
}
}
// As soon as a rollback has been performed, each object memoized should be discarded
CMDBObject::SetCurrentChange(null);
// Leave the place clean
\UserRights::Logoff();
foreach ($this->aEventListeners as $sListenerId) {
EventService::UnRegisterListener($sListenerId);
}
parent::tearDown();
}
/**
* @inheritDoc
*/
protected function LoadRequiredItopFiles(): void
{
parent::LoadRequiredItopFiles();
$this->RequireOnceItopFile('application/utils.inc.php');
}
/**
* @return string Environment the test will run in
* @since 2.7.9 3.0.4 3.1.0
*/
protected function GetTestEnvironment(): string
{
return self::DEFAULT_TEST_ENVIRONMENT;
}
/**
* @return string Absolute path of the configuration file used for the test
* @since 2.7.9 3.0.4 3.1.0
*/
protected function GetConfigFileAbsPath(): string
{
return utils::GetConfigFilePath($this->GetTestEnvironment());
}
/**
* Prepare the iTop environment for test to run
*
* @return void
* @throws \CoreException
* @throws \DictExceptionUnknownLanguage
* @throws \MySQLException
* @since 2.7.9 3.0.4 3.1.0
*/
protected function PrepareEnvironment(): void
{
$sEnv = $this->GetTestEnvironment();
$sConfigFile = $this->GetConfigFileAbsPath();
// Start MetaModel for the prepared environment
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
}
/**
* @return mixed
*/
@@ -147,6 +205,31 @@ class ItopDataTestCase extends ItopTestCase
return $this->iTestOrgId;
}
/////////////////////////////////////////////////////////////////////////////
/// Facades for environment settings
/////////////////////////////////////////////////////////////////////////////
/**
* Facade for EventService::RegisterListener
*
* @param string $sEvent
* @param callable $callback
* @param $sEventSource
* @param array $aCallbackData
* @param $context
* @param float $fPriority
* @param $sModuleId
*
* @return string
*/
public function EventService_RegisterListener(string $sEvent, callable $callback, $sEventSource = null, array $aCallbackData = [], $context = null, float $fPriority = 0.0, $sModuleId = ''): string
{
$ret = EventService::RegisterListener($sEvent, $callback, $sEventSource, $aCallbackData, $context, $fPriority, $sModuleId);
if (false !== $ret) {
$this->aEventListeners[] = $ret;
}
return $ret;
}
/////////////////////////////////////////////////////////////////////////////
/// MetaModel Utilities
/////////////////////////////////////////////////////////////////////////////
@@ -841,49 +924,6 @@ 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
*
@@ -910,6 +950,17 @@ class ItopDataTestCase extends ItopTestCase
}
}
protected function assertDBChangeOpCount(string $sClass, $iId, int $iExpectedCount)
{
$oSearch = new \DBObjectSearch('CMDBChangeOp');
$oSearch->AddCondition('objclass', $sClass);
$oSearch->AddCondition('objkey', $iId);
$oSearch->AllowAllData();
$oSet = new \DBObjectSet($oSearch);
$iCount = $oSet->Count();
$this->assertEquals($iExpectedCount, $iCount, "Found $iCount changes for object $sClass::$iId");
}
/**
* Import a set of XML files describing a consistent set of iTop objects
* @param string[] $aFiles

View File

@@ -1,61 +1,77 @@
<?php
/**
* Copyright (C) 2013-2023 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
/*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest;
/**
* Created by PhpStorm.
* User: Eric
* Date: 20/11/2017
* Time: 11:21
*/
use CMDBSource;
use MySQLTransactionNotClosedException;
use PHPUnit\Framework\TestCase;
use SetupUtils;
class ItopTestCase extends TestCase
/**
* Class ItopTestCase
*
* Helper class to extend for tests that DO NOT need to access the DataModel or the Database
*
* @author Eric Espie <eric.espie@combodo.com>
* @package Combodo\iTop\Test\UnitTest
*/
abstract class ItopTestCase extends TestCase
{
const TEST_LOG_DIR = 'test';
static $DEBUG_UNIT_TEST = false;
public const TEST_LOG_DIR = 'test';
public static $DEBUG_UNIT_TEST = false;
/** @noinspection UsingInclusionOnceReturnValueInspection avoid errors for approot includes */
protected function setUp(): void {
$sAppRootRelPath = 'approot.inc.php';
$sDepthSeparator = '../';
for ($iDepth = 0; $iDepth < 8; $iDepth++) {
if (file_exists($sAppRootRelPath)) {
require_once $sAppRootRelPath;
break;
}
/**
* Override the default value to disable the backup of globals in case of tests run in a separate process
*/
protected $preserveGlobalState = false;
$sAppRootRelPath = $sDepthSeparator.$sAppRootRelPath;
}
/**
* This method is called before the first test of this test class is run (in the current process).
*/
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
static::$DEBUG_UNIT_TEST = getenv('DEBUG_UNIT_TEST');
require_once static::GetAppRoot() . 'approot.inc.php';
if (false === defined('ITOP_PHPUNIT_RUNNING_CONSTANT_NAME')) {
// setUp might be called multiple times, so protecting the define() call !
define('ITOP_PHPUNIT_RUNNING_CONSTANT_NAME', true);
}
}
/**
* This method is called after the last test of this test class is run (in the current process).
*/
public static function tearDownAfterClass(): void
{
parent::tearDownAfterClass();
if (method_exists('utils', 'GetConfig')) {
// Reset the config by forcing the load from disk
$oConfig = \utils::GetConfig(true);
if (method_exists('MetaModel', 'SetConfig')) {
\MetaModel::SetConfig($oConfig);
}
}
if (method_exists('Dict', 'SetUserLanguage')) {
\Dict::SetUserLanguage();
}
}
protected function setUp(): void {
parent::setUp();
$this->debug("\n----------\n---------- ".$this->getName()."\n----------\n");
if (false === defined(ITOP_PHPUNIT_RUNNING_CONSTANT_NAME)) {
// setUp might be called multiple times, so protecting the define() call !
define(ITOP_PHPUNIT_RUNNING_CONSTANT_NAME, true);
}
$this->LoadRequiredItopFiles();
$this->LoadRequiredTestFiles();
}
/**
@@ -68,10 +84,60 @@ class ItopTestCase extends TestCase
if (CMDBSource::IsInsideTransaction()) {
// Nested transactions were opened but not finished !
// Rollback to avoid side effects on next tests
while (CMDBSource::IsInsideTransaction()) {
CMDBSource::Query('ROLLBACK');
}
throw new MySQLTransactionNotClosedException('Some DB transactions were opened but not closed ! Fix the code by adding ROLLBACK or COMMIT statements !', []);
}
}
/** Helper than can be called in the context of a data provider */
public static function GetAppRoot()
{
if (defined('APPROOT')) {
return APPROOT;
}
$sSearchPath = __DIR__;
for ($iDepth = 0; $iDepth < 8; $iDepth++) {
if (file_exists($sSearchPath.'/approot.inc.php')) {
break;
}
$iOffsetSep = strrpos($sSearchPath, '/');
if ($iOffsetSep === false) {
$iOffsetSep = strrpos($sSearchPath, '\\');
if ($iOffsetSep === false) {
// Do not throw an exception here as PHPUnit will not show it clearly when determing the list of test to perform
return 'Could not find the approot file in '.$sSearchPath;
}
}
$sSearchPath = substr($sSearchPath, 0, $iOffsetSep);
}
return $sSearchPath.'/';
}
/**
* Overload this method to require necessary files through {@see \Combodo\iTop\Test\UnitTest\ItopTestCase::RequireOnceItopFile()}
*
* @return void
* @since 2.7.9 3.0.4 3.1.0
*/
protected function LoadRequiredItopFiles(): void
{
// Empty until we actually need to require some files in the class
}
/**
* Overload this method to require necessary files through {@see \Combodo\iTop\Test\UnitTest\ItopTestCase::RequireOnceUnitTestFile()}
*
* @return void
* @since 2.7.10 3.0.4 3.1.0
*/
protected function LoadRequiredTestFiles(): void
{
// Empty until we actually need to require some files in the class
}
/**
* Require once an iTop file (core or extension) from its relative path to the iTop root dir.
* This ensure to always use the right absolute path, especially in {@see \Combodo\iTop\Test\UnitTest\ItopTestCase::RequireOnceUnitTestFile()}
@@ -83,7 +149,7 @@ class ItopTestCase extends TestCase
*/
protected function RequireOnceItopFile(string $sFileRelPath): void
{
require_once APPROOT . $sFileRelPath;
require_once $this->GetAppRoot() . $sFileRelPath;
}
/**
@@ -151,7 +217,7 @@ class ItopTestCase extends TestCase
/**
* @since 2.7.4 3.0.0
*/
public function InvokeNonPublicStaticMethod($sObjectClass, $sMethodName, $aArgs)
public function InvokeNonPublicStaticMethod($sObjectClass, $sMethodName, $aArgs = [])
{
return $this->InvokeNonPublicMethod($sObjectClass, $sMethodName, null, $aArgs);
}
@@ -168,7 +234,7 @@ class ItopTestCase extends TestCase
*
* @since 2.7.4 3.0.0
*/
public function InvokeNonPublicMethod($sObjectClass, $sMethodName, $oObject, $aArgs)
public function InvokeNonPublicMethod($sObjectClass, $sMethodName, $oObject, $aArgs = [])
{
$class = new \ReflectionClass($sObjectClass);
$method = $class->getMethod($sMethodName);

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Hook;
require_once __DIR__ . '/../../../../approot.inc.php';
use PHPUnit\Runner\AfterLastTestHook;
use PHPUnit\Runner\BeforeFirstTestHook;
use utils;
/**
* Class TestsRunStartHook
*
* IMPORTANT: This will no longer work in PHPUnit 10.0 and there is no alternative for now, so we will have to migrate it when the time comes
* @link https://localheinz.com/articles/2023/02/14/extending-phpunit-with-its-new-event-system/#content-hooks-event-system
*
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @package Combodo\iTop\Test\UnitTest\Hook
* @since N°6097 2.7.10 3.0.4 3.1.1
*/
class TestsRunStartHook implements BeforeFirstTestHook, AfterLastTestHook
{
/**
* Use the modification time on this file to check whereas it is newer than the requirements in a test case
*
* @return string Abs. path to a file generated when the global tests run starts.
*/
public static function GetRunStartedFileAbsPath(): string
{
// Note: This can't be put in the cache-<ENV> folder as we have multiple <ENV> running across the test cases
// We also don't want to put it in the unit tests folder as it is not supposed to be writable
return APPROOT.'data/.php-unit-tests-run-started';
}
/**
* @inheritDoc
*/
public function executeBeforeFirstTest(): void
{
// Create / change modification timestamp of file marking the beginning of the tests run
touch(static::GetRunStartedFileAbsPath());
}
/**
* @inheritDoc
*/
public function executeAfterLastTest(): void
{
// Cleanup of file marking the beginning of the tests run
if (file_exists(static::GetRunStartedFileAbsPath())) {
unlink(static::GetRunStartedFileAbsPath());
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
/*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Service;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use IssueLog;
use MFCoreModule;
use ReflectionClass;
use RunTimeEnvironment;
/**
* Class UnitTestRunTimeEnvironment
*
* Runtime env. dedicated to creating a temp. environment for a group of unit tests with XML deltas.
*
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @since N°6097 2.7.10 3.0.4 3.1.1
*/
class UnitTestRunTimeEnvironment extends RunTimeEnvironment
{
/**
* @inheritDoc
*/
protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir)
{
$aRet = parent::GetMFModulesToCompile($sSourceEnv, $sSourceDir);
/** @var string[] $aDeltaFiles Referential of loaded deltas. Mostly to avoid duplicates. */
$aDeltaFiles = [];
foreach (get_declared_classes() as $sClass) {
// Filter on classes derived from this \Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCaseItopCustomDatamodelTestCase
if (false === is_a($sClass, ItopCustomDatamodelTestCase::class, true)) {
continue;
}
$oReflectionClass = new ReflectionClass($sClass);
$oReflectionMethod = $oReflectionClass->getMethod('GetDatamodelDeltaAbsPath');
// Filter on classes with an actual XML delta (eg. not \Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase and maybe some other deriving from a class with a delta)
if ($oReflectionMethod->isAbstract()) {
continue;
}
/** @var \Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase $oTestClassInstance */
$oTestClassInstance = new $sClass();
// Check test class is for desired environment
if ($oTestClassInstance->GetTestEnvironment() !== $this->sFinalEnv) {
continue;
}
// Check XML delta actually exists
$sDeltaFile = $oTestClassInstance->GetDatamodelDeltaAbsPath();
if (false === is_file($sDeltaFile)) {
$this->fail("Could not prepare '$this->sFinalEnv' as the XML delta file '$sDeltaFile' (used in $sClass) does not seem to exist");
}
// Avoid duplicates
if (in_array($sDeltaFile, $aDeltaFiles)) {
continue;
}
// Prepare fake module name for delta
$sDeltaName = preg_replace('/[^\d\w]/', '', $sDeltaFile);
// Note: We can't use \MFDeltaModule as we can't specify the ID which leads to only 1 delta being applied... In the future we might introduce a new MFXXXModule, but in the meantime it feels alright (GLA / RQU)
$oDelta = new MFCoreModule($sDeltaName, $sDeltaName, $sDeltaFile);
IssueLog::Debug('XML delta found for unit tests', static::class, [
'Unit test class' => $sClass,
'Delta file path' => $sDeltaFile,
]);
$aDeltaFiles[] = $sDeltaFile;
$aRet[$sDeltaName] = $oDelta;
}
return $aRet;
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* Usage: php run_class_by_class.php
*
* Execute the whole test suite (as declared in phpunit.xml.dist) one class at a time.
* This is to ensure that test class are still independant from each other, after a rework of ItopTestCase, for instance.
*/
const PHP_EXE = 'php';
const ITOP_ROOT = __DIR__.'/../../..';
const ITOP_PHPUNIT = ITOP_ROOT.'/tests/php-unit-tests';
const PHPUNIT_COMMAND = PHP_EXE.' '.ITOP_PHPUNIT.'/vendor/phpunit/phpunit/phpunit';
function ListTests($sUnitaryTestsDir = '')
{
$sConfigFile = ITOP_PHPUNIT."/phpunit.xml.dist";
$sCommand = PHPUNIT_COMMAND." --configuration $sConfigFile --list-tests $sUnitaryTestsDir";
exec($sCommand, $aOutput, $iResultCode);
//passthru($sCommand, $iResultCode);
if ($iResultCode != 0) { // or 1 in case of a failing test
echo "Failed executing command: $sCommand\n";
return [];
}
$aClasses = [];
foreach ($aOutput as $iLine => $sLine) {
// Example of formats to be filtered
//- DatamodelsXmlFilesTest::testAllItopXmlFilesCovered
//- Combodo\iTop\Test\UnitTest\Application\DashboardLayoutTest::testGetDashletCoordinates"OneColLayout-Cell0"
//if (preg_match('@^- ([a-z]+\\\\)*([a-z]+::[a-z0-9]+)@i', $sLine, $aMatches)) {
if (preg_match('@([a-z0-9]+)::test@i', $sLine, $aMatches)) {
$sTestClass = $aMatches[1];
$aClasses[$sTestClass] = $sTestClass;
}
}
return array_keys($aClasses);
}
function RunTests($sFilterRegExp, $sUnitaryTestsDir = '', $bPassthru = false)
{
$sRegExpShellArg = '"'.str_replace('"', '\\"', $sFilterRegExp).'"';
$sConfigFile = ITOP_PHPUNIT."/phpunit.xml.dist";
$sCommand = PHPUNIT_COMMAND." --configuration $sConfigFile --filter $sRegExpShellArg $sUnitaryTestsDir";
///echo "executing <<<$sCommand>>>\n";
if ($bPassthru) {
passthru($sCommand, $iResultCode);
}
else {
exec($sCommand, $aTrashedOutput, $iResultCode);
}
$bTestSuccess = ($iResultCode == 0); // or 1 in case of a failing test
return $bTestSuccess;
}
$sUnitaryTestsDir = '';
$aTestClasses = ListTests($sUnitaryTestsDir);
echo "Found ".count($aTestClasses)." to execute: ".implode(", ", $aTestClasses)."\n";
echo "Testing...\n";
foreach ($aTestClasses as $sTestClass) {
$fStarted = microtime(true);
$bSuccess = RunTests($sTestClass);
$sDuration = round(microtime(true) - $fStarted, 3);
echo "$sTestClass: ".($bSuccess ? 'Ok' : "FAILURE")." [$sDuration s]\n";
}

View File

@@ -7,8 +7,6 @@ use Combodo\iTop\Test\UnitTest\ItopTestCase;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class SessionTest extends ItopTestCase
{

View File

@@ -7,9 +7,6 @@ use FindStylesheetObject;
use ThemeHandler;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
* @covers ThemeHandler
*/
class ThemeHandlerTest extends ItopTestCase

View File

@@ -9,9 +9,6 @@ use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
/**
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
* Class NavigationMenuTest
*
* @package UI\Base\Layout

View File

@@ -0,0 +1,168 @@
<?php
/*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Application;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use MetaModel;
use utils;
/**
* @covers \iBackofficeLinkedScriptsExtension
*/
class ApplicationExtensionTest extends ItopCustomDatamodelTestCase
{
protected const ENUM_API_CALL_METHOD_ENUMPLUGINS = 'MetaModel::EnumPlugins';
protected const ENUM_API_CALL_METHOD_GETCLASSESFORINTERFACE = 'utils::GetClassesForInterface';
/**
* @inheritDoc
*/
public function GetDatamodelDeltaAbsPath(): string
{
return __DIR__ . '/Delta/application-extension-usages-in-snippets.xml';
}
/**
* This test ensures that APIs are discovered / registered / called.
*
* It was introduced after {@since N°6436} when some APIs registration in \MetaModel::InitClasses() were lost during a merge {@link https://github.com/Combodo/iTop/commit/6432678de9f635990e22a6512e5b30713c22204a#diff-c94890a26989b5a5ce638f82e8cc7d4c7aa24e6fbb9c2ca89850e8fa4e0e9adaL3004} preventing them from being called when requested. This was hard to detect as it needed an extension to use the lost API to witness that it was no longer called.
*
* For each new API, a new test case should be added here to ensure that we don't lose it later.
* To do so:
* - Add the API to the provider
* - Add a class extending / implementing the API in ./Delta/application-extension-usages-in-snippets.xml
*
* @param string $sAPIFQCN
* @param string $sCallMethod
*
* @return void
* @dataProvider ExtensionAPIRegisteredAndCalledProvider
*/
public function testExtensionAPIRegisteredAndCalled(string $sAPIFQCN, string $sCallMethod)
{
if ($sCallMethod === static::ENUM_API_CALL_METHOD_ENUMPLUGINS) {
$iExtendingClassesCount = count(MetaModel::EnumPlugins($sAPIFQCN));
} else {
$iExtendingClassesCount = count(utils::GetClassesForInterface($sAPIFQCN, '', ['[\\\\/]lib[\\\\/]', '[\\\\/]node_modules[\\\\/]', '[\\\\/]test[\\\\/]', '[\\\\/]tests[\\\\/]']));
}
$this->assertGreaterThan(0, $iExtendingClassesCount, "Found no class extending the $sAPIFQCN API");
}
public function ExtensionAPIRegisteredAndCalledProvider(): array
{
// APIs not concerned by this test:
// * \iRestServiceProvider as it is discovered by iterating over declared classes directly
// * \iLoginUIExtension as it is not iterated directly, only its derived interfaces
return [
\iLoginFSMExtension::class => [
\iLoginFSMExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iLogoutExtension::class => [
\iLogoutExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iLoginUIExtension::class => [
\iLoginUIExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iPreferencesExtension::class => [
\iPreferencesExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iApplicationUIExtension::class => [
\iApplicationUIExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iApplicationObjectExtension::class => [
\iApplicationObjectExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iPopupMenuExtension::class => [
\iPopupMenuExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iPageUIExtension::class => [
\iPageUIExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iPageUIBlockExtension::class => [
\iPageUIBlockExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iBackofficeLinkedScriptsExtension::class => [
\iBackofficeLinkedScriptsExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iBackofficeEarlyScriptExtension::class => [
\iBackofficeEarlyScriptExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iBackofficeScriptExtension::class => [
\iBackofficeScriptExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iBackofficeInitScriptExtension::class => [
\iBackofficeInitScriptExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iBackofficeReadyScriptExtension::class => [
\iBackofficeReadyScriptExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iBackofficeLinkedStylesheetsExtension::class => [
\iBackofficeLinkedStylesheetsExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iBackofficeStyleExtension::class => [
\iBackofficeStyleExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iBackofficeDictEntriesExtension::class => [
\iBackofficeDictEntriesExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iBackofficeDictEntriesPrefixesExtension::class => [
\iBackofficeDictEntriesPrefixesExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iPortalUIExtension::class => [
\iPortalUIExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iQueryModifier::class => [
\iQueryModifier::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iOnClassInitialization::class => [
\iOnClassInitialization::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iFieldRendererMappingsExtension::class => [
\iFieldRendererMappingsExtension::class,
static::ENUM_API_CALL_METHOD_GETCLASSESFORINTERFACE,
],
\iModuleExtension::class => [
\iModuleExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iKPILoggerExtension::class => [
\iKPILoggerExtension::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\ModuleHandlerApiInterface::class => [
\ModuleHandlerApiInterface::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
\iNewsroomProvider::class => [
\iNewsroomProvider::class,
static::ENUM_API_CALL_METHOD_ENUMPLUGINS,
],
];
}
}

View File

@@ -0,0 +1,397 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.7">
<snippets>
<!-- These snippets just implements application/applicationextension.inc.php APIs for the ApplicationExtensionTest unit test -->
<snippet id="ExampleFor_iLoginFSMExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iLoginFSMExtension extends \AbstractLoginFSMExtension
{
public function ListSupportedLoginModes()
{
// Do nothing, we just need the class to exists for the unit test
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iLogoutExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iLogoutExtension implements \iLogoutExtension
{
public function LogoutAction()
{
// Do nothing, we just need the class to exists for the unit test
}
public function ListSupportedLoginModes()
{
// Do nothing, we just need the class to exists for the unit test
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iLoginUIExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iLoginUIExtension implements \iLoginUIExtension
{
public function GetTwigContext()
{
// Do nothing, we just need the class to exists for the unit test
}
public function ListSupportedLoginModes()
{
// Do nothing, we just need the class to exists for the unit test
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iPreferencesExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iPreferencesExtension extends \AbstractPreferencesExtension
{
// Do nothing, we just need the class to exists for the unit test
}
]]></content>
</snippet>
<snippet id="ExampleFor_iApplicationUIExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iApplicationUIExtension extends \AbstractApplicationUIExtension
{
// Do nothing, we just need the class to exists for the unit test
}
]]></content>
</snippet>
<snippet id="ExampleFor_iApplicationObjectExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iApplicationObjectExtension extends \AbstractApplicationObjectExtension
{
// Do nothing, we just need the class to exists for the unit test
}
]]></content>
</snippet>
<snippet id="ExampleFor_iPopupMenuExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iPopupMenuExtension implements \iPopupMenuExtension
{
public static function EnumItems($iMenuId, $param)
{
// Do nothing, we just need the class to exists for the unit test
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_ApplicationPopupMenuItem" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_ApplicationPopupMenuItem extends \ApplicationPopupMenuItem
{
public function GetMenuItem()
{
// Do nothing, we just need the class to exists for the unit test
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iPageUIExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iPageUIExtension extends \AbstractPageUIExtension
{
// Do nothing, we just need the class to exists for the unit test
}
]]></content>
</snippet>
<snippet id="ExamplePageUIBlockExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iPageUIBlockExtension extends \AbstractPageUIBlockExtension
{
// Do nothing, we just need the class to exists for the unit test
}
]]></content>
</snippet>
<snippet id="ExampleFor_iBackofficeLinkedScriptsExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iBackofficeLinkedScriptsExtension implements \iBackofficeLinkedScriptsExtension
{
public function GetLinkedScriptsAbsUrls(): array
{
return [
'https://foo.bar/first.js',
'https://foo.bar/second.js',
];
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iBackofficeEarlyScriptExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iBackofficeEarlyScriptExtension implements \iBackofficeEarlyScriptExtension
{
public function GetEarlyScript(): string
{
return <<<JS
console.log('This is a PHP unit test');
JS;
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iBackofficeScriptExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iBackofficeScriptExtension implements \iBackofficeScriptExtension
{
public function GetScript(): string
{
return <<<JS
console.log('This is a PHP unit test');
JS;
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iBackofficeInitScriptExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iBackofficeInitScriptExtension implements \iBackofficeInitScriptExtension
{
public function GetInitScript(): string
{
return <<<JS
console.log('This is a PHP unit test');
JS;
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iBackofficeReadyScriptExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iBackofficeReadyScriptExtension implements \iBackofficeReadyScriptExtension
{
public function GetReadyScript(): string
{
return <<<JS
console.log('This is a PHP unit test');
JS;
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iBackofficeLinkedStylesheetsExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iBackofficeLinkedStylesheetsExtension implements \iBackofficeLinkedStylesheetsExtension
{
public function GetLinkedStylesheetsAbsUrls(): array
{
return [
'https://foo.bar/first.css',
'https://foo.bar/second.css',
];
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iBackofficeStyleExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iBackofficeStyleExtension implements \iBackofficeStyleExtension
{
public function GetStyle(): string
{
return <<<CSS
.foo {
color: inherit;
}
CSS;
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iBackofficeDictEntriesExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iBackofficeDictEntriesExtension implements \iBackofficeDictEntriesExtension
{
public function GetDictEntries(): array
{
return [
'Foo:First' => 'Foo is first',
'Foo:Second' => 'Foo is second',
];
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iBackofficeDictEntriesPrefixesExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iBackofficeDictEntriesPrefixesExtension implements \iBackofficeDictEntriesPrefixesExtension
{
public function GetDictEntriesPrefixes(): array
{
return [
'Foo:',
'Bar:',
];
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iPortalUIExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iPortalUIExtension extends \AbstractPortalUIExtension
{
// Do nothing, we just need the class to exists for the unit test
}
]]></content>
</snippet>
<snippet id="ExampleFor_iQueryModifier" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iQueryModifier implements \iQueryModifier
{
public function __construct()
{
// Do nothing, we just need the class to exists for the unit test
}
public function GetFieldExpression(QueryBuilderContext &$oBuild, $sClass, $sAttCode, $sColId, Expression $oFieldSQLExp, SQLQuery &$oSelect)
{
// Do nothing, we just need the class to exists for the unit test
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iOnClassInitialization" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iOnClassInitialization implements \iOnClassInitialization
{
public function OnAfterClassInitialization($sClass)
{
// Do nothing, we just need the class to exists for the unit test
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iFieldRendererMappingsExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iFieldRendererMappingsExtension implements \iFieldRendererMappingsExtension
{
public static function RegisterSupportedFields(): array
{
// Do nothing, we just need the class to exists for the unit test
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iModuleExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iModuleExtension implements \iModuleExtension
{
public function __construct()
{
// Do nothing, we just need the class to exists for the unit test
}
}
]]></content>
</snippet>
<snippet id="ExampleFor_iKPILoggerExtension" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iKPILoggerExtension implements \iKPILoggerExtension
{
public function InitStats()
{
// Do nothing, we just need the class to exists for the unit test
}
public function LogOperation($oKpiLogData)
{
// Do nothing, we just need the class to exists for the unit test
}
}
]]></content>
</snippet>
<!-- These snippets just implements core/modulehandler.class.inc.php APIs for the ApplicationExtensionTest unit test -->
<snippet id="ExampleFor_ModuleHandlerApiInterface" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_ModuleHandlerApiInterface extends \ModuleHandlerAPI
{
// Do nothing, we just need the class to exists for the unit test
}
]]></content>
</snippet>
<!-- These snippets just implements application/newsroomprovider.class.inc.php APIs for the ApplicationExtensionTest unit test -->
<snippet id="ExampleFor_iNewsroomProvider" _delta="define">
<placement>core</placement>
<rank>0</rank>
<content><![CDATA[
class ExampleFor_iNewsroomProvider extends \NewsroomProviderBase
{
public function GetLabel()
{
// Do nothing, we just need the class to exists for the unit test
}
public function GetFetchURL()
{
// Do nothing, we just need the class to exists for the unit test
}
public function GetMarkAllAsReadURL()
{
// Do nothing, we just need the class to exists for the unit test
}
public function GetViewAllURL()
{
// Do nothing, we just need the class to exists for the unit test
}
}
]]></content>
</snippet>
</snippets>
</itop_design>

View File

@@ -4,11 +4,6 @@ use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Service\Events\EventService;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class cmdbAbstractObjectTest extends ItopDataTestCase {
const USE_TRANSACTION = true;
const CREATE_TEST_ORG = true;
@@ -18,6 +13,8 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
protected function setUp(): void
{
static::$aEventCalls = [];
static::$iEventCalls = 0;
parent::setUp();
}
@@ -80,6 +77,9 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
self::assertSame($aExpectedLinkStack, $aLinkModificationsStack);
}
/**
* @runInSeparateProcess
*/
public function testProcessClassIdDeferredUpdate()
{
// Create the team
@@ -157,6 +157,7 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
}
/**
* @runInSeparateProcess
* Check that EVENT_DB_LINKS_CHANGED events are not sent to the current updated/created object (Team)
* the events are sent to the other side (Person)
*
@@ -188,7 +189,7 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
$this->debug("\n-------------> Test Starts HERE\n");
$oEventReceiver = new LinksEventReceiver();
$oEventReceiver = new LinksEventReceiver($this);
$oEventReceiver->RegisterCRUDListeners();
$oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'persons_list' => $oLinkSet, 'org_id' => $this->getTestOrgId()]);
@@ -200,6 +201,8 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
}
/**
* @runInSeparateProcess
*
* Check that EVENT_DB_LINKS_CHANGED events are sent to all the linked objects when creating a new lnk object
*
* @return void
@@ -223,7 +226,7 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
$this->assertIsObject($oTeam);
$this->debug("\n-------------> Test Starts HERE\n");
$oEventReceiver = new LinksEventReceiver();
$oEventReceiver = new LinksEventReceiver($this);
$oEventReceiver->RegisterCRUDListeners();
// The link creation will signal both the Person an the Team
@@ -235,6 +238,7 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
}
/**
* @runInSeparateProcess
* Check that EVENT_DB_LINKS_CHANGED events are sent to all the linked objects when updating an existing lnk object
*
* @return void
@@ -262,7 +266,7 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
$oLink->DBInsert();
$this->debug("\n-------------> Test Starts HERE\n");
$oEventReceiver = new LinksEventReceiver();
$oEventReceiver = new LinksEventReceiver($this);
$oEventReceiver->RegisterCRUDListeners();
// The link update will signal both the Person, the Team and the ContactType
@@ -277,6 +281,7 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
}
/**
* @runInSeparateProcess
* Check that when a link changes from an object to another, then both objects are notified
*
* @return void
@@ -307,7 +312,7 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
$oLink->DBInsert();
$this->debug("\n-------------> Test Starts HERE\n");
$oEventReceiver = new LinksEventReceiver();
$oEventReceiver = new LinksEventReceiver($this);
$oEventReceiver->RegisterCRUDListeners();
// The link update will signal both the Persons and the Team
@@ -320,6 +325,7 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
}
/**
* @runInSeparateProcess
* Check that EVENT_DB_LINKS_CHANGED events are sent to all the linked objects when deleting an existing lnk object
*
* @return void
@@ -347,7 +353,7 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
$oLink->DBInsert();
$this->debug("\n-------------> Test Starts HERE\n");
$oEventReceiver = new LinksEventReceiver();
$oEventReceiver = new LinksEventReceiver($this);
$oEventReceiver->RegisterCRUDListeners();
// The link delete will signal both the Person an the Team
@@ -383,10 +389,16 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
*/
class LinksEventReceiver
{
private $oTestCase;
private $aCallbacks = [];
public static $bIsObjectInCrudStack;
public function __construct(ItopDataTestCase $oTestCase)
{
$this->oTestCase = $oTestCase;
}
public function AddCallback(string $sEvent, string $sClass, string $sFct, int $iCount = 1): void
{
$this->aCallbacks[$sEvent][$sClass] = [
@@ -423,53 +435,10 @@ class LinksEventReceiver
{
$this->Debug('Registering Test event listeners');
if (is_null($sEvent)) {
EventService::RegisterListener(EVENT_DB_LINKS_CHANGED, [$this, 'OnEvent']);
$this->oTestCase->EventService_RegisterListener(EVENT_DB_LINKS_CHANGED, [$this, 'OnEvent']);
return;
}
EventService::RegisterListener($sEvent, [$this, 'OnEvent'], $mEventSource);
}
/**
* @param $oObject
*
* @return void
* @throws \ArchivedObjectException
* @throws \CoreCannotSaveObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \CoreWarning
* @throws \MySQLException
* @throws \OQLException
*/
private function AddRoleToLink($oObject): void
{
$this->Debug(__METHOD__);
$oContactType = MetaModel::NewObject(ContactType::class, ['name' => 'test_'.$oObject->GetKey()]);
$oContactType->DBInsert();
$oObject->Set('role_id', $oContactType->GetKey());
}
private function SetPersonFunction($oObject): void
{
$this->Debug(__METHOD__);
$oObject->Set('function', 'CRUD_function_'.rand());
}
private function SetPersonFirstName($oObject): void
{
$this->Debug(__METHOD__);
$oObject->Set('first_name', 'CRUD_first_name_'.rand());
}
private function CheckCrudStack(DBObject $oObject): void
{
self::$bIsObjectInCrudStack = DBObject::IsObjectCurrentlyInCrud(get_class($oObject), $oObject->GetKey());
}
private function CheckUpdateInLnk(lnkPersonToTeam $oLnkPersonToTeam)
{
$iTeamId = $oLnkPersonToTeam->Get('team_id');
self::$bIsObjectInCrudStack = DBObject::IsObjectCurrentlyInCrud(Team::class, $iTeamId);
$this->oTestCase->EventService_RegisterListener($sEvent, [$this, 'OnEvent'], $mEventSource);
}
private function Debug($msg)

View File

@@ -25,10 +25,6 @@ use privUITransactionFile;
use UserRights;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*
* @covers utils
* @group sampleDataNeeded
* @group defaultProfiles
@@ -180,7 +176,7 @@ class privUITransactionFileTest extends ItopDataTestCase
$this->assertTrue($bResult, 'Token created by support user must be removed in the support user context');
// test when no user logged (combodo-unauthenticated-form module for example)
UserRights::_ResetSessionCache();
UserRights::Logoff();
$sTransactionIdUnauthenticatedUser = privUITransactionFile::GetNewTransactionId();
$bResult = privUITransactionFile::IsTransactionValid($sTransactionIdUnauthenticatedUser, false);
$this->assertTrue($bResult, 'Token created by unauthenticated user must be valid when no user logged');

View File

@@ -33,9 +33,6 @@ use QueryOQL;
* All objects created in this test will be deleted by the test.
*
* @group iTopQuery
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class QueryTest extends ItopDataTestCase
{

View File

@@ -25,6 +25,7 @@ use Combodo\iTop\Test\UnitTest\ItopTestCase;
use utils;
/**
* @runClassInSeparateProcess
* @covers utils
*/
class utilsTest extends ItopTestCase
@@ -68,36 +69,36 @@ class utilsTest extends ItopTestCase
public function realPathDataProvider()
{
parent::setUp(); // if not called, APPROOT won't be defined :(
$sAppRoot = static::GetAppRoot();
$sSep = DIRECTORY_SEPARATOR;
$sItopRootRealPath = realpath(APPROOT).$sSep;
$sItopRootRealPath = realpath($sAppRoot).$sSep;
$sLicenseFileName = 'license.txt';
if (!is_file(APPROOT.$sLicenseFileName))
if (!is_file($sAppRoot.$sLicenseFileName))
{
$sLicenseFileName = 'LICENSE';
}
return [
$sLicenseFileName => [APPROOT.$sLicenseFileName, APPROOT, $sItopRootRealPath.$sLicenseFileName],
'unexisting file' => [APPROOT.'license_DOES_NOT_EXIST.txt', APPROOT, false],
'/'.$sLicenseFileName => [APPROOT.$sSep.$sLicenseFileName, APPROOT, $sItopRootRealPath.$sLicenseFileName],
'%2f'.$sLicenseFileName => [APPROOT.'%2f'. $sLicenseFileName, APPROOT, false],
'../'.$sLicenseFileName => [APPROOT.'..'.$sSep.$sLicenseFileName, APPROOT, false],
'%2e%2e%2f'.$sLicenseFileName => [APPROOT.'%2e%2e%2f'.$sLicenseFileName, APPROOT, false],
$sLicenseFileName => [$sAppRoot.$sLicenseFileName, $sAppRoot, $sItopRootRealPath.$sLicenseFileName],
'unexisting file' => [$sAppRoot.'license_DOES_NOT_EXIST.txt', $sAppRoot, false],
'/'.$sLicenseFileName => [$sAppRoot.$sSep.$sLicenseFileName, $sAppRoot, $sItopRootRealPath.$sLicenseFileName],
'%2f'.$sLicenseFileName => [$sAppRoot.'%2f'. $sLicenseFileName, $sAppRoot, false],
'../'.$sLicenseFileName => [$sAppRoot.'..'.$sSep.$sLicenseFileName, $sAppRoot, false],
'%2e%2e%2f'.$sLicenseFileName => [$sAppRoot.'%2e%2e%2f'.$sLicenseFileName, $sAppRoot, false],
'application/utils.inc.php with basepath=APPROOT' => [
APPROOT.'application/utils.inc.php',
APPROOT,
$sAppRoot.'application/utils.inc.php',
$sAppRoot,
$sItopRootRealPath.'application'.$sSep.'utils.inc.php',
],
'application/utils.inc.php with basepath=APPROOT/application' => [
APPROOT.'application/utils.inc.php',
APPROOT.'application',
$sAppRoot.'application/utils.inc.php',
$sAppRoot.'application',
$sItopRootRealPath.'application'.$sSep.'utils.inc.php',
],
'basepath containing / and \\' => [
APPROOT.'sources/Form/Form.php',
APPROOT.'sources/Form\\Form.php',
$sAppRoot.'sources/Form/Form.php',
$sAppRoot.'sources/Form\\Form.php',
$sItopRootRealPath.'sources'.$sSep.'Form'.$sSep.'Form.php',
],
];
@@ -117,13 +118,14 @@ class utilsTest extends ItopTestCase
public function LocalPathProvider()
{
$sAppRoot = static::GetAppRoot();
return array(
'index.php' => array(
'sAbsolutePath' => APPROOT.'index.php',
'sAbsolutePath' => $sAppRoot.'index.php',
'expected' => 'index.php',
),
'non existing' => array(
'sAbsolutePath' => APPROOT.'nonexisting/nonexisting',
'sAbsolutePath' => $sAppRoot.'nonexisting/nonexisting',
'expected' => false,
),
'outside' => array(
@@ -131,15 +133,15 @@ class utilsTest extends ItopTestCase
'expected' => false,
),
'application/cmdbabstract.class.inc.php' => array(
'sAbsolutePath' => APPROOT.'application/cmdbabstract.class.inc.php',
'sAbsolutePath' => $sAppRoot.'application/cmdbabstract.class.inc.php',
'expected' => 'application/cmdbabstract.class.inc.php',
),
'dir' => array(
'sAbsolutePath' => APPROOT.'application/.',
'sAbsolutePath' => $sAppRoot.'application/.',
'expected' => 'application',
),
'root' => array(
'sAbsolutePath' => APPROOT.'.',
'sAbsolutePath' => $sAppRoot.'.',
'expected' => '',
),
);
@@ -288,7 +290,7 @@ class utilsTest extends ItopTestCase
public function GetDefaultUrlAppRootProvider()
{
$this->setUp();
$sAppRoot = static::GetAppRoot();
$baseServerVar = [
'REMOTE_ADDR' => '127.0.0.1', //is not set, disable IsProxyTrusted
@@ -298,7 +300,7 @@ class utilsTest extends ItopTestCase
'HTTP_X_FORWARDED_PORT' => null,
'REQUEST_URI' => '/index.php?baz=1',
'SCRIPT_NAME' => '/index.php',
'SCRIPT_FILENAME' => APPROOT.'index.php',
'SCRIPT_FILENAME' => $sAppRoot.'index.php',
'QUERY_STRING' => 'baz=1',
'HTTP_X_FORWARDED_PROTO' => null,
'HTTP_X_FORWARDED_PROTOCOL' => null,
@@ -629,23 +631,21 @@ class utilsTest extends ItopTestCase
}
/**
* @dataProvider GetMentionedObjectsFromTextProvider
* @covers utils::GetMentionedObjectsFromText
*
* @param string $sInput
* @param string $sFormat
* @param array $aExceptedMentionedObjects
*
* @throws \Exception
*/
public function testGetMentionedObjectsFromText(string $sInput, string $sFormat, array $aExceptedMentionedObjects)
public function testGetMentionedObjectsFromText()
{
$aTestedMentionedObjects = utils::GetMentionedObjectsFromText($sInput, $sFormat);
// Emulate the "Case provider mechanism" (reason: the data provider requires utils constants not available before the application startup)
foreach ($this->GetMentionedObjectsFromTextProvider() as $sCase => list($sInput, $sFormat, $aExceptedMentionedObjects)) {
$aTestedMentionedObjects = utils::GetMentionedObjectsFromText($sInput, $sFormat);
$sExpectedAsString = print_r($aExceptedMentionedObjects, true);
$sTestedAsString = print_r($aTestedMentionedObjects, true);
$sExpectedAsString = print_r($aExceptedMentionedObjects, true);
$sTestedAsString = print_r($aTestedMentionedObjects, true);
$this->assertEquals($sTestedAsString, $sExpectedAsString, "Found mentioned objects don't match. Got: $sTestedAsString, expected $sExpectedAsString");
$this->assertEquals($sTestedAsString, $sExpectedAsString, "Case '$sCase': Found mentioned objects don't match. Got: $sTestedAsString, expected $sExpectedAsString");
}
}
/**
@@ -779,17 +779,13 @@ class utilsTest extends ItopTestCase
/**
* Test sanitizer.
*
* @param $type string type of sanitizer
* @param $valueToSanitize ? value to sanitize
* @param $expectedResult ? expected result
*
* @return void
*
* @dataProvider sanitizerDataProvider
*/
public function testSanitizer($type, $valueToSanitize, $expectedResult)
public function testSanitizer()
{
$this->assertEquals($expectedResult, utils::Sanitize($valueToSanitize, null, $type), 'url sanitize failed');
// Emulate the "Case provider mechanism" (reason: the data provider requires utils constants not available before the application startup)
foreach ($this->sanitizerDataProvider() as $sCase => list($type, $valueToSanitize, $expectedResult)) {
$this->assertEquals($expectedResult, utils::Sanitize($valueToSanitize, null, $type), "Case '$sCase': url sanitize failed");
}
}
/**

View File

@@ -10,9 +10,6 @@ use utils;
use Dict;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
* @covers \ActionEmail
*/
class ActionEmailTest extends ItopDataTestCase

View File

@@ -5,11 +5,6 @@ namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use MetaModel;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class AttributeDefinitionTest extends ItopDataTestCase {
const CREATE_TEST_ORG = true;

View File

@@ -5,11 +5,6 @@ namespace Combodo\iTop\Test\UnitTest\Core;
use AttributeURLDefaultPattern;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class AttributeURLTest extends ItopTestCase {
public function setUp(): void
{

View File

@@ -7,9 +7,7 @@ use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use MetaModel;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
* @runClassInSeparateProcess
*/
class BulkChangeTest extends ItopDataTestCase {
const CREATE_TEST_ORG = true;

View File

@@ -15,11 +15,6 @@ use MetaModel;
*/
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class CMDBObjectTest extends ItopDataTestCase
{
private $sAdminLogin;
@@ -99,6 +94,7 @@ class CMDBObjectTest extends ItopDataTestCase
* @covers CMDBObject::SetCurrentChange
* @since 3.0.1 N°5135 - Impersonate: history of changes versus log entries
*
* @runInSeparateProcess
* @dataProvider CurrentChangeUnderImpersonationProvider
*/
public function testCurrentChangeUnderImpersonation($sTrackInfo=null, $sExpectedChangeLogWhenImpersonation=null) {

View File

@@ -6,6 +6,9 @@ namespace Combodo\iTop\Test\UnitTest\Core;
use CMDBSource;
use Combodo\iTop\Core\DbConnectionWrapper;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use Exception;
use IssueLog;
use LogChannels;
use utils;
/**
@@ -14,11 +17,6 @@ use utils;
* @package Combodo\iTop\Test\UnitTest\Core
*/
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class CMDBSourceTest extends ItopTestCase
{
protected function setUp(): void
@@ -123,6 +121,8 @@ class CMDBSourceTest extends ItopTestCase
* @throws \CoreException
* @throws \MySQLException
* @since 3.0.0 N°4215
*
* @runInSeparateProcess Resetting DB connection, thus making other tests to fail!
*/
public function testIsOpenedDbConnectionUsingTls()
{
@@ -137,4 +137,49 @@ class CMDBSourceTest extends ItopTestCase
$bIsTlsCnx = $this->InvokeNonPublicStaticMethod(CMDBSource::class, 'IsOpenedDbConnectionUsingTls', [$oMysqli]);
$this->assertFalse($bIsTlsCnx);
}
/**
* @since 2.7.10 3.0.4 3.1.1 3.2.0 N°6643 Checks writing in IssueLog is really done
*/
public function testLogDeadLock(): void
{
$sExceptionMessage = 'Test exception for deadlock';
$oDeadlockException = new Exception($sExceptionMessage);
// \CMDBSource::LogDeadLock uses mysqli::errno and mysqli::query()
// I didn't achieve mocking the errno property by either of the following means :
// - PHPUnit mock => property is read only error
// - DbConnectionWrapper::SetDbConnectionMockForQuery with a custom mysqli children
// - override of errno property with an assignment (public $errno = ...;) => property is read only error
// - override of __get() for the errno property => no error but no change
// Solution for errno was to add a new parameter to disable errno read :/
/** @noinspection PhpDeprecationInspection */
$oMockMysqli = $this->getMockBuilder('mysqli')
->setMethods(['query'])
->getMock();
$oMockMysqli->expects($this->any())
->method('query')
->willReturnCallback(function () {
return false;
});
DbConnectionWrapper::SetDbConnectionMockForQuery($oMockMysqli);
$sTestErrorLogPath = APPROOT . 'log/error.phpunit.log';
IssueLog::Enable($sTestErrorLogPath);
try {
$this->InvokeNonPublicStaticMethod(CMDBSource::class, 'LogDeadLock', [$oDeadlockException, true, false]);
$sLastErrorLogLine = $this->GetErrorLogLastLines($sTestErrorLogPath, 10); // we are getting multiple lines as the context log introduced multiple lines per log
$this->assertStringContainsString(LogChannels::DEADLOCK, $sLastErrorLogLine);
$this->assertStringContainsString($sExceptionMessage, $sLastErrorLogLine);
} finally {
if (file_exists($sTestErrorLogPath)) {
unlink($sTestErrorLogPath);
}
}
}
private function GetErrorLogLastLines(string $sErrorLogPath, int $iLineNumbers = 1): string
{
return trim(implode("", array_slice(file($sErrorLogPath), -$iLineNumbers)));
}
}

View File

@@ -14,10 +14,6 @@ use MetaModel;
use MySQLTransactionNotClosedException;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*
* @group itopRequestMgmt
* @group specificOrgInSampleData
* Class TransactionsTest
@@ -108,6 +104,15 @@ class TransactionsTest extends ItopTestCase
}
}
/**
* This test case was originaly in DBInsertProvider
* @runInSeparateProcess Failing when run in the same process as other...
*/
public function testDBInsertCaseHistory38()
{
$this->testDBInsert(40, false);
}
public function DBInsertProvider()
{
return [
@@ -151,7 +156,6 @@ class TransactionsTest extends ItopTestCase
"History 35" => ['iFailAt' => 37, 'bIsInDB' => false],
"History 36" => ['iFailAt' => 38, 'bIsInDB' => false],
"History 37" => ['iFailAt' => 39, 'bIsInDB' => false],
"History 38" => ['iFailAt' => 40, 'bIsInDB' => false],
];
}
@@ -276,6 +280,7 @@ class TransactionsTest extends ItopTestCase
protected function tearDown(): void
{
try {
DbConnectionWrapper::SetDbConnectionMockForQuery();
parent::tearDown();
}
catch (MySQLTransactionNotClosedException $e) {

View File

@@ -21,11 +21,6 @@ use Person;
use Team;
use utils;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class CRUDEventTest extends ItopDataTestCase
{
const USE_TRANSACTION = true;
@@ -37,6 +32,8 @@ class CRUDEventTest extends ItopDataTestCase
protected function setUp(): void
{
static::$aEventCalls = [];
static::$iEventCalls = 0;
parent::setUp();
}
@@ -54,7 +51,7 @@ class CRUDEventTest extends ItopDataTestCase
*/
public function testDBInsert()
{
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
$oEventReceiver->RegisterCRUDListeners();
$oOrg = $this->CreateOrganization('Organization1');
@@ -77,7 +74,7 @@ class CRUDEventTest extends ItopDataTestCase
$oOrg = $this->CreateOrganization('Organization1');
$this->assertIsObject($oOrg);
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
$oEventReceiver->RegisterCRUDListeners();
$oOrg->Set('name', 'test');
@@ -101,7 +98,7 @@ class CRUDEventTest extends ItopDataTestCase
$oOrg = $this->CreateOrganization('Organization1');
$this->assertIsObject($oOrg);
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
$oEventReceiver->RegisterCRUDListeners();
$oOrg->DBUpdate();
@@ -122,7 +119,7 @@ class CRUDEventTest extends ItopDataTestCase
*/
public function testComputeValuesOnInsert()
{
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
// Set the person's first name during Compute Values
$oEventReceiver->AddCallback(EVENT_DB_COMPUTE_VALUES, Person::class, 'SetPersonFirstName');
$oEventReceiver->RegisterCRUDListeners(EVENT_DB_COMPUTE_VALUES);
@@ -155,7 +152,7 @@ class CRUDEventTest extends ItopDataTestCase
$oPerson = $this->CreatePerson(1);
$this->assertIsObject($oPerson);
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
// Set the person's first name during Compute Values
$oEventReceiver->AddCallback(EVENT_DB_COMPUTE_VALUES, Person::class, 'SetPersonFirstName');
$oEventReceiver->RegisterCRUDListeners(EVENT_DB_COMPUTE_VALUES);
@@ -180,7 +177,7 @@ class CRUDEventTest extends ItopDataTestCase
*/
public function testCheckToWriteProtectedOnInsert()
{
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
// Modify the person's function
$oEventReceiver->AddCallback(EVENT_DB_CHECK_TO_WRITE, Person::class, 'SetPersonFunction');
$oEventReceiver->RegisterCRUDListeners(EVENT_DB_CHECK_TO_WRITE);
@@ -201,7 +198,7 @@ class CRUDEventTest extends ItopDataTestCase
$this->assertIsObject($oPerson);
// Modify the person's function
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
$oEventReceiver->AddCallback(EVENT_DB_CHECK_TO_WRITE, Person::class, 'SetPersonFunction');
$oEventReceiver->RegisterCRUDListeners(EVENT_DB_CHECK_TO_WRITE);
@@ -221,7 +218,7 @@ class CRUDEventTest extends ItopDataTestCase
*/
public function testModificationsDuringCreateDone()
{
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
// Set the person's first name during Compute Values
$oEventReceiver->AddCallback(EVENT_DB_AFTER_WRITE, Person::class, 'SetPersonFirstName');
$oEventReceiver->RegisterCRUDListeners();
@@ -254,7 +251,7 @@ class CRUDEventTest extends ItopDataTestCase
$oPerson = $this->CreatePerson(1);
$this->assertIsObject($oPerson);
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
// Set the person's first name during Compute Values
$oEventReceiver->AddCallback(EVENT_DB_AFTER_WRITE, Person::class, 'SetPersonFirstName');
$oEventReceiver->RegisterCRUDListeners();
@@ -287,7 +284,7 @@ class CRUDEventTest extends ItopDataTestCase
$oPerson = $this->CreatePerson(1);
$this->assertIsObject($oPerson);
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
// Set the person's first name during Compute Values
$oEventReceiver->AddCallback(EVENT_DB_AFTER_WRITE, Person::class, 'SetPersonFirstName', 100);
$oEventReceiver->RegisterCRUDListeners(EVENT_DB_AFTER_WRITE);
@@ -329,7 +326,7 @@ class CRUDEventTest extends ItopDataTestCase
$this->debug("\n-------------> Test Starts HERE\n");
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
$oEventReceiver->RegisterCRUDListeners();
$oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'persons_list' => $oLinkSet, 'org_id' => $this->getTestOrgId()]);
@@ -375,7 +372,7 @@ class CRUDEventTest extends ItopDataTestCase
$oLinkSet->AddItem($oLink);
$this->debug("\n-------------> Test Starts HERE\n");
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
// Create a new role and add it to the newly created lnkPersonToTeam
$oEventReceiver->AddCallback(EVENT_DB_AFTER_WRITE, lnkPersonToTeam::class, 'AddRoleToLink');
$oEventReceiver->RegisterCRUDListeners();
@@ -411,13 +408,13 @@ class CRUDEventTest extends ItopDataTestCase
*/
public function testPostponedUpdates()
{
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
// Set the person's function after the creation
$oEventReceiver->AddCallback(EVENT_DB_AFTER_WRITE, Person::class, 'SetPersonFunction');
$oEventReceiver->RegisterCRUDListeners(EVENT_DB_AFTER_WRITE);
// Intentionally register twice so 2 modifications will be done
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
$oEventReceiver->AddCallback(EVENT_DB_AFTER_WRITE, Person::class, 'SetPersonFirstName');
$oEventReceiver->RegisterCRUDListeners(EVENT_DB_AFTER_WRITE);
@@ -444,7 +441,7 @@ class CRUDEventTest extends ItopDataTestCase
public function testCrudStack()
{
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
// Modify the person's function
$oEventReceiver->AddCallback(EVENT_DB_COMPUTE_VALUES, Person::class, 'CheckCrudStack');
$oEventReceiver->RegisterCRUDListeners(EVENT_DB_COMPUTE_VALUES);
@@ -490,7 +487,7 @@ class CRUDEventTest extends ItopDataTestCase
$oTeam->DBInsert();
// Start receiving events
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
$oEventReceiver->RegisterCRUDListeners();
// Create a link between Person and Team => generate 2 EVENT_DB_LINKS_CHANGED
@@ -514,7 +511,7 @@ class CRUDEventTest extends ItopDataTestCase
$oLnk->DBInsert();
// Start receiving events
$oEventReceiver = new CRUDEventReceiver();
$oEventReceiver = new CRUDEventReceiver($this);
$oEventReceiver->RegisterCRUDListeners();
$oLnk->DBDelete();
@@ -553,10 +550,16 @@ class ClassesWithDebug
*/
class CRUDEventReceiver extends ClassesWithDebug
{
private $oTestCase;
private $aCallbacks = [];
public static $bIsObjectInCrudStack;
public function __construct(ItopDataTestCase $oTestCase)
{
$this->oTestCase = $oTestCase;
}
//
/**
@@ -613,30 +616,21 @@ class CRUDEventReceiver extends ClassesWithDebug
{
$this->Debug('Registering Test event listeners');
if (is_null($sEvent)) {
EventService::RegisterListener(EVENT_DB_COMPUTE_VALUES, [$this, 'OnEvent']);
EventService::RegisterListener(EVENT_DB_CHECK_TO_WRITE, [$this, 'OnEvent']);
EventService::RegisterListener(EVENT_DB_CHECK_TO_DELETE, [$this, 'OnEvent']);
EventService::RegisterListener(EVENT_DB_BEFORE_WRITE, [$this, 'OnEvent']);
EventService::RegisterListener(EVENT_DB_AFTER_WRITE, [$this, 'OnEvent']);
EventService::RegisterListener(EVENT_DB_AFTER_DELETE, [$this, 'OnEvent']);
EventService::RegisterListener(EVENT_DB_LINKS_CHANGED, [$this, 'OnEvent']);
$this->oTestCase->EventService_RegisterListener(EVENT_DB_COMPUTE_VALUES, [$this, 'OnEvent']);
$this->oTestCase->EventService_RegisterListener(EVENT_DB_CHECK_TO_WRITE, [$this, 'OnEvent']);
$this->oTestCase->EventService_RegisterListener(EVENT_DB_CHECK_TO_DELETE, [$this, 'OnEvent']);
$this->oTestCase->EventService_RegisterListener(EVENT_DB_BEFORE_WRITE, [$this, 'OnEvent']);
$this->oTestCase->EventService_RegisterListener(EVENT_DB_AFTER_WRITE, [$this, 'OnEvent']);
$this->oTestCase->EventService_RegisterListener(EVENT_DB_AFTER_DELETE, [$this, 'OnEvent']);
$this->oTestCase->EventService_RegisterListener(EVENT_DB_LINKS_CHANGED, [$this, 'OnEvent']);
return;
}
EventService::RegisterListener($sEvent, [$this, 'OnEvent'], $mEventSource);
$this->oTestCase->EventService_RegisterListener($sEvent, [$this, 'OnEvent'], $mEventSource);
}
/**
* @param $oObject
*
* @return void
* @throws \ArchivedObjectException
* @throws \CoreCannotSaveObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \CoreWarning
* @throws \MySQLException
* @throws \OQLException
* @noinspection PhpUnusedPrivateMethodInspection Used as a callback
*/
private function AddRoleToLink($oObject): void
{
@@ -646,26 +640,30 @@ class CRUDEventReceiver extends ClassesWithDebug
$oObject->Set('role_id', $oContactType->GetKey());
}
/**
* @noinspection PhpUnusedPrivateMethodInspection Used as a callback
*/
private function SetPersonFunction($oObject): void
{
$this->Debug(__METHOD__);
$oObject->Set('function', 'CRUD_function_'.rand());
}
/**
* @noinspection PhpUnusedPrivateMethodInspection Used as a callback
*/
private function SetPersonFirstName($oObject): void
{
$this->Debug(__METHOD__);
$oObject->Set('first_name', 'CRUD_first_name_'.rand());
}
/**
* @noinspection PhpUnusedPrivateMethodInspection Used as a callback
*/
private function CheckCrudStack(DBObject $oObject): void
{
self::$bIsObjectInCrudStack = DBObject::IsObjectCurrentlyInCrud(get_class($oObject), $oObject->GetKey());
}
private function CheckUpdateInLnk(lnkPersonToTeam $oLnkPersonToTeam)
{
$iTeamId = $oLnkPersonToTeam->Get('team_id');
self::$bIsObjectInCrudStack = DBObject::IsObjectCurrentlyInCrud(Team::class, $iTeamId);
}
}

View File

@@ -26,6 +26,7 @@
namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use CoreException;
use DBObject;
@@ -36,19 +37,20 @@ use MetaModel;
/**
* @group specificOrgInSampleData
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class DBObjectTest extends ItopDataTestCase
{
const CREATE_TEST_ORG = true;
// Counts
public $aReloadCount = [];
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('core/dbobject.class.php');
$this->EventService_RegisterListener(EVENT_DB_OBJECT_RELOAD, [$this, 'CountObjectReload']);
}
/**
@@ -897,4 +899,46 @@ class DBObjectTest extends ItopDataTestCase
return $oPerson;
}
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;
}
}

View File

@@ -17,10 +17,6 @@ use DBSearch;
* <ul>
* <li>MakeGroupByQuery</li>
* </ul>
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class DBSearchCommitTest extends ItopDataTestCase
{

View File

@@ -11,10 +11,6 @@ use DBSearch;
* Class DBSearchIntersectTest
*
* @package Combodo\iTop\Test\UnitTest\Core
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class DBSearchIntersectTest extends ItopTestCase
{
@@ -71,16 +67,16 @@ class DBSearchIntersectTest extends ItopTestCase
'result' => "SELECT `L`, `P` FROM Person AS `P` JOIN Location AS `L` ON `P`.location_id = `L`.id WHERE (`P`.`org_id` = 3)");
$aTests['Multiple selected classes inverted 1'] = array(
'left' => "SELECT `L`, `P`, `D` FROM Person AS `P` JOIN Location AS `L` ON `P`.location_id = `L`.id JOIN PC AS D ON D.location_id = L.id JOIN Person AS P2 ON P.manager_id = P2.id WHERE 1",
'left' => "SELECT `L`, `P`, `D` FROM Person AS `P` JOIN Location AS `L` ON `P`.location_id = `L`.id JOIN Server AS D ON D.location_id = L.id JOIN Person AS P2 ON P.manager_id = P2.id WHERE 1",
'right' => "SELECT Location WHERE org_id = 3",
'alias' => "L",
'result' => "SELECT `L`, `P`, `D` FROM Person AS `P` JOIN Location AS `L` ON `P`.location_id = `L`.id JOIN PC AS `D` ON `D`.location_id = `L`.id JOIN Person AS `P2` ON `P`.manager_id = `P2`.id WHERE (`L`.`org_id` = 3)");
'result' => "SELECT `L`, `P`, `D` FROM Person AS `P` JOIN Location AS `L` ON `P`.location_id = `L`.id JOIN Server AS `D` ON `D`.location_id = `L`.id JOIN Person AS `P2` ON `P`.manager_id = `P2`.id WHERE (`L`.`org_id` = 3)");
$aTests['Multiple selected classes inverted 2'] = array(
'left' => "SELECT `L`, `P`, `D` FROM Person AS `P` JOIN Location AS `L` ON `P`.location_id = `L`.id JOIN PC AS D ON D.location_id = L.id JOIN Person AS P2 ON P.manager_id = P2.id WHERE (`L`.`org_id` = 3)",
'left' => "SELECT `L`, `P`, `D` FROM Person AS `P` JOIN Location AS `L` ON `P`.location_id = `L`.id JOIN Server AS D ON D.location_id = L.id JOIN Person AS P2 ON P.manager_id = P2.id WHERE (`L`.`org_id` = 3)",
'right' => "SELECT Person WHERE org_id = 3",
'alias' => "P",
'result' => "SELECT `L`, `P`, `D` FROM Person AS `P` JOIN Location AS `L` ON `P`.location_id = `L`.id JOIN PC AS `D` ON `D`.location_id = `L`.id JOIN Person AS `P2` ON `P`.manager_id = `P2`.id WHERE ((`L`.`org_id` = 3) AND (`P`.`org_id` = 3))");
'result' => "SELECT `L`, `P`, `D` FROM Person AS `P` JOIN Location AS `L` ON `P`.location_id = `L`.id JOIN Server AS `D` ON `D`.location_id = `L`.id JOIN Person AS `P2` ON `P`.manager_id = `P2`.id WHERE ((`L`.`org_id` = 3) AND (`P`.`org_id` = 3))");
$aTests['Same class'] = array(
'left' => "SELECT Contact WHERE name = 'Christie'",
@@ -191,9 +187,9 @@ class DBSearchIntersectTest extends ItopTestCase
'result' => "SELECT `L`, `P` FROM Person AS `P` JOIN Location AS `L` ON `P`.location_id = `L`.id WHERE (`P`.`org_id` = 3)");
$aTests['Multiple selected classes inverted 2'] = array(
'left' => "SELECT `L`, `P`, `D` FROM Person AS `P` JOIN Location AS `L` ON `P`.location_id = `L`.id JOIN PC AS D ON D.location_id = L.id JOIN Person AS P2 ON P.manager_id = P2.id WHERE (`L`.`org_id` = 3)",
'left' => "SELECT `L`, `P`, `D` FROM Person AS `P` JOIN Location AS `L` ON `P`.location_id = `L`.id JOIN Server AS D ON D.location_id = L.id JOIN Person AS P2 ON P.manager_id = P2.id WHERE (`L`.`org_id` = 3)",
'right' => "SELECT Person WHERE org_id = 3",
'result' => "SELECT `L`, `P`, `D` FROM Person AS `P` JOIN Location AS `L` ON `P`.location_id = `L`.id JOIN PC AS `D` ON `D`.location_id = `L`.id JOIN Person AS `P2` ON `P`.manager_id = `P2`.id WHERE ((`L`.`org_id` = 3) AND (`P`.`org_id` = 3))");
'result' => "SELECT `L`, `P`, `D` FROM Person AS `P` JOIN Location AS `L` ON `P`.location_id = `L`.id JOIN Server AS `D` ON `D`.location_id = `L`.id JOIN Person AS `P2` ON `P`.manager_id = `P2`.id WHERE ((`L`.`org_id` = 3) AND (`P`.`org_id` = 3))");
$aTests['Same class'] = array(
'left' => "SELECT Contact WHERE name = 'Christie'",

View File

@@ -11,10 +11,6 @@ use DBSearch;
* Class DBSearchIntersectTest
*
* @package Combodo\iTop\Test\UnitTest\Core
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class DBSearchJoinTest extends ItopDataTestCase {

View File

@@ -43,10 +43,6 @@ use FunctionExpression;
* <ul>
* <li>MakeGroupByQuery</li>
* </ul>
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class DBSearchTest extends ItopDataTestCase
{

View File

@@ -10,10 +10,6 @@ use DBObjectSearch;
* Class DBSearchUpdateRealiasingMapTest
*
* @package Combodo\iTop\Test\UnitTest\Core
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class DBSearchUpdateRealiasingMapTest extends ItopDataTestCase
{

View File

@@ -7,11 +7,6 @@
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class DBUnionSearchTest extends ItopDataTestCase
{

View File

@@ -5,7 +5,7 @@ namespace Combodo\iTop\Test\UnitTest\Core;
use CMDBSource;
use Combodo\iTop\Test\UnitTest\iTopDataTestCase;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use DateInterval;
use DateTime;
use Expression;
@@ -13,12 +13,7 @@ use FunctionExpression;
use MetaModel;
use ScalarExpression;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class ExpressionEvaluateTest extends iTopDataTestCase
class ExpressionEvaluateTest extends ItopDataTestCase
{
const USE_TRANSACTION = false;
@@ -38,7 +33,7 @@ class ExpressionEvaluateTest extends iTopDataTestCase
$aParameters = $oExpression->GetParameters($sParentFilter);
sort($aExpectedParameters);
sort($aParameters);
static::assertEquals($aExpectedParameters, $aParameters);
$this->assertEquals($aExpectedParameters, $aParameters);
}
public function GetParametersProvider()
@@ -86,7 +81,7 @@ class ExpressionEvaluateTest extends iTopDataTestCase
{
$oExpression = Expression::FromOQL($sExpression);
$value = $oExpression->Evaluate(array());
static::assertEquals($expectedValue, $value);
$this->assertEquals($expectedValue, $value);
}
public function VariousExpressionsProvider()
@@ -225,7 +220,7 @@ class ExpressionEvaluateTest extends iTopDataTestCase
$sNewExpression = "return $sExpression;";
$oExpression = eval($sNewExpression);
$res = $oExpression->Evaluate(array());
static::assertEquals($expectedValue, $res);
$this->assertEquals($expectedValue, $res);
}
public function NotYetParsableExpressionsProvider()
@@ -287,7 +282,7 @@ class ExpressionEvaluateTest extends iTopDataTestCase
$value = $aResults[0]["test_$i"];
$expectedValue = $aTest[1];
$this->debug("Test #$i: {$aTests[$i][0]} => ".var_export($value, true));
static::assertEquals($expectedValue, $value);
$this->assertEquals($expectedValue, $value);
}
}
@@ -310,7 +305,7 @@ class ExpressionEvaluateTest extends iTopDataTestCase
$res = $oObject->EvaluateExpression($oExpression);
static::assertEquals($expected, $res);
$this->assertEquals($expected, $res);
}
public function ExpressionsWithObjectFieldsProvider()
@@ -340,12 +335,18 @@ class ExpressionEvaluateTest extends iTopDataTestCase
{
$oExpression = Expression::FromOQL($sExpression);
$res = $oExpression->Evaluate($aParameters);
static::assertEquals($expected, $res);
$this->assertEquals($expected, $res);
}
public function ExpressionWithParametersProvider()
{
return array(
['`DBVariables["analyze_sample_percentage"]` > 10', ['DBVariables["analyze_sample_percentage"]' => 20], true],
['`DataBase["DBDataSize"]`', ['DataBase["DBDataSize"]' => 4096], 4096],
['`FileSystem["ItopInstallationIntegrity"]`', ['FileSystem["ItopInstallationIntegrity"]' => 'not_conform'], 'not_conform'],
['`DBTablesInfo["attachment"].DataSize` > 100', ['DBTablesInfo["attachment"].DataSize' => 200], true],
['`DBTablesInfo[].DataSize` > 100', ['DBTablesInfo[].DataSize' => 50], false],
['(`DBTablesInfo[].DataSize` > 100) AND (`DBTablesInfo[].DataFree` * 100 / (`DBTablesInfo[].DataSize` + `DBTablesInfo[].IndexSize` + `DBTablesInfo[].DataFree`) > 10)', ['DBTablesInfo[].DataSize' => 200, 'DBTablesInfo[].DataFree' => 100, 'DBTablesInfo[].IndexSize' => 10], true],
array('CONCAT(SUBSTR(name, 4), " cause")', array('name' => 'noble'), 'le cause'),
);
}
@@ -369,11 +370,11 @@ class ExpressionEvaluateTest extends iTopDataTestCase
$res = $oExpression->IsTrue();
if ($bExpectTrue)
{
static::assertTrue($res, 'arg: '.$sExpression);
$this->assertTrue($res, 'arg: '.$sExpression);
}
else
{
static::assertFalse($res, 'arg: '.$sExpression);
$this->assertFalse($res, 'arg: '.$sExpression);
}
}
@@ -414,10 +415,10 @@ class ExpressionEvaluateTest extends iTopDataTestCase
if ($bProcessed)
{
$sqlValue = CMDBSource::QueryToScalar("SELECT DATE_FORMAT('$sDate', '%$sFormat')");
static::assertEquals($sqlValue, $sValueOrException, 'Check test against MySQL');
$this->assertEquals($sqlValue, $sValueOrException, 'Check test against MySQL');
$res = $oExpression->Evaluate(array());
static::assertEquals($sValueOrException, $res, 'Check evaluation');
$this->assertEquals($sValueOrException, $res, 'Check evaluation');
}
else
{
@@ -510,7 +511,7 @@ class ExpressionEvaluateTest extends iTopDataTestCase
{
$oExpression = new FunctionExpression('DATE_FORMAT', array(new ScalarExpression($sDate), new ScalarExpression("%$sFormat")));
$itopExpressionResult = $oExpression->Evaluate(array());
static::assertSame($aMysqlDateFormatRsultsForAllFormats[$sFormat], $itopExpressionResult, "Format %$sFormat not matching MySQL for '$sDate'");
$this->assertSame($aMysqlDateFormatRsultsForAllFormats[$sFormat], $itopExpressionResult, "Format %$sFormat not matching MySQL for '$sDate'");
}
}
}
@@ -546,7 +547,7 @@ class ExpressionEvaluateTest extends iTopDataTestCase
$oDate = new DateTime($sStartDate);
for ($i = 0 ; $i < $iRepeat ; $i++)
{
$sDate = date_format($oDate, 'Y-m-d, H:i:s');
$sDate = date_format($oDate, 'Y-m-d H:i:s');
$this->debug("Checking '$sDate'");
$this->testEveryTimeFormat($sDate);
$oDate->add(new DateInterval($sInterval));

View File

@@ -5,11 +5,6 @@ namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Expression;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class ExpressionTest extends ItopDataTestCase
{
const USE_TRANSACTION = false;

View File

@@ -20,10 +20,6 @@ use UserRightsProfile;
* @group specificOrgInSampleData
* Class GetSelectFilterTest
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*
* @package Combodo\iTop\Test\UnitTest\Webservices
*/
class GetSelectFilterTest extends ItopDataTestCase

View File

@@ -10,12 +10,6 @@ namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use InlineImage;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class InlineImageTest extends ItopDataTestCase
{
/**

View File

@@ -39,6 +39,8 @@ class DeprecatedCallsLogTest extends ItopTestCase {
}
/**
* @runInSeparateProcess Necessary, due to the DeprecatedCallsLog being enabled (no mean to reset)
*
* The error handler set by DeprecatedCallsLog during startup was causing PHPUnit to miss PHP notices like "undefined offset"
*
* The error handler is now disabled when running PHPUnit

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