Compare commits

..

121 Commits

Author SHA1 Message Date
odain
e844b4c3ce 6912- add test 2023-11-03 14:31:19 +01:00
odain
3eee03d504 N°6912 - Provision user without contact 2023-11-03 14:15:57 +01:00
odain-cbd
eaa80c5396 N°6824 - Notification with current_contact placeholder trigger hundred of email sent (#562)
Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
Co-authored-by: Thomas Casteleyn <thomas.casteleyn@super-visions.com>
Co-authored-by: Romain Quetiez <romain.quetiez@combodo.com>
2023-10-31 16:17:57 +01:00
Molkobain
3aec6bff79 N°6866 - Fix regression from previous merge 2023-10-31 16:13:54 +01:00
Molkobain
83313ce1d5 N°6866 - Fix usages of hard-coded "listInObject" to \DisplayBlock::ENUM_STYLE_LIST_IN_OBJECT 2023-10-31 14:01:19 +01:00
Molkobain
8aa3dcdaa7 N°6866 - Fix regression from previous merge 2023-10-31 13:56:17 +01:00
Molkobain
306d8136ef Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts:
#	sources/Application/UI/Base/Component/DataTable/DataTableUIBlockFactory.php
2023-10-31 11:16:13 +01:00
Molkobain
cbb37f27d7 N°6866 - Fix issue when creating new fields in Request Template in French 2023-10-31 11:10:54 +01:00
odain-cbd
e78fa18359 N°6849 - Enhance setup error message in case of unmet module dependencies (#557)
* N°6849 - add cross/checkmark emoj to missing depenency message and remove unmet word

* N°6849 - PR advice to avoid modifying aDeps
2023-10-31 11:07:29 +01:00
Benjamin Dalsass
1385a3dc03 N°6774 - Risque d'erreur de chargement des relations dans le portail en visualisation 2023-10-31 10:18:21 +01:00
Benjamin Dalsass
789f0c826b N°6557 - PHP 8.1 : Deprecated Constant when adding a contact to ticket on ticket creation 2023-10-31 09:34:00 +01:00
Molkobain
ac070b0cbe Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-10-30 16:58:43 +01:00
Molkobain
e21dc4d21c Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-10-30 16:55:45 +01:00
Molkobain
a49a4e6c2b N°6886 - Add OAuth tests folder to removable directories list 2023-10-30 16:54:28 +01:00
Stephen Abello
95aa4afe75 N°6385 - Allow to disable LinkedSet (1:n & n:n) edition by XML in host edition 2023-10-30 16:29:25 +01:00
bdalsass
74004fa375 N°6651 - 3.1 bulk modify n:n like Tagset: checkbox ignored (#552)
- handle selectize enable/disable to update operation hidden input state
- don't execute elements click callback when input disabled
- merge current and initial values for the initial value
2023-10-30 13:57:41 +01:00
vdumas
d2fc87c6f9 N°6884 - Error on creating User from a Person edit screen 2023-10-30 12:22:07 +01:00
Romain Quetiez
5264a1f10d New test related to the build of SQL queries 2023-10-27 16:01:57 +02:00
Romain Quetiez
f0199a4cf2 Optimize tests execution time (no need for process isolation as long as we leave the premises clean) 2023-10-27 15:50:17 +02:00
Romain Quetiez
f8877ef3e7 Optimize tests execution time (no need for process isolation as long as we leave the premises clean) 2023-10-27 15:49:58 +02:00
Romain Quetiez
00b1156526 Optimize tests execution time (force cache usage and set the modification date in the past instead of waiting 1s) 2023-10-27 15:45:28 +02:00
Romain Quetiez
39d2ba8d1b Optimize tests execution time (no need for process isolation as long as we leave the premises clean) 2023-10-27 15:43:11 +02:00
Romain Quetiez
322adcb180 Optimize tests execution time (no need for process isolation as long as we leave the premises clean) 2023-10-27 14:27:00 +02:00
Romain Quetiez
77e7685c90 Optimize tests execution time (no need for process isolation as long as we leave the premises clean) 2023-10-27 14:24:31 +02:00
Romain Quetiez
3c14e5e032 Optimize tests execution time (no need for process isolation as long as we leave the premises clean) 2023-10-27 14:24:16 +02:00
Romain Quetiez
f58ec7e38e Optimize tests execution time (no need for process isolation as long as we leave the premises clean) 2023-10-27 14:22:20 +02:00
Romain Quetiez
73fd0b06b2 Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts:
#	tests/php-unit-tests/integration-tests/DictionariesConsistencyTest.php
#	tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php
#	tests/php-unit-tests/unitary-tests/core/CMDBSource/TransactionsTest.php
#	tests/php-unit-tests/unitary-tests/sources/Application/TwigBase/Twig/TwigTest.php
2023-10-27 14:12:06 +02:00
Romain Quetiez
8fa9336568 Fix regression introduced with the optimization done in 15148f7, and seen only in the context of the CI 2023-10-27 11:21:56 +02:00
Romain Quetiez
15148f7d1d Fix regression introduced with the optimization done in 798cd10, and seen only if APC is enabled 2023-10-27 10:43:13 +02:00
Romain Quetiez
7e8589ba95 Fix regression introduced with the optimization done in d641504. Cope with the fact that sometimes the admin account already exists, sometimes not. 2023-10-27 09:21:36 +02:00
Romain Quetiez
fba668207f Optimize tests execution time (test rework and defensive cleanup) 2023-10-26 21:35:52 +02:00
Romain Quetiez
798cd10d6b Optimize tests execution time (no need for process isolation as long as we leave the premises clean) 2023-10-26 21:23:47 +02:00
Romain Quetiez
442721bcb5 Optimize tests execution time (no need for process isolation as long as we leave the premises clean) 2023-10-26 21:22:54 +02:00
Romain Quetiez
1a9049d277 Optimize tests execution time (no need for process isolation as long as we leave the premises clean) 2023-10-26 21:16:24 +02:00
Romain Quetiez
c0931af91a Optimize tests execution time (no need for process isolation as long as we leave the premises clean, set file modification date instead of waiting for 1 second) 2023-10-26 21:15:57 +02:00
Romain Quetiez
29e9a06dc1 Optimize tests execution time (no need for process isolation as long as we leave the premises clean) 2023-10-26 21:10:47 +02:00
Romain Quetiez
d6415042ae Optimize tests execution time (no need for process isolation as long as we leave the premises clean) 2023-10-26 21:10:07 +02:00
Romain Quetiez
90006667fe Optimize tests execution time (copy fixture files only when necessary) 2023-10-26 20:58:26 +02:00
Romain Quetiez
fd351df08b Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-10-26 20:53:24 +02:00
Romain Quetiez
7419749ba6 Prerequisites for boosting tests 2023-10-26 20:51:28 +02:00
Romain Quetiez
73bed04555 Twig tests not executed 2023-10-26 10:47:11 +02:00
Romain Quetiez
8893cdac1d Optimize tests execution time (no need for process isolation as long as we leave the premises clean) 2023-10-26 10:44:37 +02:00
Romain Quetiez
7f245a15be Optimize tests execution time (suppress meaningless test and merge two test in one, while preserving test coverage) 2023-10-25 23:01:05 +02:00
Romain Quetiez
b5c46ccd4a Optimize tests execution time (x10 / no need for a systematic check of date formats, which was ok as a first approach) 2023-10-25 22:59:03 +02:00
Romain Quetiez
7fbc211c43 Optimize tests execution time (x50 / eval is way faster than exec) 2023-10-25 22:57:03 +02:00
Romain Quetiez
cf774cdb90 Explain why process isolation is a must 2023-10-25 22:18:05 +02:00
Romain Quetiez
722a58491c Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php
2023-10-25 22:08:08 +02:00
Romain Quetiez
037dfe1df6 Optimize tests execution time 2023-10-25 17:51:12 +02:00
Romain Quetiez
0b26d45014 Prerequisites for boosting tests 2023-10-25 17:50:41 +02:00
Eric Espie
44e826543d N°6870 - Community re-integration support 2023-10-25 14:30:16 +02:00
Eric Espie
50f5ab6be4 Fix ModelFactory tests 2023-10-25 14:03:26 +02:00
Eric Espie
941412a365 Community integration: changed default behaviour for datamodel XML nodes without _delta from 'must_exist' to 'merge' 2023-10-25 10:05:46 +02:00
Stephen Abello
a9bd62dc43 N°6385 - Allow to disable LinkedSet (1:n & n:n) edition by XML
Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2023-10-24 11:09:12 +02:00
Stephen Abello
4f336abeb8 N°6861 - Display warning message when creating/editing a mandatory blob in modal 2023-10-23 15:51:38 +02:00
Molkobain
40d1ae0b1c Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-10-23 15:17:43 +02:00
Molkobain
b9c566238a Merge remote-tracking branch 'origin/support/2.7' into support/3.0
Remove PHPUnit annotations as from support/3.0 and newer they are no longer necessary
2023-10-23 15:09:49 +02:00
Molkobain
4fd8177165 N°3715 - Fix unit tests 2023-10-23 14:55:06 +02:00
Anne-Cath
c597c34e5c Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-10-20 17:06:11 +02:00
Anne-Cath
0cc0f39d9e Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-10-20 17:04:26 +02:00
Anne-Catherine
a2cdf214f0 N°3715 - Export above 1000 entries ignore obsolete data from user preference (#468)
* N°3715 - Export above 10000 entries ignore obsolete data from user preference
2023-10-20 17:02:16 +02:00
Anne-Cath
189fd1d9e3 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-10-20 16:55:41 +02:00
Anne-Cath
4f75d012e5 Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-10-20 16:54:37 +02:00
Anne-Catherine
013173019f N°2909 - Search on Enum, Date, TagSet,... with index fails (#496) 2023-10-20 16:45:35 +02:00
Anne-Catherine
ed8d4df5ef N°5786 - Problem with text color in public log and in AttributeHTML (#527)
* N°5786 - Problem with text color in public log and in AttributeHTML
2023-10-17 12:09:52 +02:00
Pierre Goiffon
31a1370028 Merge branch 'support/3.1.0' into support/3.1
# Conflicts:
#	core/DbConnectionWrapper.php
#	tests/php-unit-tests/unitary-tests/core/CMDBSource/TransactionsTest.php
2023-10-17 11:13:29 +02:00
Pierre Goiffon
f36f3aa05b 💡 N°6848 Fix PHPDoc 2023-10-17 11:12:38 +02:00
Pierre Goiffon
d0d90d7c69 N°6848 Fix TransactionsTest::testTransactionOpenedNotClosed failing
Was caused by 239c51bb, which adds 65bb76b9 tests improvements but only partly : we were missing in TransactionsTest::tearDown the mysqli reset from mock to original mysqli cnx

Without this reset the rollback made in ItopTestCase::tearDown is throwing an exception (query() method on the DbConnectionWrapper cnx returns false in \CMDBSource::DBQuery) on PHP 8.1 and 8.2

Co-authored-by: Romain Quetiez <romain.queriez@combodo.com>
2023-10-17 10:53:26 +02:00
Stephen Abello
aa618468d1 Merge branch 'support/3.0' into support/3.1 2023-10-17 09:23:06 +02:00
Stephen Abello
fadfd94bac Merge branch 'support/2.7' into support/3.0 2023-10-17 09:19:16 +02:00
Stephen Abello
9469681a0c N°6777 - Security hardening 2023-10-17 09:12:40 +02:00
Pierre Goiffon
d4ab55dd9a Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts:
#	pages/ajax.render.php
2023-10-13 17:33:26 +02:00
Pierre Goiffon
da27ddba82 Merge remote-tracking branch 'origin/support/2.7' into support/3.0
# Conflicts:
#	application/utils.inc.php
#	pages/ajax.render.php
2023-10-13 17:27:03 +02:00
Pierre Goiffon
c72cb7e70e N°6606 security hardening 2023-10-13 17:15:37 +02:00
Pierre Goiffon
9df92665e0 N°6606 Backport of utils::ENUM_SANITIZATION_FILTER_* constants
Were introduced in 3.0.0, but not added to the support/2.7 branch
2023-10-13 17:10:35 +02:00
Molkobain
819baa3951 Merge remote-tracking branch 'origin/support/3.1.0' into support/3.1 2023-10-06 17:47:52 +02:00
Eric Espie
c78024394e Better log of twig syntax errors 2023-10-06 15:25:27 +02:00
Eric Espie
4267f2b855 N°6747 - Customizing UserLDAP, generates presentation error messages in Designer during MTP 2023-10-06 09:53:26 +02:00
vdumas
ba8f18e1d4 N°6815 - DataModel : wrong attribute type for SLA.customercontracts_list 2023-10-05 18:08:46 +02:00
vdumas
ba13d24206 N°6814 - Datamodel remove lnkConnectableCIToNetworkDevice uniqueness rule 2023-10-05 18:08:46 +02:00
odain
239c51bb53 ci enhancement: complete tearDown to cleanup transactions and cmdb changes properly 2023-10-03 10:09:26 +02:00
Stephen Abello
ab3a4a2468 Merge branch 'support/3.0' into support/3.1
# Conflicts:
#	pages/ajax.render.php
2023-10-02 15:16:18 +02:00
Stephen Abello
3647291475 N°6778 - Security hardening 2023-10-02 15:06:17 +02:00
Molkobain
a472d83e3d Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts:
#	application/wizardhelper.class.inc.php
#	pages/ajax.render.php
2023-09-26 22:21:48 +02:00
Molkobain
6dc6392fab Merge remote-tracking branch 'origin/support/3.0.3' into support/3.0 2023-09-26 22:20:02 +02:00
Anne-Catherine
e793b02f8b N°6766 - Fix dependent fields not updated due to WizardHelper.UpdateFields() being triggered too early (#548)
* N°6766 - Javascript : function WizardHelper.UpdateFields triggered to early does not update fields

* N°6766 - Code review

---------

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2023-09-26 12:25:56 +02:00
Pierre Goiffon
c5cb84f976 N°6733 Fix some attributes impl not prompted in transitions when mandatory 2023-09-26 11:32:06 +02:00
Molkobain
12c0edc530 N°6547 - Fix flags init being an hard-coded int instead of the corresponding constant 2023-09-21 18:25:44 +02:00
Stephen Abello
61565b25a3 N°6767 - Error in ajax request when there's dict to load and no onready scripts 2023-09-21 10:40:22 +02:00
Pierre Goiffon
7619d055dd N°6695 Handle multiline dict entries in portal tooltips (#542)
Report Tippy fix from admin console (/css/backoffice/vendors/_tippy.scss) to user portal.
Thanks DaveHeart  for the bug report !
2023-09-20 18:10:56 +02:00
Molkobain
f01997f2ad Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-09-20 16:16:53 +02:00
Molkobain
fc6e98b534 Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-09-20 16:12:51 +02:00
Molkobain
8ecebee511 PHP unit tests: Fix typo for "final private" methods as they can't be both 2023-09-20 16:11:39 +02:00
Eric Espie
b5e26061e1 Merge branch 'issue/6667_trigger_on_state_enter' into support/3.1 2023-09-20 10:11:58 +02:00
Pierre Goiffon
14fa20b428 Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts:
#	tests/ci_description.ini
2023-09-19 12:30:16 +02:00
Pierre Goiffon
2690fa3315 Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-09-19 12:27:19 +02:00
Pierre Goiffon
35cd965360 N°6629 Update ci_description php_version 2023-09-19 12:25:40 +02:00
Pierre Goiffon
133fc29ad5 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-09-19 09:58:53 +02:00
Pierre Goiffon
83a5b98f82 Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-09-19 09:58:40 +02:00
Pierre Goiffon
e5dd51f637 N°6600 Portal download attachment : don't display anymore SQL query on attachment not found error (#525) 2023-09-19 09:54:43 +02:00
Molkobain
2f6bcc3534 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-09-19 08:41:46 +02:00
Molkobain
4923418f58 Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-09-19 08:39:41 +02:00
Molkobain
0a6c82dfe1 N°6752 - PHP unit tests: Fix typo in postbuild_integration.xml.dist 2023-09-19 08:37:46 +02:00
Molkobain
f89d843ab3 Merge remote-tracking branch 'origin/support/3.0' into support/3.1 2023-09-18 16:06:53 +02:00
Molkobain
2dd7f5cada Merge remote-tracking branch 'origin/support/2.7' into support/3.0 2023-09-18 15:37:32 +02:00
Molkobain
24c0f4950f Add missing .htaccess / web.config files in .gitignore for /extensions folder 2023-09-18 15:26:06 +02:00
Molkobain
d4dbbc59d4 N°6754 - PHP unit tests: Add local PHPUnit XML files to .gitignore 2023-09-18 15:23:52 +02:00
Molkobain
dc0cd44c79 N°6752 - PHP unit tests: Migrate usages of unitestautoload.php to composer autoloader 2023-09-18 15:14:44 +02:00
Pierre Goiffon
d7df249586 Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts:
#	application/ajaxwebpage.class.inc.php
#	application/capturewebpage.class.inc.php
#	application/clipage.class.inc.php
#	application/csvpage.class.inc.php
#	application/errorpage.class.inc.php
#	application/itopwebpage.class.inc.php
#	application/itopwizardwebpage.class.inc.php
#	application/nicewebpage.class.inc.php
#	application/pdfpage.class.inc.php
#	application/webpage.class.inc.php
#	application/xmlpage.class.inc.php
2023-09-18 15:10:19 +02:00
Pierre Goiffon
f3c4fcb0f5 💡 Pages files : add depreciation version 2023-09-18 15:07:32 +02:00
Pierre Goiffon
9a8e9a0b01 N°5491 Fix HU iTopUpdate:Error:MissingFile dict key 2023-09-18 08:45:45 +02:00
vdumas
f6a7e6b4e1 N°3506 - allow_target_creation based on UR_ACTION_MODIFY 2023-09-15 16:40:20 +02:00
Stephen Abello
2829fb291f Update contribution stickers with 2023 design (better late than never 🙃) 2023-09-15 15:43:50 +02:00
odain
6110abfc7f Merge branch 'support/3.0' into support/3.1 2023-09-15 10:08:37 +02:00
odain
6046f44f56 Merge branch 'support/2.7' into support/3.0 2023-09-15 10:08:08 +02:00
odain
6c6131ce03 N°5491 - test enhancement to reduce false positive 2023-09-15 10:07:42 +02:00
Stephen Abello
178c922407 Merge branch 'support/3.0' into support/3.1 2023-09-15 09:57:59 +02:00
Stephen Abello
343e87a8d4 N°6581 - Security hardening 2023-09-15 09:55:51 +02:00
Stephen Abello
d7e5d6fb7f N°6734 - Fix not being able to add dashlets to a dashboard when iBackofficeDictEntriesExtension is used and its JS files are loaded through ajax calls 2023-09-14 17:25:00 +02:00
Pierre Goiffon
993606f102 Merge branch 'support/3.1.0' into support/3.1
# Conflicts:
#	setup/itopdesignformat.class.inc.php
2023-09-14 16:54:28 +02:00
Pierre Goiffon
bdf0b4daa9 N°6562 Fix DOMNode->textContent write
This attribute is read only
Causes layout issues on PHP 8.1.21 and 8.2.8

(cherry picked from commit 734a788340)
(cherry picked from commit 7aa478d6ff)
2023-09-14 15:31:02 +02:00
Eric Espie
cc4af0a027 N°6667 - Ignore trigger on state entering with auto-dispatch 2023-08-22 14:30:18 +02:00
124 changed files with 1586 additions and 686 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

6
.gitignore vendored
View File

@@ -37,7 +37,9 @@ tests/*/vendor/*
# iTop extensions
/extensions/**
!/extensions/.htaccess
!/extensions/readme.txt
!/extensions/web.config
# all logs but listing prevention
/log/**
@@ -45,8 +47,10 @@ tests/*/vendor/*
!/log/index.php
!/log/web.config
# PHPUnit cache file
# PHPUnit: Cache file, local XML working copies
/tests/php-unit-tests/.phpunit.result.cache
/tests/php-unit-tests/phpunit.xml
/tests/php-unit-tests/postbuild_integration.xml
# Jetbrains

View File

@@ -161,4 +161,4 @@ We have one sticker per contribution type. You might get multiple stickers with
Here is the design of each stickers for year 2022:
![iTop stickers 2022](.doc/contributing-guide/2022.contributing-stickers-side-by-side.png)
![iTop stickers 2023](.doc/contributing-guide/2023.contributing-stickers-side-by-side.png)

View File

@@ -502,6 +502,7 @@ class UserRightsProfile extends UserRightsAddOnAPI
}
protected $m_aUserOrgs = array(); // userid -> array of orgid
protected $m_aAdministrators = null; // [user id]
// Built on demand, could be optimized if necessary (doing a query for each attribute that needs to be read)
protected $m_aObjectActionGrants = array();
@@ -558,6 +559,7 @@ class UserRightsProfile extends UserRightsAddOnAPI
// Cache
$this->m_aObjectActionGrants = array();
$this->m_aAdministrators = null;
}
public function LoadCache()
@@ -700,12 +702,10 @@ class UserRightsProfile extends UserRightsAddOnAPI
*/
private function GetAdministrators()
{
static $aAdministrators = null;
if ($aAdministrators === null)
if ($this->m_aAdministrators === null)
{
// Find all administrators
$aAdministrators = array();
$this->m_aAdministrators = array();
$oAdministratorsFilter = new DBObjectSearch('User');
$oLnkFilter = new DBObjectSearch('URP_UserProfile');
$oExpression = new FieldExpression('profileid', 'URP_UserProfile');
@@ -718,10 +718,10 @@ class UserRightsProfile extends UserRightsAddOnAPI
$oSet->OptimizeColumnLoad(array('User' => array('login')));
while($oUser = $oSet->Fetch())
{
$aAdministrators[] = $oUser->GetKey();
$this->m_aAdministrators[] = $oUser->GetKey();
}
}
return $aAdministrators;
return $this->m_aAdministrators;
}
/**

View File

@@ -1,6 +1,6 @@
<?php
/**
* @deprecated will be removed in 3.1.0 - moved to sources/Application/WebPage/AjaxPage.php, now loadable using autoloader
* @deprecated 3.0.0 will be removed in 3.1.0 - moved to sources/Application/WebPage/AjaxPage.php, now loadable using autoloader
* @license http://opensource.org/licenses/AGPL-3.0
* @copyright Copyright (C) 2010-2023 Combodo SARL
*/

View File

@@ -1,6 +1,6 @@
<?php
/**
* @deprecated will be removed in 3.1.0 - moved to sources/Application/WebPage/CaptureWebPage.php, now loadable using autoloader
* @deprecated 3.0.0 will be removed in 3.1.0 - moved to sources/Application/WebPage/CaptureWebPage.php, now loadable using autoloader
* @license http://opensource.org/licenses/AGPL-3.0
* @copyright Copyright (C) 2010-2023 Combodo SARL
*/

View File

@@ -1,6 +1,6 @@
<?php
/**
* @deprecated will be removed in 3.1.0 - moved to sources/Application/WebPage/CLIPage.php, now loadable using autoloader
* @deprecated 3.0.0 will be removed in 3.1.0 - moved to sources/Application/WebPage/CLIPage.php, now loadable using autoloader
* @license http://opensource.org/licenses/AGPL-3.0
* @copyright Copyright (C) 2010-2023 Combodo SARL
*/

View File

@@ -744,7 +744,11 @@ HTML
$oPage->SetCurrentTab($sTabCode, $oAttDef->GetLabel().$sCount, $sTabDescription);
$aArgs = array('this' => $this);
$bReadOnly = ($iFlags & (OPT_ATT_READONLY | OPT_ATT_SLAVE));
$sEditWhen = $oAttDef->GetEditWhen();
$bIsEditableBasedOnEditWhen = ($sEditWhen === LINKSET_EDITWHEN_ALWAYS || $sEditWhen === LINKSET_EDITWHEN_ON_HOST_EDITION);
$bReadOnly = ($iFlags & (OPT_ATT_READONLY | OPT_ATT_SLAVE)) || !$bIsEditableBasedOnEditWhen;
if ($bEditMode && (!$bReadOnly)) {
$sInputId = $this->m_iFormId.'_'.$sAttCode;
$sDisplayValue = ''; // not used

View File

@@ -1,6 +1,6 @@
<?php
/**
* @deprecated will be removed in 3.1.0 - moved to sources/Application/WebPage/CSVPage.php, now loadable using autoloader
* @deprecated 3.0.0 will be removed in 3.1.0 - moved to sources/Application/WebPage/CSVPage.php, now loadable using autoloader
* @license http://opensource.org/licenses/AGPL-3.0
* @copyright Copyright (C) 2010-2023 Combodo SARL
*/

View File

@@ -918,7 +918,7 @@ class RuntimeDashboard extends Dashboard
{
$bCustomized = false;
$sDashboardFileSanitized = utils::RealPath($sDashboardFile, APPROOT);
$sDashboardFileSanitized = utils::RealPath(APPROOT.$sDashboardFile, APPROOT);
if (false === $sDashboardFileSanitized) {
throw new SecurityException('Invalid dashboard file !');
}
@@ -1141,7 +1141,7 @@ JS
$oToolbar->AddSubBlock($oActionButton);
$aActions = array();
$sFile = addslashes($this->sDefinitionFile);
$sFile = addslashes(utils::LocalPath($this->sDefinitionFile));
$sJSExtraParams = json_encode($aExtraParams);
if ($this->HasCustomDashboard()) {
$oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:EditCustom'), "return EditDashboard('{$this->sId}', '$sFile', $sJSExtraParams)");
@@ -1264,12 +1264,12 @@ EOF
$sOkButtonLabel = Dict::S('UI:Button:Save');
$sCancelButtonLabel = Dict::S('UI:Button:Cancel');
$sId = addslashes($this->sId);
$sLayoutClass = addslashes($this->sLayoutClass);
$sId = utils::HtmlEntities($this->sId);
$sLayoutClass = utils::HtmlEntities($this->sLayoutClass);
$sAutoReload = $this->bAutoReload ? 'true' : 'false';
$sAutoReloadSec = (string) $this->iAutoReloadSec;
$sTitle = addslashes($this->sTitle);
$sFile = addslashes($this->GetDefinitionFile());
$sTitle = utils::HtmlEntities($this->sTitle);
$sFile = utils::HtmlEntities($this->GetDefinitionFile());
$sUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
$sReloadURL = $this->GetReloadURL();

View File

@@ -2042,7 +2042,7 @@ class MenuBlock extends DisplayBlock
}
//----------------------------------------------------
// Any style but NOT "listInObject" (linksets) actions
// Any style but NOT \DisplayBlock::ENUM_STYLE_LIST_IN_OBJECT (linksets) actions
//----------------------------------------------------
if ($this->m_sStyle !== static::ENUM_STYLE_LIST_IN_OBJECT) {
switch ($iSetCount) {

View File

@@ -1,6 +1,6 @@
<?php
/**
* @deprecated will be removed in 3.1.0 - moved to sources/Application/WebPage/ErrorPage.php, now loadable using autoloader
* @deprecated 3.0.0 will be removed in 3.1.0 - moved to sources/Application/WebPage/ErrorPage.php, now loadable using autoloader
* @license http://opensource.org/licenses/AGPL-3.0
* @copyright Copyright (C) 2010-2023 Combodo SARL
*/

View File

@@ -1,6 +1,6 @@
<?php
/**
* @deprecated will be removed in 3.1.0 - moved to sources/Application/WebPage/iTopWebPage.php, now loadable using autoloader
* @deprecated 3.0.0 will be removed in 3.1.0 - moved to sources/Application/WebPage/iTopWebPage.php, now loadable using autoloader
* @license http://opensource.org/licenses/AGPL-3.0
* @copyright Copyright (C) 2010-2023 Combodo SARL
*/

View File

@@ -1,6 +1,6 @@
<?php
/**
* @deprecated will be removed in 3.1.0 - moved to sources/Application/WebPage/iTopWizardWebPage.php, now loadable using autoloader
* @deprecated 3.0.0 will be removed in 3.1.0 - moved to sources/Application/WebPage/iTopWizardWebPage.php, now loadable using autoloader
* @license http://opensource.org/licenses/AGPL-3.0
* @copyright Copyright (C) 2010-2023 Combodo SARL
*/

View File

@@ -3,7 +3,7 @@
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// 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.
@@ -38,7 +38,7 @@ class LoginWebPage extends NiceWebPage
const EXIT_PROMPT = 0;
const EXIT_HTTP_401 = 1;
const EXIT_RETURN = 2;
const EXIT_CODE_OK = 0;
const EXIT_CODE_MISSINGLOGIN = 1;
const EXIT_CODE_MISSINGPASSWORD = 2;
@@ -80,7 +80,7 @@ class LoginWebPage extends NiceWebPage
}
protected static $m_sLoginFailedMessage = '';
public function __construct($sTitle = null)
{
if ($sTitle === null) {
@@ -92,7 +92,7 @@ class LoginWebPage extends NiceWebPage
$this->no_cache();
$this->add_xframe_options();
}
public function SetStyleSheet()
{
$this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/login.css');
@@ -867,12 +867,12 @@ class LoginWebPage extends NiceWebPage
* @api
*
* @param string $sAuthUser
* @param Person $oPerson
* @param Person|null $oPerson
* @param array $aRequestedProfiles profiles to add to the new user
*
* @return \UserExternal|null
*/
public static function ProvisionUser($sAuthUser, $oPerson, $aRequestedProfiles)
public static function ProvisionUser(string $sAuthUser, ?Person $oPerson, array $aRequestedProfiles)
{
if (!MetaModel::IsValidClass('URP_Profiles'))
{
@@ -897,7 +897,9 @@ class LoginWebPage extends NiceWebPage
{
$oUser = MetaModel::NewObject('UserExternal');
$oUser->Set('login', $sAuthUser);
$oUser->Set('contactid', $oPerson->GetKey());
if (! is_null($oPerson)){
$oUser->Set('contactid', $oPerson->GetKey());
}
$oUser->Set('language', MetaModel::GetConfig()->GetDefaultLanguage());
}
@@ -949,7 +951,7 @@ class LoginWebPage extends NiceWebPage
* Overridable: depending on the user, head toward a dedicated portal
* @param string|null $sRequestedPortalId
* @param int $iOnExit How to complete the call: redirect or return a code
*/
*/
protected static function ChangeLocation($sRequestedPortalId = null, $iOnExit = self::EXIT_PROMPT)
{
$ret = call_user_func(array(self::$sHandlerClass, 'Dispatch'), $sRequestedPortalId);
@@ -1010,9 +1012,9 @@ class LoginWebPage extends NiceWebPage
static function DoLoginEx($sRequestedPortalId = null, $bMustBeAdmin = false, $iOnExit = self::EXIT_PROMPT)
{
$operation = utils::ReadParam('loginop', '');
$sMessage = self::HandleOperations($operation); // May exit directly
$iRet = self::Login($iOnExit);
if ($iRet == self::EXIT_CODE_OK)
{
@@ -1042,7 +1044,7 @@ class LoginWebPage extends NiceWebPage
{
return $sMessage;
}
}
}
protected static function HandleOperations($operation)
{
$sMessage = ''; // most of the operations never return, but some can return a message to be displayed
@@ -1156,11 +1158,11 @@ class LoginWebPage extends NiceWebPage
}
return $sMessage;
}
protected static function Dispatch($sRequestedPortalId)
{
if ($sRequestedPortalId === null) return true; // allowed to any portal => return true
$aPortalsConf = PortalDispatcherData::GetData();
$aDispatchers = array();
foreach($aPortalsConf as $sPortalId => $aConf)
@@ -1168,7 +1170,7 @@ class LoginWebPage extends NiceWebPage
$sHandlerClass = $aConf['handler'];
$aDispatchers[$sPortalId] = new $sHandlerClass($sPortalId);
}
if (array_key_exists($sRequestedPortalId, $aDispatchers) && $aDispatchers[$sRequestedPortalId]->IsUserAllowed())
{
return true;

View File

@@ -1,6 +1,6 @@
<?php
/**
* @deprecated will be removed in 3.1.0 - moved to sources/Application/WebPage/NiceWebPage.php, now loadable using autoloader
* @deprecated 3.0.0 will be removed in 3.1.0 - moved to sources/Application/WebPage/NiceWebPage.php, now loadable using autoloader
* @license http://opensource.org/licenses/AGPL-3.0
* @copyright Copyright (C) 2010-2023 Combodo SARL
*/

View File

@@ -1,6 +1,6 @@
<?php
/**
* @deprecated will be removed in 3.1.0 - moved to sources/Application/WebPage/PDFPage.php, now loadable using autoloader
* @deprecated 3.0.0 will be removed in 3.1.0 - moved to sources/Application/WebPage/PDFPage.php, now loadable using autoloader
* @license http://opensource.org/licenses/AGPL-3.0
* @copyright Copyright (C) 2010-2023 Combodo SARL
*/

View File

@@ -163,7 +163,7 @@ class UIExtKeyWidget
$oPage->add_linked_script('../js/extkeywidget.js');
$oPage->add_linked_script('../js/forms-json-utils.js');
$bCreate = (!$this->bSearchMode) && (UserRights::IsActionAllowed($this->sTargetClass, UR_ACTION_BULK_MODIFY) && $bAllowTargetCreation);
$bCreate = (!$this->bSearchMode) && (UserRights::IsActionAllowed($this->sTargetClass, UR_ACTION_MODIFY) && $bAllowTargetCreation);
$bExtensions = true;
$sMessage = Dict::S('UI:Message:EmptyList:UseSearchForm');
$sAttrFieldPrefix = ($this->bSearchMode) ? '' : 'attr_';
@@ -975,6 +975,10 @@ HTML
// Remove blob edition from creation form @see N°5863 to allow blob edition in modal context
FormHelper::DisableAttributeBlobInputs($this->sTargetClass, $aFormExtraParams);
if(FormHelper::HasMandatoryAttributeBlobInputs($oNewObj)){
$oPage->AddUiBlock(FormHelper::GetAlertForMandatoryAttributeBlobInputsInModal());
}
cmdbAbstractObject::DisplayCreationForm($oPage, $this->sTargetClass, $oNewObj, array(), $aFormExtraParams);
$oPage->add(<<<HTML
</div>

View File

@@ -143,6 +143,10 @@ JS
// Remove blob edition from creation form @see N°5863 to allow blob edition in modal context
FormHelper::DisableAttributeBlobInputs($sRealClass, $aFormExtraParams);
if(FormHelper::HasMandatoryAttributeBlobInputs($oObj)){
$oPage->AddUiBlock(FormHelper::GetAlertForMandatoryAttributeBlobInputsInModal());
}
cmdbAbstractObject::DisplayCreationForm($oPage, $sRealClass, $oObj, array(), $aFormExtraParams);
}

View File

@@ -52,22 +52,31 @@ class utils
{
/**
* @var string
* @since 3.0.0
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_INTEGER = 'integer';
/**
* Datamodel class
* @var string
* @since 3.0.0
* @since 2.7.10 3.0.0
* @since 2.7.10 3.0.4 3.1.1 3.2.0 N°6606 update PHPDoc
* @uses MetaModel::IsValidClass()
*/
public const ENUM_SANITIZATION_FILTER_CLASS = 'class';
/**
* @var string
* @since 3.0.0
* @since 2.7.10 3.0.4 3.1.1 3.2.0 N°6606
* @uses class_exists()
*/
public const ENUM_SANITIZATION_FILTER_PHP_CLASS = 'php_class';
/**
* @var string
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_STRING = 'string';
/**
* @var string
* @since 3.0.0
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_CONTEXT_PARAM = 'context_param';
/**
@@ -82,22 +91,22 @@ class utils
public const ENUM_SANITIZATION_FILTER_OPERATION = 'operation';
/**
* @var string
* @since 3.0.0
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_PARAMETER = 'parameter';
/**
* @var string
* @since 3.0.0
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_FIELD_NAME = 'field_name';
/**
* @var string
* @since 3.0.0
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_TRANSACTION_ID = 'transaction_id';
/**
* @var string For XML / HTML node identifiers
* @since 3.0.0
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER = 'element_identifier';
/**
@@ -107,12 +116,13 @@ class utils
public const ENUM_SANITIZATION_FILTER_VARIABLE_NAME = 'variable_name';
/**
* @var string
* @since 3.0.0
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_RAW_DATA = 'raw_data';
/**
* @var string
* @since 3.0.2, 3.1.0 N°4899
* @since 3.0.2 3.1.0 N°4899
* @since 2.7.10 N°6606
*/
public const ENUM_SANITIZATION_FILTER_URL = 'url';
@@ -155,6 +165,8 @@ class utils
private static $iNextId = 0;
private static $m_sAppRootUrl = null;
protected static function LoadParamFile($sParamFile)
{
if (!file_exists($sParamFile)) {
@@ -396,6 +408,10 @@ class utils
* @since 2.7.0 new 'element_identifier' filter
* @since 3.0.0 new utils::ENUM_SANITIZATION_* const
* @since 2.7.7, 3.0.2, 3.1.0 N°4899 - new 'url' filter
* @since 2.7.10 N°6606 use the utils::ENUM_SANITIZATION_* const
* @since 2.7.10 N°6606 new case for ENUM_SANITIZATION_FILTER_PHP_CLASS
*
* @link https://www.php.net/manual/en/filter.filters.sanitize.php PHP sanitization filters
*/
protected static function Sanitize_Internal($value, $sSanitizationFilter)
{
@@ -416,6 +432,13 @@ class utils
$retValue = filter_var($value, FILTER_SANITIZE_SPECIAL_CHARS);
break;
case static::ENUM_SANITIZATION_FILTER_PHP_CLASS:
$retValue = $value;
if (!class_exists($value)) {
$retValue = false;
}
break;
case static::ENUM_SANITIZATION_FILTER_CONTEXT_PARAM:
case static::ENUM_SANITIZATION_FILTER_ROUTE:
case static::ENUM_SANITIZATION_FILTER_OPERATION:
@@ -481,6 +504,7 @@ class utils
// For URL
case static::ENUM_SANITIZATION_FILTER_URL:
// N°6350 - returns only valid URLs
$retValue = filter_var($value, FILTER_VALIDATE_URL);
break;
@@ -1024,7 +1048,7 @@ class utils
*/
public static function GetAbsoluteUrlAppRoot($bForceTrustProxy = false)
{
static $sUrl = null;
$sUrl = static::$m_sAppRootUrl;
if ($sUrl === null || $bForceTrustProxy)
{
$sUrl = self::GetConfig()->Get('app_root_url');
@@ -1045,8 +1069,9 @@ class utils
}
$sUrl = str_replace(SERVER_NAME_PLACEHOLDER, $sServerName, $sUrl);
}
static::$m_sAppRootUrl = $sUrl;
}
return $sUrl;
return static::$m_sAppRootUrl;
}
/**

View File

@@ -1,6 +1,6 @@
<?php
/**
* @deprecated will be removed in 3.1.0 - moved to sources/Application/WebPage/WebPage.php, now loadable using autoloader
* @deprecated 3.0.0 will be removed in 3.1.0 - moved to sources/Application/WebPage/WebPage.php, now loadable using autoloader
* @license http://opensource.org/licenses/AGPL-3.0
* @copyright Copyright (C) 2010-2023 Combodo SARL
*/

View File

@@ -351,6 +351,7 @@ class WizardHelper
/**
* @return string JS code to be executed for fields update
* @since 3.0.0 N°3198
* @deprecated 3.0.3-2 3.0.4 3.1.1 3.2.0 Use {@see \WizardHelper::AddJsForUpdateFields()} instead
*/
public function GetJsForUpdateFields()
{
@@ -363,6 +364,32 @@ class WizardHelper
JS;
}
/**
* Add necessary JS snippets (to the page) to be executed for fields update
*
* @param \WebPage $oPage
* @return void
* @since 3.0.3-2 3.0.4 3.1.1 3.2.0 N°6766
*/
public function AddJsForUpdateFields(WebPage $oPage)
{
$sWizardHelperJsVar = (!is_null($this->m_aData['m_sWizHelperJsVarName'])) ? utils::Sanitize($this->m_aData['m_sWizHelperJsVarName'], '', utils::ENUM_SANITIZATION_FILTER_PARAMETER) : 'oWizardHelper'.$this->GetFormPrefix();
$sWizardHelperJson = $this->ToJSON();
$oPage->add_script(<<<JS
{$sWizardHelperJsVar}.m_oData = {$sWizardHelperJson};
{$sWizardHelperJsVar}.UpdateFields();
JS
);
$oPage->add_ready_script(<<<JS
if ({$sWizardHelperJsVar}.m_oDependenciesUpdatedPromiseResolve !== null){
{$sWizardHelperJsVar}.m_oDependenciesUpdatedPromiseResolve();
}
JS
);
}
/*
* Function with an old pattern of code
* @deprecated 3.1.0
@@ -371,11 +398,9 @@ JS;
{
$aSet = json_decode($sJsonSet, true); // true means hash array instead of object
$oSet = CMDBObjectSet::FromScratch($sLinkClass);
foreach ($aSet as $aLinkObj)
{
foreach ($aSet as $aLinkObj) {
$oLink = MetaModel::NewObject($sLinkClass);
foreach ($aLinkObj as $sAttCode => $value)
{
foreach ($aLinkObj as $sAttCode => $value) {
$oAttDef = MetaModel::GetAttributeDef($sLinkClass, $sAttCode);
if (($oAttDef->IsExternalKey()) && ($value != '') && ($value > 0))
{

View File

@@ -1,6 +1,6 @@
<?php
/**
* @deprecated will be removed in 3.1.0 - moved to sources/Application/WebPage/XMLPage.php, now loadable using autoloader
* @deprecated 3.0.0 will be removed in 3.1.0 - moved to sources/Application/WebPage/XMLPage.php, now loadable using autoloader
* @license http://opensource.org/licenses/AGPL-3.0
* @copyright Copyright (C) 2010-2023 Combodo SARL
*/

View File

@@ -60,6 +60,7 @@ class DbConnectionWrapper
*
* @param \mysqli|null $oMysqli
* @since 3.0.4 3.1.1 3.2.0 Param $oMysqli becomes nullable
* @since 3.1.0-4 N°6848 backport of restoring cnx on null parameter value
*/
public static function SetDbConnectionMockForQuery(?mysqli $oMysqli = null): void
{

View File

@@ -91,6 +91,12 @@ define('LINKSET_EDITMODE_ACTIONS', 2); // Show the usual 'Actions' popup menu
define('LINKSET_EDITMODE_INPLACE', 3); // The "linked" objects can be created/modified/deleted in place
define('LINKSET_EDITMODE_ADDREMOVE', 4); // The "linked" objects can be added/removed in place
define('LINKSET_EDITWHEN_NEVER', 0); // The linkset cannot be edited at all from inside this object
define('LINKSET_EDITWHEN_ON_HOST_EDITION', 1); // The only possible action is to open a new window to create a new object
define('LINKSET_EDITWHEN_ON_HOST_DISPLAY', 2); // Show the usual 'Actions' popup menu
define('LINKSET_EDITWHEN_ALWAYS', 3); // Show the usual 'Actions' popup menu
define('LINKSET_DISPLAY_STYLE_PROPERTY', 'property');
define('LINKSET_DISPLAY_STYLE_TAB', 'tab');
@@ -791,7 +797,7 @@ abstract class AttributeDefinition
public function HasAValue($proposedValue): bool
{
// Default implementation, we don't really know what type $proposedValue will be
return is_null($proposedValue);
return !(is_null($proposedValue));
}
/**
@@ -1703,6 +1709,15 @@ class AttributeLinkedSet extends AttributeDefinition
public function GetEditMode()
{
return $this->GetOptional('edit_mode', LINKSET_EDITMODE_ACTIONS);
}
/**
* @return int see LINKSET_EDITWHEN_* constants
* @since 3.1.1 3.2.0 N°6385
*/
public function GetEditWhen(): int
{
return $this->GetOptional('edit_when', LINKSET_EDITWHEN_ALWAYS);
}
/**

View File

@@ -149,7 +149,9 @@ abstract class BulkExport
$this->oSearch = null;
$this->iChunkSize = 0;
$this->sFormatCode = null;
$this->aStatusInfo = array();
$this->aStatusInfo = [
'show_obsolete_data' => utils::ShowObsoleteData(),
];
$this->oBulkExportResult = null;
$this->sTmpFile = '';
$this->bLocalizeOutput = false;
@@ -203,15 +205,17 @@ abstract class BulkExport
if ($oInfo && ($oInfo->Get('user_id') == UserRights::GetUserId()))
{
$sFormatCode = $oInfo->Get('format');
$oSearch = DBObjectSearch::unserialize($oInfo->Get('search'));
$aStatusInfo = json_decode($oInfo->Get('status_info'),true);
$oSearch = DBObjectSearch::unserialize($oInfo->Get('search'));
$oSearch->SetShowObsoleteData($aStatusInfo['show_obsolete_data']);
$oBulkExporter = self::FindExporter($sFormatCode, $oSearch);
if ($oBulkExporter)
{
$oBulkExporter->SetFormat($sFormatCode);
$oBulkExporter->SetObjectList($oSearch);
$oBulkExporter->SetChunkSize($oInfo->Get('chunk_size'));
$oBulkExporter->SetStatusInfo(json_decode($oInfo->Get('status_info'), true));
$oBulkExporter->SetStatusInfo($aStatusInfo);
$oBulkExporter->SetLocalizeOutput($oInfo->Get('localize_output'));
@@ -289,6 +293,7 @@ abstract class BulkExport
*/
public function SetObjectList(DBSearch $oSearch)
{
$oSearch->SetShowObsoleteData($this->aStatusInfo['show_obsolete_data']);
$this->oSearch = $oSearch;
}

View File

@@ -3582,7 +3582,8 @@ abstract class DBObject implements iDisplay
$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()) {
@@ -3596,6 +3597,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
@@ -4291,36 +4330,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

@@ -1671,6 +1671,8 @@ class ExceptionLog extends LogAPI
*/
private static function GetLastEventIssue()
{
return self::$oLastEventIssue;
$oRet = self::$oLastEventIssue;
self::$oLastEventIssue = null;
return $oRet;
}
}

View File

@@ -1241,7 +1241,7 @@ abstract class MetaModel
}
$sTable = self::DBGetTable($sClass);
// Could be completed later with all the classes that are using a given table
// Could be completed later with all the classes that are using a given table
if (!array_key_exists($sTable, $aTables)) {
$aTables[$sTable] = array();
}
@@ -3522,7 +3522,7 @@ abstract class MetaModel
}
// Set the "host class" as soon as possible, since HierarchicalKeys use it for their 'target class' as well
// and this needs to be know early (for Init_IsKnowClass 19 lines below)
// and this needs to be know early (for Init_IsKnowClass 19 lines below)
$oAtt->SetHostClass($sTargetClass);
// Some attributes could refer to a class
@@ -3564,7 +3564,7 @@ abstract class MetaModel
self::$m_aAttribDefs[$sTargetClass][$oAtt->GetCode()] = $oAtt;
self::$m_aAttribOrigins[$sTargetClass][$oAtt->GetCode()] = $sTargetClass;
// Note: it looks redundant to put targetclass there, but a mix occurs when inheritance is used
// Note: it looks redundant to put targetclass there, but a mix occurs when inheritance is used
}
/**
@@ -3764,7 +3764,7 @@ abstract class MetaModel
self::$m_aStimuli[$sTargetClass][$oStimulus->GetCode()] = $oStimulus;
// I wanted to simplify the syntax of the declaration of objects in the biz model
// Therefore, the reference to the host class is set there
// Therefore, the reference to the host class is set there
$oStimulus->SetHostClass($sTargetClass);
}
@@ -4219,40 +4219,77 @@ abstract class MetaModel
}
else
{
$aCurrentUser = array();
$aCurrentContact = array();
$aCurrentUser = [];
$aCurrentContact = [];
foreach ($aExpectedArgs as $expression)
{
$aName = explode('->', $expression->GetName());
if ($aName[0] == 'current_contact_id') {
$aPlaceholders['current_contact_id'] = UserRights::GetContactId();
}
if ($aName[0] == 'current_user') {
} else if ($aName[0] == 'current_user') {
array_push($aCurrentUser, $aName[1]);
}
if ($aName[0] == 'current_contact') {
} else if ($aName[0] == 'current_contact') {
array_push($aCurrentContact, $aName[1]);
}
}
if (count($aCurrentUser) > 0) {
$oUser = UserRights::GetUserObject();
$aPlaceholders['current_user->object()'] = $oUser;
foreach ($aCurrentUser as $sField) {
$aPlaceholders['current_user->'.$sField] = $oUser->Get($sField);
}
static::FillObjectPlaceholders($aPlaceholders, 'current_user', UserRights::GetUserObject(), $aCurrentUser);
}
if (count($aCurrentContact) > 0) {
$oPerson = UserRights::GetContactObject();
$aPlaceholders['current_contact->object()'] = $oPerson;
foreach ($aCurrentContact as $sField) {
$aPlaceholders['current_contact->'.$sField] = $oPerson->Get($sField);
}
static::FillObjectPlaceholders($aPlaceholders, 'current_contact', UserRights::GetContactObject(), $aCurrentContact);
}
}
return $aPlaceholders;
}
/**
* @since 3.1.1 N°6824
* @param array $aPlaceholders
* @param string $sPlaceHolderPrefix
* @param ?\DBObject $oObject
* @param array $aCurrentUser
*
* @return void
*
*/
private static function FillObjectPlaceholders(array &$aPlaceholders, string $sPlaceHolderPrefix, ?\DBObject $oObject, array $aCurrentUser) : void {
$sPlaceHolderKey = $sPlaceHolderPrefix."->object()";
if (is_null($oObject)){
$aContext = [
"current_user_id" => UserRights::GetUserId(),
"null object type" => $sPlaceHolderPrefix,
"fields" => $aCurrentUser,
];
IssueLog::Warning("Unresolved placeholders due to null object in current context", null,
$aContext);
$aPlaceholders[$sPlaceHolderKey] = \Dict::Format("Core:Placeholder:CannotBeResolved", $sPlaceHolderKey);
foreach ($aCurrentUser as $sField) {
$sPlaceHolderKey = $sPlaceHolderPrefix . "->$sField";
$aPlaceholders[$sPlaceHolderKey] = \Dict::Format("Core:Placeholder:CannotBeResolved", $sPlaceHolderKey);
}
} else {
$aPlaceholders[$sPlaceHolderKey] = $oObject;
foreach ($aCurrentUser as $sField) {
$sPlaceHolderKey = $sPlaceHolderPrefix . "->$sField";
if (false === MetaModel::IsValidAttCode(get_class($oObject), $sField)){
$aContext = [
"current_user_id" => UserRights::GetUserId(),
"obj_class" => get_class($oObject),
"placeholder" => $sPlaceHolderKey,
"invalid_field" => $sField,
];
IssueLog::Warning("Unresolved placeholder due to invalid attribute", null,
$aContext);
$aPlaceholders[$sPlaceHolderKey] = \Dict::Format("Core:Placeholder:CannotBeResolved", $sPlaceHolderKey);
continue;
}
$aPlaceholders[$sPlaceHolderKey] = $oObject->Get($sField);
}
}
}
/**
* @param \DBSearch $oFilter
*
@@ -6479,7 +6516,7 @@ abstract class MetaModel
$aCache['m_aExtensionClassNames'] = self::$m_aExtensionClassNames;
$aCache['m_Category2Class'] = self::$m_Category2Class;
$aCache['m_aRootClasses'] = self::$m_aRootClasses; // array of "classname" => "rootclass"
$aCache['m_aParentClasses'] = self::$m_aParentClasses; // array of ("classname" => array of "parentclass")
$aCache['m_aParentClasses'] = self::$m_aParentClasses; // array of ("classname" => array of "parentclass")
$aCache['m_aChildClasses'] = self::$m_aChildClasses; // array of ("classname" => array of "childclass")
$aCache['m_aClassParams'] = self::$m_aClassParams; // array of ("classname" => array of class information)
$aCache['m_aAttribDefs'] = self::$m_aAttribDefs; // array of ("classname" => array of attributes)
@@ -6737,7 +6774,13 @@ abstract class MetaModel
if ($bMustBeFound && empty($aRow))
{
throw new CoreException("No result for the single row query: '$sSQL'");
$sNotFoundErrorMessage = "No result for the single row query";
IssueLog::Info($sNotFoundErrorMessage, LogChannels::CMDB_SOURCE, [
'class' => $sClass,
'key' => $iKey,
'sql_query' => $sSQL,
]);
throw new CoreException($sNotFoundErrorMessage);
}
return $aRow;

View File

@@ -13,6 +13,9 @@ $body-overflow-x: hidden !default;
$body-overflow-y: auto !default;
/*N°5786 - Avoid strong text to always be grey (default Bulma color for this var.) so strong text can keep its color. This is mostly for text within the .ibo-is-html-content. */
$text-strong: inherit !default;
/**
* customize Bulma content variables
* See https://bulma.io/documentation/elements/content/

View File

@@ -355,4 +355,26 @@
</presentation>
</class>
</classes>
<meta>
<classes>
<class id="UserInternal" _delta="define_if_not_exists">
<fields>
<field id="contactid" xsi:type="AttributeExternalKey">
<target_class>Contact</target_class>
</field>
<field id="first_name" xsi:type="AttributeExternalField"/>
<field id="last_name" xsi:type="AttributeExternalField"/>
<field id="status" xsi:type="AttributeEnum"/>
<field id="org_id" xsi:type="AttributeExternalField"/>
<field id="email" xsi:type="AttributeExternalField"/>
<field id="login" xsi:type="AttributeString"/>
<field id="language" xsi:type="AttributeApplicationLanguage"/>
<field id="status" xsi:type="AttributeEnum"/>
<field id="allowed_org_list" xsi:type="AttributeLinkedSetIndirect"/>
<field id="profile_list" xsi:type="AttributeLinkedSetIndirect"/>
<field id="log" xsi:type="AttributeCaseLog"/>
</fields>
</class>
</classes>
</meta>
</itop_design>

View File

@@ -6354,17 +6354,7 @@
<attribute id="connectableci_id"/>
</attributes>
</reconciliation>
<uniqueness_rules>
<rule id="no_duplicate">
<attributes>
<attribute id="networkdevice_id"/>
<attribute id="connectableci_id"/>
</attributes>
<filter><![CDATA[]]></filter>
<disabled>false</disabled>
<is_blocking>true</is_blocking>
</rule>
</uniqueness_rules>
<uniqueness_rules/>
</properties>
<fields>
<field id="networkdevice_id" xsi:type="AttributeExternalKey">

View File

@@ -88,7 +88,7 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', array(
// Errors
'iTopUpdate:Error:MissingFunction' => 'Lehetetlen elindítani a frissítést, hiányzó funkció',
'iTopUpdate:Error:MissingFile' => 'Hiányzó fájl: %1$',
'iTopUpdate:Error:MissingFile' => 'Hiányzó fájl: %1$s',
'iTopUpdate:Error:CorruptedFile' => 'A %1$s fájl sérült',
'iTopUpdate:Error:BadFileFormat' => 'A frissítési fájl nem zip fájl',
'iTopUpdate:Error:BadFileContent' => 'A frissítési fájl nem alkalmazás archívum',

File diff suppressed because one or more lines are too long

View File

@@ -1803,6 +1803,11 @@ table .group-actions .item-action-wrapper .panel-body > p:last-child{
text-decoration: line-through;
}
/* Tippy: Handle multi-line content */
.tippy-content {
white-space: pre-line;
}
/**********************************************************/
/* Shameful area (things that should be refactored soon) */
/**********************************************************/

View File

@@ -1105,7 +1105,11 @@ class ObjectController extends BrickController
// When reaching to an Attachment, we have to check security on its host object instead of the Attachment itself
if ($sObjectClass === 'Attachment')
{
$oAttachment = MetaModel::GetObject($sObjectClass, $sObjectId, true, true);
$oAttachment = MetaModel::GetObject($sObjectClass, $sObjectId, false, true);
if ($oAttachment === null) {
throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
}
$sHostClass = $oAttachment->Get('item_class');
$sHostId = $oAttachment->Get('item_id');
@@ -1384,7 +1388,7 @@ class ObjectController extends BrickController
$aObjectIds = $oRequestManipulator->ReadParam('aObjectIds', array(), FILTER_UNSAFE_RAW);
$aObjectAttCodes = $oRequestManipulator->ReadParam('aObjectAttCodes', array(), FILTER_UNSAFE_RAW);
$aLinkAttCodes = $oRequestManipulator->ReadParam('aLinkAttCodes', array(), FILTER_UNSAFE_RAW);
$sDateTimePickerWidgetParent = $oRequestManipulator->ReadParam('sDateTimePickerWidgetParent', array(), FILTER_SANITIZE_STRING);
$sDateTimePickerWidgetParent = $oRequestManipulator->ReadParam('sDateTimePickerWidgetParent', array(), FILTER_UNSAFE_RAW);
if (empty($sObjectClass) || empty($aObjectIds) || empty($aObjectAttCodes)) {
IssueLog::Info(__METHOD__.' at line '.__LINE__.' : sObjectClass, aObjectIds and aObjectAttCodes expected, "'.$sObjectClass.'", "'.implode('/',

View File

@@ -1748,48 +1748,15 @@ public function PrefillSearchForm(&$aContextParam)
<ext_key_to_remote>slt_id</ext_key_to_remote>
<duplicates/>
</field>
<field id="customercontracts_list" xsi:type="AttributeLinkedSetIndirect">
<field id="customercontracts_list" xsi:type="AttributeLinkedSet">
<linked_class>lnkCustomerContractToService</linked_class>
<ext_key_to_me>sla_id</ext_key_to_me>
<count_min>0</count_min>
<count_max>0</count_max>
<ext_key_to_remote>customercontract_id</ext_key_to_remote>
<duplicates>true</duplicates>
<edit_mode>none</edit_mode>
</field>
</fields>
<methods>
<method id="DoCheckToWrite">
<static>false</static>
<access>public</access>
<code><![CDATA[
public function DoCheckToWrite()
{
parent::DoCheckToWrite();
$aCustomerContracts = $this->Get("customercontracts_list");
foreach ($aCustomerContracts as $sAttCode => $oCustomerContracts)
{
// Recurse inside the subdirectories
$sOql = "SELECT lnkCustomerContractToService AS ccs WHERE ccs.customercontract_id=:customercontract_id AND ccs.service_id=:service_id";
$aQueryParams['customercontract_id'] = $oCustomerContracts->Get("customercontract_id");
$aQueryParams['service_id'] = $oCustomerContracts->Get("service_id");
if ($this->Get("id") != null)
{
$sOql = $sOql." AND ccs.sla_id!=:sla_id";
$aQueryParams['sla_id'] = $this->Get("id");
}
$oQuery = DBSearch::FromOQL($sOql, $aQueryParams);
$oResultSql = new DBObjectSet($oQuery);
$oResultSql->OptimizeColumnLoad(['ccs' => ['customercontract_name','service_name']]);
if ($aCurrentRow = $oResultSql->Fetch())
{
$this->m_aCheckIssues[] = Dict::Format('Class:SLA/Error:UniqueLnkCustomerContractToService',$aCurrentRow->Get('customercontract_name'),$aCurrentRow->Get('service_name'));
}
}
}
]]></code>
</method>
</methods>
<methods/>
<presentation>
<details>
<items>

View File

@@ -713,6 +713,7 @@
<linked_class>User</linked_class>
<ext_key_to_me>contactid</ext_key_to_me>
<edit_mode>add_only</edit_mode>
<edit_when>on_host_display</edit_when>
<count_min>0</count_min>
<count_max>0</count_max>
</field>

View File

@@ -49,6 +49,7 @@ Dict::Add('EN US', 'English', 'English', array(
'Core:AttributeTagSet' => 'List of tags',
'Core:AttributeTagSet+' => '',
'Core:AttributeSet:placeholder' => 'click to add',
'Core:Placeholder:CannotBeResolved' => '(%1$s : cannot be resolved)',
'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)',

View File

@@ -39,6 +39,7 @@ Dict::Add('FR FR', 'French', 'Français', array(
'Core:AttributeTagSet' => 'Liste d\'étiquettes',
'Core:AttributeTagSet+' => '',
'Core:AttributeSet:placeholder' => 'cliquer pour ajouter',
'Core:Placeholder:CannotBeResolved' => '(%1$s : non remplacé)',
'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromClass' => '%1$s (%2$s)',
'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromOneChildClass' => '%1$s (%2$s de la classe %3$s)',
'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromSeveralChildClasses' => '%1$s (%2$s d\'une sous-classe)',

View File

@@ -18,4 +18,5 @@
*/
Dict::Add('CS CZ', 'Czech', 'Čeština', array(
'UI:Object:Modal:Title' => 'Create an object~~',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'This form contains a mandatory file attribute which is not supported in modal mode. The creation/modification of this object may be incomplete and may be completed in a full-page form.~~',
));

View File

@@ -18,4 +18,5 @@
*/
Dict::Add('DA DA', 'Danish', 'Dansk', array(
'UI:Object:Modal:Title' => 'Create an object~~',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'This form contains a mandatory file attribute which is not supported in modal mode. The creation/modification of this object may be incomplete and may be completed in a full-page form.~~',
));

View File

@@ -18,4 +18,5 @@
*/
Dict::Add('DE DE', 'German', 'Deutsch', array(
'UI:Object:Modal:Title' => 'Ein Objekt erstellen',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'This form contains a mandatory file attribute which is not supported in modal mode. The creation/modification of this object may be incomplete and may be completed in a full-page form.~~',
));

View File

@@ -19,4 +19,5 @@
Dict::Add('EN US', 'English', 'English', array(
'UI:Object:Modal:Title' => 'Create an object',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'This form contains a mandatory file attribute which is not supported in modal mode. The creation/modification of this object may be incomplete and may be completed in a full-page form.',
));

View File

@@ -18,4 +18,5 @@
*/
Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array(
'UI:Object:Modal:Title' => 'Create an object~~',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'This form contains a mandatory file attribute which is not supported in modal mode. The creation/modification of this object may be incomplete and may be completed in a full-page form.~~',
));

View File

@@ -18,4 +18,5 @@
*/
Dict::Add('FR FR', 'French', 'Français', array(
'UI:Object:Modal:Title' => 'Create an object~~',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'Ce formulaire contient un attribut fichier obligatoire qui n\'est pas supporté en mode pop-up. La création/modification de cet objet risque d\'être incomplète et pourra être complété dans un formulaire en pleine page.',
));

View File

@@ -18,4 +18,5 @@
*/
Dict::Add('HU HU', 'Hungarian', 'Magyar', array(
'UI:Object:Modal:Title' => 'Create an object~~',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'This form contains a mandatory file attribute which is not supported in modal mode. The creation/modification of this object may be incomplete and may be completed in a full-page form.~~',
));

View File

@@ -18,4 +18,5 @@
*/
Dict::Add('IT IT', 'Italian', 'Italiano', array(
'UI:Object:Modal:Title' => 'Create an object~~',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'This form contains a mandatory file attribute which is not supported in modal mode. The creation/modification of this object may be incomplete and may be completed in a full-page form.~~',
));

View File

@@ -18,4 +18,5 @@
*/
Dict::Add('JA JP', 'Japanese', '日本語', array(
'UI:Object:Modal:Title' => 'Create an object~~',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'This form contains a mandatory file attribute which is not supported in modal mode. The creation/modification of this object may be incomplete and may be completed in a full-page form.~~',
));

View File

@@ -18,4 +18,5 @@
*/
Dict::Add('NL NL', 'Dutch', 'Nederlands', array(
'UI:Object:Modal:Title' => 'Create an object~~',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'This form contains a mandatory file attribute which is not supported in modal mode. The creation/modification of this object may be incomplete and may be completed in a full-page form.~~',
));

View File

@@ -18,4 +18,5 @@
*/
Dict::Add('PL PL', 'Polish', 'Polski', array(
'UI:Object:Modal:Title' => 'Create an object~~',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'This form contains a mandatory file attribute which is not supported in modal mode. The creation/modification of this object may be incomplete and may be completed in a full-page form.~~',
));

View File

@@ -18,4 +18,5 @@
*/
Dict::Add('PT BR', 'Brazilian', 'Brazilian', array(
'UI:Object:Modal:Title' => 'Create an object~~',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'This form contains a mandatory file attribute which is not supported in modal mode. The creation/modification of this object may be incomplete and may be completed in a full-page form.~~',
));

View File

@@ -18,4 +18,5 @@
*/
Dict::Add('RU RU', 'Russian', 'Русский', array(
'UI:Object:Modal:Title' => 'Create an object~~',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'This form contains a mandatory file attribute which is not supported in modal mode. The creation/modification of this object may be incomplete and may be completed in a full-page form.~~',
));

View File

@@ -18,4 +18,5 @@
*/
Dict::Add('SK SK', 'Slovak', 'Slovenčina', array(
'UI:Object:Modal:Title' => 'Create an object~~',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'This form contains a mandatory file attribute which is not supported in modal mode. The creation/modification of this object may be incomplete and may be completed in a full-page form.~~',
));

View File

@@ -18,4 +18,5 @@
*/
Dict::Add('TR TR', 'Turkish', 'Türkçe', array(
'UI:Object:Modal:Title' => 'Create an object~~',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'This form contains a mandatory file attribute which is not supported in modal mode. The creation/modification of this object may be incomplete and may be completed in a full-page form.~~',
));

View File

@@ -18,4 +18,5 @@
*/
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'UI:Object:Modal:Title' => 'Create an object~~',
'UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text' => 'This form contains a mandatory file attribute which is not supported in modal mode. The creation/modification of this object may be incomplete and may be completed in a full-page form.~~',
));

View File

@@ -332,11 +332,14 @@ $(function()
oParams.dashlet_class = sDashletClass;
oParams.dashlet_id = sDashletId;
oParams.dashlet_type = options.dashlet_type;
oParams.ajax_promise_id = 'ajax_promise_' + sDashletId;
var me = this;
$.post(this.options.render_to, oParams, function (data) {
me.ajax_div.html(data);
me.add_dashlet_finalize(options, sDashletId, sDashletClass);
me.mark_as_modified();
window[oParams.ajax_promise_id].then(function(){
me.add_dashlet_finalize(options, sDashletId, sDashletClass);
me.mark_as_modified();
});
});
},
on_dashlet_moved: function (oDashlet, oReceiver, bRefresh) {

View File

@@ -152,7 +152,7 @@ $(function()
_initChooseDefaultOperator: function()
{
//if the class has an index, in order to maximize the performance, we force the default operator to "equal"
if (this.options.field.has_index && typeof this.options.available_operators['='] == 'object' && this.options.values.length == 0)
if (this.options.field.has_index && this.options.available_operators['='] != null && typeof this.options.available_operators['='] == 'object' && this.options.values.length == 0)
{
this.options.operator = '=';
this.options.available_operators['='].rank = -1;//we want it to be the first displayed

View File

@@ -154,6 +154,11 @@ Selectize.define("combodo_multi_values_synthesis", function (aOptions) {
// Listen item element click event
$item.on('click', function(){
// input disabled
if(oSelf.$input.is(':disabled')){
return;
}
// If element has operation
if(aOperations[sItem] === OPERATIONS.add || aOperations[sItem] === OPERATIONS.remove) {

View File

@@ -43,6 +43,24 @@ Selectize.define("combodo_update_operations", function (aOptions) {
};
})();
// Override enable function
oSelf.enable = (function () {
let oOriginal = oSelf.enable;
return function () {
oOriginal.apply(oSelf, arguments);
oSelf.$operationsInput.prop('disabled', false);
}
})();
// Override disable function
oSelf.disable = (function () {
let oOriginal = oSelf.disable;
return function () {
oOriginal.apply(oSelf, arguments);
oSelf.$operationsInput.prop('disabled', true);
}
})();
// Override addItem function
oSelf.addItem = (function () {
let oOriginal = oSelf.addItem;

View File

@@ -74,6 +74,11 @@ function WizardHelper(sClass, sFormPrefix, sState, sInitialState, sStimulus) {
'm_sWizHelperJsVarName': null // if set will use this name when server returns JS code in \WizardHelper::GetJsForUpdateFields
};
this.m_oData.m_sClass = sClass;
/**
* Promise resolve callback when dependencies have been updated
* @since 3.0.3-2 3.0.4 3.1.1 3.2.0 N°6766
* */
this.m_oDependenciesUpdatedPromiseResolve = null;
// Setting optional transition data
if (sInitialState !== undefined)
@@ -152,6 +157,7 @@ function WizardHelper(sClass, sFormPrefix, sState, sInitialState, sStimulus) {
};
this.UpdateFields = function () {
const me = this;
var aRefreshed = [];
//console.log('** UpdateFields **');
// Set the full HTML for the input field
@@ -185,10 +191,17 @@ function WizardHelper(sClass, sFormPrefix, sState, sInitialState, sStimulus) {
}
}
// For each "refreshed" field, asynchronously trigger a change in case there are dependent fields to update
for (i = 0; i < aRefreshed.length; i++)
{
for (i = 0; i < aRefreshed.length; i++) {
var sString = "$('#"+aRefreshed[i]+"').trigger('change').trigger('update');";
window.setTimeout(sString, 1); // Synchronous 'trigger' does nothing, call it asynchronously
const oPromise = new Promise(function (resolve) {
// Store the resolve callback so we can call it later from outside
me.m_oDependenciesUpdatedPromiseResolve = resolve;
});
oPromise.then(function () {
window.setTimeout(sString, 1); // Synchronous 'trigger' does nothing, call it asynchronously
// Resolve callback is reinitialized in case the redirection fails for any reason and we might need to retry
me.m_oDependenciesUpdatedPromiseResolve = null;
});
}
if($('[data-field-status="blocked"]').length === 0) {
$('.disabledDuringFieldLoading').prop("disabled", false).removeClass('disabledDuringFieldLoading');

View File

@@ -561,7 +561,7 @@ try
}
}
}
$oPage->add_script($oWizardHelper->GetJsForUpdateFields());
$oWizardHelper->AddJsForUpdateFields($oPage);
break;
case 'obj_creation_form':
@@ -1030,9 +1030,9 @@ EOF
case 'new_dashlet_id':
$sDashboardDivId = utils::ReadParam("dashboardid");
$bIsCustomized = true; // Only called at runtime when customizing a dashboard
$iRow = utils::ReadParam("iRow");
$iCol = utils::ReadParam("iCol");
$sDashletIdOrig = utils::ReadParam("dashletid");
$iRow = utils::ReadParam("iRow", 0, false, utils::ENUM_SANITIZATION_FILTER_INTEGER);
$iCol = utils::ReadParam("iCol", 0, false, utils::ENUM_SANITIZATION_FILTER_INTEGER);
$sDashletIdOrig = utils::ReadParam("dashletid", '', false, utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
$sFinalDashletId = Dashboard::GetDashletUniqueId($bIsCustomized, $sDashboardDivId, $iRow, $iCol, $sDashletIdOrig);
$oPage = new AjaxPage('');
$oPage->SetOutputDataOnly(true);
@@ -1042,8 +1042,8 @@ EOF
case 'new_dashlet':
require_once(APPROOT.'application/forms.class.inc.php');
require_once(APPROOT.'application/dashlet.class.inc.php');
$sDashletClass = utils::ReadParam('dashlet_class', '');
$sDashletId = utils::ReadParam('dashlet_id', '', false, 'raw_data');
$sDashletClass = utils::ReadParam('dashlet_class', '', false, utils::ENUM_SANITIZATION_FILTER_PHP_CLASS);
$sDashletId = utils::ReadParam('dashlet_id', '', false, utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
if (is_subclass_of($sDashletClass, 'Dashlet')) {
$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sDashletId);
$offset = $oPage->start_capture();
@@ -1065,13 +1065,14 @@ EOF
case 'update_dashlet_property':
require_once(APPROOT.'application/forms.class.inc.php');
require_once(APPROOT.'application/dashlet.class.inc.php');
$aExtraParams = utils::ReadParam('extra_params', array(), false, 'raw_data');
$aParams = utils::ReadParam('params', '', false, 'raw_data');
$sDashletClass = $aParams['attr_dashlet_class'];
$sDashletType = $aParams['attr_dashlet_type'];
$sDashletId = utils::HtmlEntities($aParams['attr_dashlet_id']);
$aUpdatedProperties = $aParams['updated']; // Code of the changed properties as an array: 'attr_xxx', 'attr_xxy', etc...
$aPreviousValues = $aParams['previous_values']; // hash array: 'attr_xxx' => 'old_value'
$aExtraParams = utils::ReadParam('extra_params', array(), false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
$aParams = utils::ReadParam('params', [], false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA); // raw_data because we need different filter depending on the options
$sDashletClass = utils::Sanitize($aParams['attr_dashlet_class'], DashletUnknown::class, utils::ENUM_SANITIZATION_FILTER_PHP_CLASS); // Dashlet PHP class or DashletUnknown if impl isn't present in the installed extensions
$sDashletType = utils::Sanitize($aParams['attr_dashlet_type'], '', utils::ENUM_SANITIZATION_FILTER_PHP_CLASS); // original Dashlet PHP class, could be non-existing on the iTop instance (XML definition loading)
$sDashletId = utils::Sanitize($aParams['attr_dashlet_id'], '', utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
$aUpdatedProperties = utils::Sanitize($aParams['updated'], [], utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER); // Code of the changed properties as an array: 'attr_xxx', 'attr_xxy' etc
$aPreviousValues = utils::Sanitize($aParams['previous_values'], [], utils::ENUM_SANITIZATION_FILTER_RAW_DATA); // hash array: 'attr_xxx' => 'old_value' - no sanitization as values will be handled in the Dashlet object
if (is_subclass_of($sDashletClass, 'Dashlet')) {
/** @var \Dashlet $oDashlet */
$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sDashletId);
@@ -1208,12 +1209,13 @@ EOF
'base/layouts/navigation-menu/menu-node'
);
// Important: Mind the back ticks to avoid line breaks to break the JS
$oPage->add_script(<<<JS
$MenuNameEscaped = utils::HtmlEntities($aValues['name']);
// Important: Mind the back ticks to avoid line breaks to break the JS
$oPage->add_script(<<<JS
$('body').trigger('add_shortcut_node.navigation_menu.itop', {
parent_menu_node_id: '{$sMenuGroupId}',
new_menu_node_html_rendering: `{$sHtml}`,
new_menu_name: `{$aValues['name']}`
new_menu_name: `{$MenuNameEscaped}`
});
JS
);

View File

@@ -938,6 +938,30 @@ EOF
return $aXmlToPHP[$sEditMode];
}
/**
* Helper to format the edit-when for direct linkset
*
* @param string $sEditWhen Value set from within the XML
* @return string PHP flag
*
* @throws \DOMFormatException
*/
protected function EditWhenToPHP($sEditWhen): string
{
static $aXmlToPHP = array(
'never' => 'LINKSET_EDITWHEN_NEVER',
'on_host_edition' => 'LINKSET_EDITWHEN_ON_HOST_EDITION',
'on_host_display' => 'LINKSET_EDITWHEN_ON_HOST_DISPLAY',
'always' => 'LINKSET_EDITWHEN_ALWAYS',
);
if (!array_key_exists($sEditWhen, $aXmlToPHP))
{
throw new DOMFormatException("Edit mode: unknown value '$sEditWhen'");
}
return $aXmlToPHP[$sEditWhen];
}
/**
* Format a path (file or url) as an absolute path or relative to the module or the app
@@ -2054,6 +2078,7 @@ EOF
$this->CompileCommonProperty('duplicates', $oField, $aParameters, $sModuleRelativeDir, false);
$this->CompileCommonProperty('display_style', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('edit_mode', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('edit_when', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('filter', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('with_php_constraint', $oField, $aParameters, $sModuleRelativeDir, false);
$aParameters['depends_on'] = $sDependencies;
@@ -2064,6 +2089,7 @@ EOF
$this->CompileCommonProperty('count_max', $oField, $aParameters, $sModuleRelativeDir, 0);
$this->CompileCommonProperty('display_style', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('edit_mode', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('edit_when', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('filter', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('with_php_constraint', $oField, $aParameters, $sModuleRelativeDir, false);
$aParameters['depends_on'] = $sDependencies;
@@ -2289,6 +2315,12 @@ EOF
$aParameters['edit_mode'] = $this->EditModeToPHP($sEditMode);
}
break;
case 'edit_when':
$sEditWhen = $oField->GetChildText('edit_when');
if(!is_null($sEditWhen)){
$aParameters['edit_when'] = $this->EditWhenToPHP($sEditWhen);
}
break;
case 'mappings':
$oMappings = $oField->GetUniqueElement('mappings');
$oMappingNodes = $oMappings->getElementsByTagName('mapping');

View File

@@ -1466,7 +1466,7 @@ EOF
switch ($sAlteration) {
case '':
if ($oNodeClone->hasAttribute('id')) {
$oNodeClone->setAttribute('_delta', 'must_exist');
//$oNodeClone->setAttribute('_delta', 'merge');
}
break;
case 'added':

View File

@@ -76,7 +76,7 @@ class ModuleDiscovery
'doc.manual_setup' => 'url',
'doc.more_information' => 'url',
);
// Cache the results and the source directories
protected static $m_aSearchDirs = null;
@@ -148,7 +148,7 @@ class ModuleDiscovery
self::$m_aModuleVersionByName[$sModuleName]['version'] = $sModuleVersion;
self::$m_aModuleVersionByName[$sModuleName]['id'] = $sId;
}
self::$m_aModules[$sId] = $aArgs;
// Now keep the relative paths, as provided
@@ -220,8 +220,8 @@ class ModuleDiscovery
public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
// Order the modules to take into account their inter-dependencies
$aDependencies = array();
$aSelectedModules = array();
$aDependencies = [];
$aSelectedModules = [];
foreach($aModules as $sId => $aModule)
{
list($sModuleName, ) = self::GetModuleName($sId);
@@ -232,7 +232,7 @@ class ModuleDiscovery
}
}
ksort($aDependencies);
$aOrderedModules = array();
$aOrderedModules = [];
$iLoopCount = 1;
while(($iLoopCount < count($aModules)) && (count($aDependencies) > 0) )
{
@@ -256,13 +256,24 @@ class ModuleDiscovery
}
if ($bAbortOnMissingDependency && count($aDependencies) > 0)
{
$aModulesInfo = array();
$aModuleDeps = array();
$aModulesInfo = [];
$aModuleDeps = [];
foreach($aDependencies as $sId => $aDeps)
{
$aModule = $aModules[$sId];
$aModuleDeps[] = "{$aModule['label']} (id: $sId) depends on: ".implode(' + ', $aDeps);
$aModulesInfo[$sId] = array('module' => $aModule, 'dependencies' => $aDeps);
$aDepsWithIcons = [];
foreach($aDeps as $sIndex => $sDepId)
{
if (self::DependencyIsResolved($sDepId, $aOrderedModules, $aSelectedModules))
{
$aDepsWithIcons[$sIndex] = '✅ ' . $sDepId;
} else
{
$aDepsWithIcons[$sIndex] = '❌ ' . $sDepId;
}
}
$aModuleDeps[] = "{$aModule['label']} (id: $sId) depends on: ".implode(' + ', $aDepsWithIcons);
$aModulesInfo[$sId] = array('module' => $aModule, 'dependencies' => $aDepsWithIcons);
}
$sMessage = "The following modules have unmet dependencies:\n".implode(",\n", $aModuleDeps);
$oException = new MissingDependencyException($sMessage);
@@ -289,7 +300,7 @@ class ModuleDiscovery
// The de-duplication is now done directly by the AddModule method
return $aModules;
}
protected static function DependencyIsResolved($sDepString, $aOrderedModules, $aSelectedModules)
{
$bResult = false;
@@ -336,12 +347,12 @@ class ModuleDiscovery
if (version_compare($sCurrentVersion, $sExpectedVersion, $sOperator))
{
$aReplacements[$sModuleId] = '(true)'; // Add parentheses to protect against invalid condition causing
// a function call that results in a runtime fatal error
// a function call that results in a runtime fatal error
}
else
{
$aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing
// a function call that results in a runtime fatal error
// a function call that results in a runtime fatal error
}
}
else
@@ -400,20 +411,20 @@ class ModuleDiscovery
{
self::ResetCache();
}
if (is_null(self::$m_aSearchDirs))
{
self::$m_aSearchDirs = $aSearchDirs;
// Not in cache, let's scan the disk
foreach($aSearchDirs as $sSearchDir)
{
$sLookupDir = realpath($sSearchDir);
$sLookupDir = realpath($sSearchDir);
if ($sLookupDir == '')
{
throw new Exception("Invalid directory '$sSearchDir'");
}
clearstatcache();
self::ListModuleFiles(basename($sSearchDir), dirname($sSearchDir));
}
@@ -425,7 +436,7 @@ class ModuleDiscovery
return self::GetModules($bAbortOnMissingDependency, $aModulesToLoad);
}
}
public static function ResetCache()
{
self::$m_aSearchDirs = null;
@@ -437,7 +448,7 @@ class ModuleDiscovery
* Helper function to interpret the name of a module
* @param $sModuleId string Identifier of the module, in the form 'name/version'
* @return array(name, version)
*/
*/
public static function GetModuleName($sModuleId)
{
$aMatches = array();
@@ -466,7 +477,7 @@ class ModuleDiscovery
{
static $iDummyClassIndex = 0;
$sDirectory = $sRootDir.'/'.$sRelDir;
if ($hDir = opendir($sDirectory))
{
// This is the correct way to loop over the directory. (according to the documentation)
@@ -502,12 +513,12 @@ class ModuleDiscovery
$idx++;
}
$bRet = eval($sModuleFileContents);
if ($bRet === false)
{
SetupLog::Warning("Eval of $sRelDir/$sFile returned false");
}
//echo "<p>Done.</p>\n";
}
catch(ParseError $e)
@@ -535,7 +546,7 @@ class ModuleDiscovery
/** Alias for backward compatibility with old module files in which
* the declaration of a module invokes SetupWebPage::AddModule()
* whereas the new form is ModuleDiscovery::AddModule()
*/
*/
class SetupWebPage extends ModuleDiscovery
{
// For backward compatibility with old modules...
@@ -562,9 +573,9 @@ class SetupWebPage extends ModuleDiscovery
public static function log($sText)
{
SetupLog::Ok($sText);
}
}
}
/** Ugly patch !!!
* In order to be able to analyse / load several times
* the same module file, we rename the class (to avoid duplicate class definitions)

View File

@@ -7,6 +7,8 @@
namespace Combodo\iTop\Application\Helper;
use AttributeBlob;
use Combodo\iTop\Application\UI\Base\Component\Alert\Alert;
use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory;
use DBObject;
use Dict;
use MetaModel;
@@ -56,6 +58,39 @@ class FormHelper
}
}
/**
* Returns true if the object has a mandatory attribute blob
*
* @see N°6861 - Display warning when creating/editing a mandatory blob in modal
*
* @param \DBObject $oObject
*
* @return bool
* @throws \CoreException
*/
public static function HasMandatoryAttributeBlobInputs(DBObject $oObject): bool
{
foreach (MetaModel::ListAttributeDefs(get_class($oObject)) as $sAttCode => $oAttDef) {
if ($oAttDef instanceof AttributeBlob && (!$oAttDef->IsNullAllowed() || ($oObject->GetFormAttributeFlags($sAttCode) & OPT_ATT_MANDATORY))) {
return true;
}
}
return false;
}
/**
* Returns an Alert explaining what will happen when a mandatory attribute blob is displayed in a form
*
* @see N°6861 - Display warning when creating/editing a mandatory blob in modal
*
* @return \Combodo\iTop\Application\UI\Base\Component\Alert\Alert
*/
public static function GetAlertForMandatoryAttributeBlobInputsInModal(): Alert
{
$oAlert = AlertUIBlockFactory::MakeForWarning('',Dict::S('UI:Object:Modal:MandatoryAttributeBlobInputs:Warning:Text'));
return $oAlert;
}
/**
* Update flags to be sent to form with url parameters
* For now only supports "readonly" param

View File

@@ -35,6 +35,7 @@ use ReflectionClass;
use SetupPage;
use SetupUtils;
use Twig\Error\Error;
use Twig\Error\SyntaxError;
use utils;
use WebPage;
use ZipArchive;
@@ -662,6 +663,9 @@ abstract class Controller extends AbstractController
{
return $this->m_oTwig->render($sName.'.'.$sTemplateFileExtension.'.twig', $aParams);
}
catch (SyntaxError $e) {
IssueLog::Error($e->getMessage().' - file: '.$e->getFile().'('.$e->getLine().')');
}
catch (Error $e) {
if (strpos($e->getMessage(), 'Unable to find template') === false)
{

View File

@@ -200,7 +200,7 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
$sSubTitle = Dict::Format('UI:Pagination:HeaderSelection', $sCountHtml, '<span class="ibo-datatable--selected-count">0</span>');
} else {
$sSubTitle = Dict::Format('UI:Pagination:HeaderNoSelection', $sCountHtml);
}
}
if (utils::IsNotNullOrEmptyString($sFilterListUrl)) {
$sSubTitle = '<a href="'.$sFilterListUrl.'" title="'.Dict::S('UI:Menu:FilterList').'">'.$sSubTitle.'</a>';
@@ -211,7 +211,6 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
if (isset($aExtraParams['panel_icon']) && strlen($aExtraParams['panel_icon']) > 0) {
$oContainer->SetIcon($aExtraParams['panel_icon']);
}
$oContainer->AddToolbarBlock($oBlockMenu);
$oContainer->AddMainBlock($oDataTable);
} else {
@@ -333,7 +332,7 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
} else {
$aExtraFields['*'][] = $sFieldName;
}
}
}
$aClassAliases = $oSet->GetFilter()->GetSelectedClasses();
$aAuthorizedClasses = array();
@@ -352,59 +351,59 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
// Regular use case, dispatch fields to their corresponding aliases
else if (array_key_exists($sAlias, $aExtraFields)) {
$aLists[$sAlias] = $aExtraFields[$sAlias];
}
}
// Finally, if unknown alias, ignore fields
else {
$aLists[$sAlias] = array();
}
// If zlist specified, merge its fields with the currently present
if ($sZListName !== false) {
if ($sZListName !== false) {
$aDefaultList = MetaModel::FlattenZList(MetaModel::GetZListItems($sClassName, $sZListName));
$aLists[$sAlias] = array_merge($aDefaultList, $aLists[$sAlias]);
}
}
// Filter the list to removed linked set since we are not able to display them here
// Filter the list to removed linked set since we are not able to display them here
foreach ($aLists[$sAlias] as $index => $sAttCode) {
$oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCode);
if ($oAttDef instanceof AttributeLinkedSet) {
// Removed from the display list
$oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCode);
if ($oAttDef instanceof AttributeLinkedSet) {
// Removed from the display list
unset($aLists[$sAlias][$index]);
}
}
if (empty($aLists[$sAlias])) {
unset($aLists[$sAlias], $aAuthorizedClasses[$sAlias]);
}
}
// Only for main class
if (!empty($sLinkageAttribute) && $sClassName === $oSet->GetFilter()->GetClass()) {
// The set to display is in fact a set of links between the object specified in the $sLinkageAttribute
// and other objects...
// The display will then group all the attributes related to the link itself:
// | Link_attr1 | link_attr2 | ... || Object_attr1 | Object_attr2 | Object_attr3 | .. | Object_attr_n |
$aDisplayList = array();
$aAttDefs = MetaModel::ListAttributeDefs($sClassName);
assert(isset($aAttDefs[$sLinkageAttribute]));
$oAttDef = $aAttDefs[$sLinkageAttribute];
assert($oAttDef->IsExternalKey());
// First display all the attributes specific to the link record
// The set to display is in fact a set of links between the object specified in the $sLinkageAttribute
// and other objects...
// The display will then group all the attributes related to the link itself:
// | Link_attr1 | link_attr2 | ... || Object_attr1 | Object_attr2 | Object_attr3 | .. | Object_attr_n |
$aDisplayList = array();
$aAttDefs = MetaModel::ListAttributeDefs($sClassName);
assert(isset($aAttDefs[$sLinkageAttribute]));
$oAttDef = $aAttDefs[$sLinkageAttribute];
assert($oAttDef->IsExternalKey());
// First display all the attributes specific to the link record
foreach ($aLists[$sAlias] as $sLinkAttCode) {
$oLinkAttDef = $aAttDefs[$sLinkAttCode];
if ((!$oLinkAttDef->IsExternalKey()) && (!$oLinkAttDef->IsExternalField())) {
$aDisplayList[] = $sLinkAttCode;
}
$oLinkAttDef = $aAttDefs[$sLinkAttCode];
if ((!$oLinkAttDef->IsExternalKey()) && (!$oLinkAttDef->IsExternalField())) {
$aDisplayList[] = $sLinkAttCode;
}
// Then display all the attributes neither specific to the link record nor to the 'linkage' object (because the latter are constant)
}
// Then display all the attributes neither specific to the link record nor to the 'linkage' object (because the latter are constant)
foreach ($aLists[$sAlias] as $sLinkAttCode) {
$oLinkAttDef = $aAttDefs[$sLinkAttCode];
if (($oLinkAttDef->IsExternalKey() && ($sLinkAttCode != $sLinkageAttribute))
|| ($oLinkAttDef->IsExternalField() && ($oLinkAttDef->GetKeyAttCode() != $sLinkageAttribute))) {
$aDisplayList[] = $sLinkAttCode;
}
$oLinkAttDef = $aAttDefs[$sLinkAttCode];
if (($oLinkAttDef->IsExternalKey() && ($sLinkAttCode != $sLinkageAttribute))
|| ($oLinkAttDef->IsExternalField() && ($oLinkAttDef->GetKeyAttCode() != $sLinkageAttribute))) {
$aDisplayList[] = $sLinkAttCode;
}
// First display all the attributes specific to the link
// Then display all the attributes linked to the other end of the relationship
}
// First display all the attributes specific to the link
// Then display all the attributes linked to the other end of the relationship
$aLists[$sAlias] = $aDisplayList;
}
}
@@ -489,7 +488,7 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
$oSet->OptimizeColumnLoad($aColumnsToLoad);
$aColumnDefinition = [];
$iIndexColumn = 0;
$iIndexColumn=0;
$bSelectMode = isset($aExtraParams['selection_mode']) ? $aExtraParams['selection_mode'] == true : false;
$bSingleSelectMode = isset($aExtraParams['selection_type']) ? ($aExtraParams['selection_type'] == 'single') : false;
@@ -510,7 +509,7 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
$sCode = ($aData['code'] == '_key_') ? 'friendlyname' : $aData['code'];
if ($aData['sort'] != 'none') {
$aSortOrder[$sClassAlias.'.'.$sCode] = ($aData['sort'] == 'asc'); // true for ascending, false for descending
$aSortDatable = [$iIndexColumn, $aData['sort']];
$aSortDatable=[$iIndexColumn,$aData['sort']];
}
elseif (isset($oCustomSettings->aSortOrder[$sAttCode])){
$aSortOrder[$sClassAlias.'.'.$sCode] = $oCustomSettings->aSortOrder[$sAttCode]; // true for ascending, false for descending
@@ -596,15 +595,14 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
$aOptions['bUseCustomSettings'] = $bUseCustomSettings;
$aOptions['bViewLink'] = $bViewLink;
$aOptions['oClassAliases'] = json_encode($aClassAliases);
if (isset($aExtraParams['selected_rows']) && !empty($aExtraParams['selected_rows'])) {
$aOptions['sSelectedRows'] = json_encode($aExtraParams['selected_rows']);
} else {
$aOptions['sSelectedRows'] = '[]';
}
$aExtraParams['table_id']=$sTableId;
$aExtraParams['list_id']=$sListId;
$aExtraParams['table_id'] = $sTableId;
$aExtraParams['list_id'] = $sListId;
$oDataTable->SetOptions($aOptions);
$oDataTable->SetAjaxUrl(utils::GetAbsoluteUrlAppRoot()."pages/ajax.render.php");
@@ -628,7 +626,7 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
if (isset($aExtraParams['creation_in_modal_js_handler'])){
$oDataTable->SetModalCreationHandler($aExtraParams['creation_in_modal_js_handler']);
}
return $oDataTable;
}
@@ -714,14 +712,14 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
$aColumnDefinition["type"] = "html";
if ($sAttCode == '_key_') {
$sAttrLabel = $aData['alias'];
$sAttLabel = $aData['alias'];
$aColumnDefinition["title"] = $aData['alias'];
$aColumnDefinition['metadata'] = [
'object_class' => $sClassName,
'class_alias' => $sClassAlias,
'attribute_code' => $sAttCode,
'attribute_type' => '_key_',
'attribute_label' => $sAttrLabel,
'attribute_label' => $sAttLabel,
];
$aColumnDefinition["data"] = $sClassAlias."/".$sAttCode;
$aColumnDefinition["render"] = [
@@ -729,8 +727,8 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
"_" => $sClassAlias."/".$sAttCode,
];
$aColumnDefinition["createdCell"] = <<<JS
$(td).attr('data-object-class', '$sClassName');
$(td).attr('data-attribute-label', '$sAttrLabel');
$(td).attr('data-object-class', `$sClassName`);
$(td).attr('data-attribute-label', `$sAttLabel`);
if (rowData["$sClassAlias/$sAttCode/raw"]) {
$(td).attr('data-value-raw', rowData["$sClassAlias/$sAttCode/raw"]);
}
@@ -759,10 +757,10 @@ JS;
"_" => $sClassAlias."/".$sAttCode,
];
$aColumnDefinition["createdCell"] = <<<JS
$(td).attr('data-object-class', '$sClassName');
$(td).attr('data-attribute-label', '$sAttLabel');
$(td).attr('data-attribute-code', '$sAttCode');
$(td).attr('data-attribute-type', '$sAttDefClass');
$(td).attr('data-object-class', `$sClassName`);
$(td).attr('data-attribute-label', `$sAttLabel`);
$(td).attr('data-attribute-code', `$sAttCode`);
$(td).attr('data-attribute-type', `$sAttDefClass`);
if (rowData["$sClassAlias/$sAttCode/raw"]) {
$(td).attr('data-value-raw', rowData["$sClassAlias/$sAttCode/raw"]);
}

View File

@@ -196,7 +196,7 @@ abstract class AbstractBlockLinkSetViewTable extends UIContentBlock
$oLinkSet = $oOrmLinkSet->ToDBObjectSet(utils::ShowObsoleteData());
// add list block
$oBlock = new DisplayBlock($oLinkSet->GetFilter(), 'listInObject', false);
$oBlock = new DisplayBlock($oLinkSet->GetFilter(), DisplayBlock::ENUM_STYLE_LIST_IN_OBJECT, false);
$this->AddSubBlock($oBlock->GetRenderContent($oPage, $this->GetExtraParam(), $this->sTableId));
}
@@ -206,7 +206,7 @@ abstract class AbstractBlockLinkSetViewTable extends UIContentBlock
*/
private function InitIsAttEditable(): void
{
$iFlags = 0;
$iFlags = OPT_ATT_NORMAL;
if ($this->oDbObject->IsNew())
{
@@ -216,8 +216,20 @@ abstract class AbstractBlockLinkSetViewTable extends UIContentBlock
{
$iFlags = $this->oDbObject->GetAttributeFlags($this->sAttCode);
}
$bEditWhen = $this->IsEditableBasedOnEditWhen();
$this->bIsAttEditable = !($iFlags & (OPT_ATT_READONLY | OPT_ATT_SLAVE | OPT_ATT_HIDDEN));
$this->bIsAttEditable = !($iFlags & (OPT_ATT_READONLY | OPT_ATT_SLAVE | OPT_ATT_HIDDEN)) && $bEditWhen;
}
/**
* Compares Linkset attribute edit_when values with its usage requirements
*
* @return bool
* @since 3.1.1 3.2.0 N°6385
*/
protected function IsEditableBasedOnEditWhen(): bool{
return true;
}
/**

View File

@@ -121,10 +121,13 @@ class BlockDirectLinkSetEditTable extends UIContentBlock
{
$this->oAttributeLinkedSet = MetaModel::GetAttributeDef($this->oUILinksDirectWidget->GetClass(), $this->oUILinksDirectWidget->GetAttCode());
$sEditWhen = $this->oAttributeLinkedSet->GetEditWhen();
$bIsEditableBasedOnEditWhen = ($sEditWhen === LINKSET_EDITWHEN_ALWAYS || $sEditWhen === LINKSET_EDITWHEN_ON_HOST_EDITION);
// User rights
$this->bIsAllowCreate = UserRights::IsActionAllowed($this->oAttributeLinkedSet->GetLinkedClass(), UR_ACTION_CREATE) == UR_ALLOWED_YES;
$this->bIsAllowModify = UserRights::IsActionAllowed($this->oAttributeLinkedSet->GetLinkedClass(), UR_ACTION_MODIFY) == UR_ALLOWED_YES;
$this->bIsAllowDelete = UserRights::IsActionAllowed($this->oAttributeLinkedSet->GetLinkedClass(), UR_ACTION_DELETE) == UR_ALLOWED_YES;
$this->bIsAllowCreate = UserRights::IsActionAllowed($this->oAttributeLinkedSet->GetLinkedClass(), UR_ACTION_CREATE) == UR_ALLOWED_YES && $bIsEditableBasedOnEditWhen;
$this->bIsAllowModify = UserRights::IsActionAllowed($this->oAttributeLinkedSet->GetLinkedClass(), UR_ACTION_MODIFY) == UR_ALLOWED_YES && $bIsEditableBasedOnEditWhen;
$this->bIsAllowDelete = UserRights::IsActionAllowed($this->oAttributeLinkedSet->GetLinkedClass(), UR_ACTION_DELETE) == UR_ALLOWED_YES && $bIsEditableBasedOnEditWhen;
}
/**

View File

@@ -179,4 +179,13 @@ class BlockDirectLinkSetViewTable extends AbstractBlockLinkSetViewTable
return $aDefaults;
}
/**
* @inheritDoc
*/
protected function IsEditableBasedOnEditWhen(): bool
{
$sEditWhen = $this->oAttDef->GetEditWhen();
return $sEditWhen === LINKSET_EDITWHEN_ALWAYS || $sEditWhen === LINKSET_EDITWHEN_ON_HOST_DISPLAY;
}
}

View File

@@ -107,10 +107,13 @@ class BlockIndirectLinkSetEditTable extends UIContentBlock
{
$this->oAttributeLinkedSetIndirect = MetaModel::GetAttributeDef($this->oUILinksWidget->GetClass(), $this->oUILinksWidget->GetAttCode());
$sEditWhen = $this->oAttributeLinkedSetIndirect->GetEditWhen();
$bIsEditableBasedOnEditWhen = ($sEditWhen === LINKSET_EDITWHEN_ALWAYS || $sEditWhen === LINKSET_EDITWHEN_ON_HOST_EDITION);
// User rights
$this->bIsAllowCreate = UserRights::IsActionAllowed($this->oAttributeLinkedSetIndirect->GetLinkedClass(), UR_ACTION_CREATE) == UR_ALLOWED_YES;
$this->bIsAllowModify = UserRights::IsActionAllowed($this->oAttributeLinkedSetIndirect->GetLinkedClass(), UR_ACTION_MODIFY) == UR_ALLOWED_YES;
$this->bIsAllowDelete = UserRights::IsActionAllowed($this->oAttributeLinkedSetIndirect->GetLinkedClass(), UR_ACTION_DELETE) == UR_ALLOWED_YES;
$this->bIsAllowCreate = UserRights::IsActionAllowed($this->oAttributeLinkedSetIndirect->GetLinkedClass(), UR_ACTION_CREATE) == UR_ALLOWED_YES && $bIsEditableBasedOnEditWhen;
$this->bIsAllowModify = UserRights::IsActionAllowed($this->oAttributeLinkedSetIndirect->GetLinkedClass(), UR_ACTION_MODIFY) == UR_ALLOWED_YES && $bIsEditableBasedOnEditWhen;
$this->bIsAllowDelete = UserRights::IsActionAllowed($this->oAttributeLinkedSetIndirect->GetLinkedClass(), UR_ACTION_DELETE) == UR_ALLOWED_YES && $bIsEditableBasedOnEditWhen;
}
/**
@@ -278,7 +281,7 @@ EOF
if ($bReadOnly) {
$aRow['form::checkbox'] = "";
foreach ($this->m_aEditableFields as $sFieldCode) {
foreach ($this->oUILinksWidget->GetEditableFields() as $sFieldCode) {
$sDisplayValue = $linkObjOrId->GetEditValue($sFieldCode);
$aRow[$sFieldCode] = $sDisplayValue;
}
@@ -432,7 +435,7 @@ JS
$oAttDef = MetaModel::GetAttributeDef($this->oUILinksWidget->GetLinkedClass(), $sFieldCode);
if ($bReadOnlyField) {
$sFieldForHtml = $sDisplayValue;
$sFieldForHtml = $oAttDef->GetAsHTML($sValue);
} else {
$sFieldForHtml = cmdbAbstractObject::GetFormElementForField(
$oP,

View File

@@ -126,4 +126,13 @@ class BlockIndirectLinkSetViewTable extends AbstractBlockLinkSetViewTable
return $sAttCodesToDisplay;
}
/**
* @inheritDoc
*/
protected function IsEditableBasedOnEditWhen(): bool
{
$sEditWhen = $this->oAttDef->GetEditWhen();
return $sEditWhen === LINKSET_EDITWHEN_ALWAYS || $sEditWhen === LINKSET_EDITWHEN_ON_HOST_DISPLAY;
}
}

View File

@@ -81,7 +81,7 @@ class LinkSetUIBlockFactory extends SetUIBlockFactory
$oSetUIBlock->GetDataProvider()->SetOptions(array_values($aInitialOptions));
// Set value
$oSetUIBlock->SetValue(json_encode($aCurrentValues));
$oSetUIBlock->SetInitialValue(json_encode($aInitialValues));
$oSetUIBlock->SetInitialValue(json_encode(array_merge($aInitialValues, $aCurrentValues)));
} else {
$oSetUIBlock->SetHasError(true);
}

View File

@@ -140,6 +140,8 @@ class iTopComposer
$APPROOT_WITH_SLASHES.'lib/symfony/web-profiler-bundle/Tests',
$APPROOT_WITH_SLASHES.'lib/symfony/yaml/Tests',
$APPROOT_WITH_SLASHES.'lib/thenetworg/oauth2-azure/tests',
$APPROOT_WITH_SLASHES.'lib/twig/twig/src/Test',
$APPROOT_WITH_SLASHES.'lib/twig/twig/lib/Twig/Test',
$APPROOT_WITH_SLASHES.'lib/twig/twig/doc/tests',

View File

@@ -170,6 +170,10 @@ JS;
// Remove blob edition from creation form @see N°5863 to allow blob edition in modal context
FormHelper::DisableAttributeBlobInputs($sRealClass, $aFormExtraParams);
if(FormHelper::HasMandatoryAttributeBlobInputs($oObjToClone)){
$oPage->AddUiBlock(FormHelper::GetAlertForMandatoryAttributeBlobInputsInModal());
}
$aFormExtraParams['js_handlers']['cancel_button_on_click'] =
<<<JS
function() {
@@ -293,6 +297,9 @@ JS;
// Remove blob edition from creation form @see N°5863 to allow blob edition in modal context
FormHelper::DisableAttributeBlobInputs($sClass, $aFormExtraParams);
if(FormHelper::HasMandatoryAttributeBlobInputs($oObj)){
$oPage->AddUiBlock(FormHelper::GetAlertForMandatoryAttributeBlobInputsInModal());
}
} else {
$oPage = new iTopWebPage('', $bPrintable);
$oPage->DisableBreadCrumb();

View File

@@ -228,6 +228,10 @@ JS
// Remove blob edition from creation form @see N°5863 to allow blob edition in modal context
FormHelper::DisableAttributeBlobInputs($sRealClass, $aExtraParams);
if(FormHelper::HasMandatoryAttributeBlobInputs($oObj)){
$oPage->AddUiBlock(FormHelper::GetAlertForMandatoryAttributeBlobInputsInModal());
}
cmdbAbstractObject::DisplayCreationForm($oPage, $sRealClass, $oObj, array(), $aExtraParams);
}

View File

@@ -57,7 +57,13 @@ class BsLinkedSetFieldRenderer extends BsFieldRenderer
// Retrieve link and remote attributes
$aAttributesToDisplay = $this->oField->GetAttributesToDisplay();
$aLnkAttributesToDisplay = $this->oField->GetLnkAttributesToDisplay();
$iLinkAttributesToDisplayCount = count($this->oField->GetLnkAttributesToDisplay()) + 1;
// we sort the table on the first non link column
$iSortColumnIndex = count($this->oField->GetLnkAttributesToDisplay());
// if we are in edition mode, we skip the first column (selection checkbox column)
if(!$this->oField->GetReadOnly()){
$iSortColumnIndex++;
}
// Vars to build the table
$sAttributesToDisplayAsJson = json_encode($aAttributesToDisplay);
@@ -275,7 +281,7 @@ EOF
// We would just have to override / complete the necessary elements
var buildTable_{$this->oField->GetGlobalId()} = function()
{
var iDefaultOrderColumnIndex = {$iLinkAttributesToDisplayCount};
var iDefaultOrderColumnIndex = {$iSortColumnIndex};
// Instantiates datatables
oTable_{$this->oField->GetGlobalId()} = $('#{$sTableId}').DataTable({

View File

@@ -40,6 +40,11 @@ class Router
return static::$oSingleton;
}
/**
* @var bool $bUseCache
*/
protected $bUseCache = null;
/**********************/
/* Non-static methods */
/**********************/
@@ -54,6 +59,14 @@ class Router
// Don't do anything, we don't want to be initialized
}
/**
* @param bool|null $bUseCache Force cache usage for testing purposes, or leave it null for the default behavior
*/
public function SetUseCache(?bool $bUseCache): void
{
$this->bUseCache = $bUseCache;
}
/**
* Generate a complete URL for a specific route and optional parameters
*
@@ -137,7 +150,7 @@ class Router
public function GetRoutes(): array
{
$aRoutes = [];
$bUseCache = false === utils::IsDevelopmentEnvironment();
$bUseCache = is_null($this->bUseCache) ? (false === utils::IsDevelopmentEnvironment()) : $this->bUseCache;
$bMustWriteCache = false;
$sCacheFilePath = $this->GetCacheFileAbsPath();

View File

@@ -46,13 +46,20 @@
{% if aPage.aJsFiles is not empty %}
{% block iboPageJsFiles %}
<script type="text/javascript">
{% if bHasOnInitOrOnDomReadyScripts == false %}
// Define a dummy empty callback if there's no script to execute
let fOnJsFilesLoaded{{ sId }} = function (fResolve) {
fResolve();
}
{% endif %}
window['{{ sPromiseId }}'] = new Promise(function (fAllJsFilesResolve, fAllJsFilesReject) {
/**
* @type {Array} aJsFilesToLoad Files required by the current \AjaxPage
*
* For each file:
* - "id": Used as an identifier to check if file is already being handled
* - "url" is the URL that will be use for loading. It should include any relevant query args, including the cache buster
* - "url" is the URL that will be used for loading. It should include any relevant query args, including the cache buster
*
* ```
* [

View File

@@ -1,5 +1,6 @@
[infra]
php_version=7.4-apache
; STS version : testing greatest PHP version possible
php_version=8.2-apache
; N°6629 perf bug on some tests on mariadb for now, so specifying MySQL
db_version=5.7

View File

@@ -151,6 +151,12 @@ class DictionariesConsistencyAfterSetupTest extends ItopTestCase
$aMismatchedKeys = [];
foreach ($aKeyArgsCountMap[$sReferenceLangCode] as $sKey => $iExpectedNbOfArgs){
if (0 === $iExpectedNbOfArgs){
//no arg needed in EN.
//let s assume job has been done correctly in EN to simplify
continue;
}
if (in_array($sKey, self::$aLabelCodeNotToCheck)){
//false positive: do not test
continue;

View File

@@ -17,6 +17,18 @@ namespace Combodo\iTop\Test\UnitTest\Integration;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
/**
* Wrapper to load dictionnary files without altering the main dictionnary
* Eval will be called within the current namespace (this is done by adding a "namespace" statement)
*/
class Dict
{
public static function Add($sLanguageCode, $sEnglishLanguageDesc, $sLocalizedLanguageDesc, $aEntries)
{
}
}
/**
* For tests on compiled dict files, see {@see CompiledDictionariesConsistencyTest}
* @group beforeSetup
@@ -141,17 +153,40 @@ class DictionariesConsistencyTest extends ItopTestCase
/**
* @param string $sDictFile complete path for the file to check
* @param bool $bIsSyntaxValid expected assert value
*
* @uses `php -l`
* @uses \assertEquals()
*/
private function CheckDictionarySyntax(string $sDictFile, $bIsSyntaxValid = true): void
{
exec("php -l {$sDictFile}", $output, $return);
$bDictFileSyntaxOk = ($return === 0);
$sMessage = "File `{$sDictFile}` syntax didn't matched expectations\nparsing results=".var_export($output, true);
self::assertEquals($bIsSyntaxValid, $bDictFileSyntaxOk, $sMessage);
$sPHP = file_get_contents($sDictFile);
// Strip php tag to allow "eval"
$sPHP = substr(trim($sPHP), strlen('<?php'));
// Make sure the Dict class is the one declared in the current file
$sPHP = 'namespace '.__NAMESPACE__.";\n".$sPHP;
$iLineShift = 1; // Cope with the shift due to the namespace statement
$sPHP = str_replace(
['ITOP_APPLICATION_SHORT', 'ITOP_APPLICATION', 'ITOP_VERSION_NAME'],
['\'itop\'', '\'itop\'', '\'1.2.3\''],
$sPHP
);
try {
eval($sPHP);
// Reaching this point => No syntax error
if (!$bIsSyntaxValid) {
$this->fail("Failed to detect syntax error in dictionary `{$sDictFile}` (which is known as being INCORRECT)");
}
}
catch (\Error $e) {
if ($bIsSyntaxValid) {
$iLine = $e->getLine() - $iLineShift;
$this->fail("Invalid dictionary: {$e->getMessage()} in {$sDictFile}:{$iLine}");
}
}
catch (\Exception $e) {
if ($bIsSyntaxValid) {
$iLine = $e->getLine() - $iLineShift;
$sExceptionClass = get_class($e);
$this->fail("Exception thrown from dictionary: '$sExceptionClass: {$e->getMessage()}' in {$sDictFile}:{$iLine}");
}
}
$this->assertTrue(true);
}
}

View File

@@ -1,10 +1,11 @@
<?php
/**
* @used-by \Combodo\iTop\Test\UnitTest\Integration\DictionariesConsistencyTest::testPlaygroundDictionariesPhpSyntax
*/
Dict::Add('FR FR', 'French', 'Français', array(
'MyDictKey1' => 'l\'échappement : bon exemple 1',
'MyDictKey1' => 'l\'échappement : bon exemple 1'.ITOP_APPLICATION,
'MyDictKey2' => 'l\'échappement : bon exemple 2',
'MyDictKey3' => 'l\'échappement : bon exemple 3',
));

View File

@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
bootstrap="unittestautoload.php"
backupGlobals="true"
colors="true"
columns="120"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnRisky="false"
stopOnSkipped="false"
verbose="true"
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"/>
<ini name="html_errors" value="Off"/>
<env name="PHPUNIT_PRETTY_PRINT_PROGRESS" value="true"/>
</php>
<testsuites>
<!-- Unitary tests -->
<testsuite name="Perf">
<directory>perf-tests</directory>
</testsuite>
</testsuites>
<!-- Code coverage white list -->
<filter>
<whitelist>
<file>../../core/apc-emulation.php</file>
<file>../../core/ormlinkset.class.inc.php</file>
<file>../../datamodels/2.x/itop-tickets/main.itop-tickets.php</file>
</whitelist>
</filter>
</phpunit>

View File

@@ -1,93 +0,0 @@
<?php
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
/**
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
class BulkDBObjectTest extends ItopDataTestCase
{
const CREATE_TEST_ORG = true;
protected function setUp(): void
{
parent::setUp();
}
public function testInsertAndReadPersonObjects()
{
echo "Part 1: Insert Persons\n";
$sOrgId = $this->getTestOrgId();
$idx = 1;
$fStart = microtime(true);
$fMaxExecutionTimeAllowed = 40.0;
$iInitialPeak = 0;
$sInitialPeak = '';
$fStartLoop = $fStart;
for ($i = 0; $i < 3000; $i++) {
$oPerson = $this->CreateObject(Person::class, ['org_id' => $sOrgId, 'name' => "Person_$i", 'first_name' => 'John']);
if (0 == ($idx % 100)) {
$fDuration = microtime(true) - $fStartLoop;
$iMemoryPeakUsage = memory_get_peak_usage();
if ($iInitialPeak === 0) {
$iInitialPeak = $iMemoryPeakUsage;
$sInitialPeak = \utils::BytesToFriendlyFormat($iInitialPeak, 4);
}
$sCurrPeak = \utils::BytesToFriendlyFormat($iMemoryPeakUsage, 4);
echo "$idx ".sprintf('%.1f ms', $fDuration * 1000)." - Peak Memory Usage: $sCurrPeak\n";
$this->assertTrue($iMemoryPeakUsage === $iInitialPeak, "Peak memory changed from $sInitialPeak to $sCurrPeak after $idx insert loops"); $fStartLoop = microtime(true);
$fTotalDuration = microtime(true) - $fStart;
$sTotalDuration = sprintf('%.3f s', $fTotalDuration);
$this->assertTrue($fTotalDuration < $fMaxExecutionTimeAllowed, "execution time $sTotalDuration should be < $fMaxExecutionTimeAllowed ($idx insert loops)");
}
$idx++;
}
$fTotalDuration = microtime(true) - $fStart;
$sTotalDuration = sprintf('%.3f s', $fTotalDuration);
echo "Total duration: $sTotalDuration\n\n";
$this->assertTrue($fTotalDuration < $fMaxExecutionTimeAllowed, "Total execution time $sTotalDuration should be < $fMaxExecutionTimeAllowed");
//////////////////////
// Part 2 Fetch all the created persons
echo "Part 1: Fetch Persons\n";
$oSearch = DBSearch::FromOQL('SELECT Person WHERE org_id=:org_id');
$oSet = new DBObjectSet($oSearch, [], ['org_id' => $sOrgId]);
$idx = 1;
$iInitialPeak = 0;
$sInitialPeak = '';
$fMaxExecutionTimeAllowed = 0.5;
$fStart = microtime(true);
$fStartLoop = $fStart;
while ($oContact = $oSet->Fetch()) {
if (0 == ($idx % 100)) {
$fDuration = microtime(true) - $fStartLoop;
$iMemoryPeakUsage = memory_get_peak_usage();
if ($iInitialPeak === 0) {
$iInitialPeak = $iMemoryPeakUsage;
$sInitialPeak = \utils::BytesToFriendlyFormat($iInitialPeak, 4);
}
$sCurrPeak = \utils::BytesToFriendlyFormat($iMemoryPeakUsage, 4);
echo "$idx ".sprintf('%.1f ms', $fDuration * 1000)." - Peak Memory Usage: $sCurrPeak\n";
$this->assertTrue($iMemoryPeakUsage === $iInitialPeak, "Peak memory changed from $sInitialPeak to $sCurrPeak after $idx fetch loops");
$fStartLoop = microtime(true);
$fTotalDuration = microtime(true) - $fStart;
$sTotalDuration = sprintf('%.3f s', $fTotalDuration);
$this->assertTrue($fTotalDuration < $fMaxExecutionTimeAllowed, "Total execution time $sTotalDuration should be < $fMaxExecutionTimeAllowed ($idx fetch loops)");
}
$idx++;
}
$fTotalDuration = microtime(true) - $fStart;
$sTotalDuration = sprintf('%.3f s', $fTotalDuration);
echo "Total duration: $sTotalDuration\n\n";
$this->assertTrue($fTotalDuration < $fMaxExecutionTimeAllowed, "Total execution time $sTotalDuration should be < $fMaxExecutionTimeAllowed");
}
}

View File

@@ -2,7 +2,7 @@
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
bootstrap="unittestautoload.php"
bootstrap="vendor/autoload.php"
backupGlobals="true"
colors="true"
columns="120"
@@ -34,9 +34,6 @@
<testsuites>
<!-- Unitary tests -->
<testsuite name="Perf">
<directory>perf-tests</directory>
</testsuite>
<testsuite name="Application">
<directory>unitary-tests/application</directory>
</testsuite>

View File

@@ -2,7 +2,7 @@
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
bootstrap="unittestautoload.php"
bootstrap="vendor/autoload.php"
backupGlobals="true"
colors="true"
columns="120"

View File

@@ -87,7 +87,7 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
/**
* @return string Absolute path to the {@see \Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase::GetTestEnvironment()} folder
*/
final private function GetTestEnvironmentFolderAbsPath(): string
final protected function GetTestEnvironmentFolderAbsPath(): string
{
return APPROOT.'env-'.$this->GetTestEnvironment().'/';
}
@@ -97,7 +97,7 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
*
* @return void
*/
final private function MarkEnvironmentReady(): void
private function MarkEnvironmentReady(): void
{
if (false === $this->IsEnvironmentReady()) {
touch(static::GetTestEnvironmentFolderAbsPath());
@@ -109,7 +109,7 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
*
* @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
final protected 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

View File

@@ -146,6 +146,8 @@ abstract class ItopDataTestCase extends ItopTestCase
EventService::UnRegisterListener($sListenerId);
}
CMDBObject::SetCurrentChange(null);
parent::tearDown();
}
@@ -267,9 +269,7 @@ abstract class ItopDataTestCase extends ItopTestCase
$oMyObj->DBInsert();
$iKey = $oMyObj->GetKey();
$this->debug("Created $sClass::$iKey");
if (!static::USE_TRANSACTION) {
$this->aCreatedObjects[] = $oMyObj;
}
$this->aCreatedObjects[] = $oMyObj;
return $oMyObj;
}

View File

@@ -13,6 +13,8 @@ use SetupUtils;
use const DEBUG_BACKTRACE_IGNORE_ARGS;
/**
* Class ItopTestCase
*
* Helper class to extend for tests that DO NOT need to access the DataModel or the Database
*
* @since 3.0.4 3.1.1 3.2.0 N°6658 move some setUp/tearDown code to the corresponding methods *BeforeClass to speed up tests process time.
@@ -21,6 +23,7 @@ abstract class ItopTestCase extends TestCase
{
public const TEST_LOG_DIR = 'test';
public static $DEBUG_UNIT_TEST = false;
protected static $aBackupStaticProperties = [];
/**
* @link https://docs.phpunit.de/en/9.6/annotations.html#preserveglobalstate PHPUnit `preserveGlobalState` annotation documentation
@@ -302,6 +305,42 @@ abstract class ItopTestCase extends TestCase
return $oProperty->getValue($oObject);
}
/**
* Backup every static property of the class (even protected ones)
* @param string $sClass
*
* @return void
*
* @since 3.2.0
*/
public static function BackupStaticProperties($sClass)
{
$class = new \ReflectionClass($sClass);
foreach ($class->getProperties() as $property) {
if (!$property->isStatic()) continue;
$property->setAccessible(true);
static::$aBackupStaticProperties[$sClass][$property->getName()] = $property->getValue();
}
}
/**
* Restore every static property of the class (even protected ones)
* @param string $sClass
*
* @return void
*
* @since 3.2.0
*/
public static function RestoreStaticProperties($sClass)
{
$class = new \ReflectionClass($sClass);
foreach ($class->getProperties() as $property) {
if (!$property->isStatic()) continue;
$property->setAccessible(true);
$property->setValue(static::$aBackupStaticProperties[$sClass][$property->getName()]);
}
}
/**
* @since 2.7.10 3.1.0
*/
@@ -337,14 +376,14 @@ abstract class ItopTestCase extends TestCase
$oProperty->setValue($value);
}
public function RecurseRmdir($dir)
public static function RecurseRmdir($dir)
{
if (is_dir($dir)) {
$objects = scandir($dir);
foreach ($objects as $object) {
if ($object != "." && $object != "..") {
if (is_dir($dir.DIRECTORY_SEPARATOR.$object)) {
$this->RecurseRmdir($dir.DIRECTORY_SEPARATOR.$object);
static::RecurseRmdir($dir.DIRECTORY_SEPARATOR.$object);
} else {
unlink($dir.DIRECTORY_SEPARATOR.$object);
}
@@ -354,7 +393,7 @@ abstract class ItopTestCase extends TestCase
}
}
public function CreateTmpdir() {
public static function CreateTmpdir() {
$sTmpDir=tempnam(sys_get_temp_dir(),'');
if (file_exists($sTmpDir))
{
@@ -369,7 +408,7 @@ abstract class ItopTestCase extends TestCase
return sys_get_temp_dir();
}
public function RecurseMkdir($sDir){
public static function RecurseMkdir($sDir){
if (strpos($sDir, DIRECTORY_SEPARATOR) === 0){
$sPath = DIRECTORY_SEPARATOR;
} else {
@@ -394,13 +433,13 @@ abstract class ItopTestCase extends TestCase
}
public function RecurseCopy($src,$dst) {
public static function RecurseCopy($src,$dst) {
$dir = opendir($src);
@mkdir($dst);
while(false !== ( $file = readdir($dir)) ) {
if (( $file != '.' ) && ( $file != '..' )) {
if ( is_dir($src . '/' . $file) ) {
$this->RecurseCopy($src . DIRECTORY_SEPARATOR . $file,$dst . DIRECTORY_SEPARATOR . $file);
static::RecurseCopy($src . DIRECTORY_SEPARATOR . $file,$dst . DIRECTORY_SEPARATOR . $file);
}
else {
copy($src . DIRECTORY_SEPARATOR . $file,$dst . DIRECTORY_SEPARATOR . $file);

View File

@@ -4,9 +4,6 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
/**
* @runTestsInSeparateProcesses
*/
class ApplicationObjectExtensionTest extends \Combodo\iTop\Test\UnitTest\ItopDataTestCase
{
const CREATE_TEST_ORG = true;

View File

@@ -0,0 +1,42 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Application;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
/**
* @covers LoginWebPage
*/
class LoginWebPageTest extends ItopDataTestCase
{
public function testProvisionUserOnly(){
$iNum = uniqid();
$sEmail = "ProvisionUser".$iNum . "@gabuzomeu.shadok";
$oUser = \LoginWebPage::ProvisionUser($sEmail, null, [ 'Portal User']);
$this->assertNotNull($oUser);
$oUser = \MetaModel::GetObject(\UserExternal::class, $oUser->GetKey());
$this->assertNotNull($oUser);
$this->assertEquals($sEmail, $oUser->Get('login'));
$this->assertEquals(\MetaModel::GetConfig()->GetDefaultLanguage(), $oUser->Get('language'));
$this->assertEquals(0, $oUser->Get('contactid'));
}
public function testProvisionUserWithPerson(){
$iNum = uniqid();
$this->CreateTestOrganization();
$oPerson = $this->CreatePerson($iNum);
$sEmail = "ProvisionUser".$iNum . "@gabuzomeu.shadok";
$oUser = \LoginWebPage::ProvisionUser($sEmail, $oPerson, [ 'Portal User']);
$this->assertNotNull($oUser);
$oUser = \MetaModel::GetObject(\UserExternal::class, $oUser->GetKey());
$this->assertNotNull($oUser);
$this->assertEquals($sEmail, $oUser->Get('login'));
$this->assertEquals(\MetaModel::GetConfig()->GetDefaultLanguage(), $oUser->Get('language'));
$this->assertEquals($oPerson->GetKey(), $oUser->Get('contactid'));
}
}

View File

@@ -6,7 +6,7 @@ use Combodo\iTop\Application\Helper\Session;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
/**
* @runTestsInSeparateProcesses
* @runClassInSeparateProcess Required because PHPUnit outputs something earlier, thus causing the headers to be sent
*/
class SessionTest extends ItopTestCase
{
@@ -24,18 +24,9 @@ class SessionTest extends ItopTestCase
/**
* @covers \Combodo\iTop\Application\Helper\Session::Start
*/
public function testStart()
{
$this->assertNull(Session::$iSessionId);
Session::Start();
$this->assertNotNull(Session::$iSessionId);
}
/**
* @covers \Combodo\iTop\Application\Helper\Session::WriteClose
*/
public function testWriteClose()
public function testStartWriteClose()
{
$this->assertNull(Session::$iSessionId);
Session::Start();

View File

@@ -12,46 +12,65 @@ use ThemeHandler;
class ThemeHandlerTest extends ItopTestCase
{
const PATTERN = '|\\\/var[^"]+testimages|';
private $oCompileCSSServiceMock;
private $sCompiledThemesDirAbsPath;
private $sCssAbsPath;
private $sDmCssAbsPath;
private $sJsonThemeParamFile;
private $sTmpDir;
private $aDirsToCleanup= [];
static private $sTmpDir = null;
static private $aDirsToCleanup = [];
static private $sAbsoluteImagePath;
static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
static::$sTmpDir = static::CreateTmpdir().'/';
static::$aDirsToCleanup[] = static::$sTmpDir;
static::$sAbsoluteImagePath = APPROOT.'tests/php-unit-tests/unitary-tests/application/theme-handler/copied/testimages/';
static::RecurseMkdir(static::$sAbsoluteImagePath);
// Required by testCompileThemesxxx - copy images in test dir
static::RecurseCopy(APPROOT.'tests/php-unit-tests/unitary-tests/application/theme-handler/expected/testimages/', static::$sAbsoluteImagePath);
static::$aDirsToCleanup[] = dirname(static::$sAbsoluteImagePath);
}
static function tearDownAfterClass(): void
{
foreach (static::$aDirsToCleanup as $sDir)
{
static::RecurseRmdir($sDir);
}
parent::tearDownAfterClass();
}
protected static function InitCSSDirectory()
{
static::RecurseCopy(APPROOT."/tests/php-unit-tests/unitary-tests/application/theme-handler/expected/css", static::$sTmpDir."/branding/css");
}
public function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('application/themehandler.class.inc.php');
$this->RequireOnceItopFile('setup/modelfactory.class.inc.php');
$this->RequireOnceUnitTestFile('../setup/SubMFCompiler.php');
$this->oCompileCSSServiceMock = $this->createMock('CompileCSSService');
ThemeHandler::mockCompileCSSService($this->oCompileCSSServiceMock);
$this->sTmpDir = $this->CreateTmpdir().'/';
$this->aDirsToCleanup[] = $this->sTmpDir;
$this->sCompiledThemesDirAbsPath = $this->sTmpDir."branding/themes/";
$this->recurseMkdir($this->sCompiledThemesDirAbsPath."basque-red/");
$this->sCompiledThemesDirAbsPath = static::$sTmpDir."branding/themes/";
static::RecurseMkdir($this->sCompiledThemesDirAbsPath."basque-red/");
$this->sCssAbsPath = $this->sCompiledThemesDirAbsPath.'basque-red/main.css';
$this->sDmCssAbsPath = $this->sCompiledThemesDirAbsPath.'datamodel-compiled-scss-rules.scss';
$this->sJsonThemeParamFile = $this->sCompiledThemesDirAbsPath.'basque-red/theme-parameters.json';
$this->RecurseCopy(APPROOT."/tests/php-unit-tests/unitary-tests/application/theme-handler/expected/css", $this->sTmpDir."/branding/css");
}
public function tearDown(): void
{
parent::tearDown();
foreach ($this->aDirsToCleanup as $sDir)
{
echo $sDir;
$this->RecurseRmdir($sDir);
}
}
function KeepSignatureDiff($sSignature1, $sSignature2) : string {
@@ -83,14 +102,14 @@ class ThemeHandlerTest extends ItopTestCase
$aDiffOuput[$sKey] = $aCurrentDiffVal;
}
} else if ($oVal1 !== $aSignature2[$sKey]){
$aDiffOuput[$sKey] = "expected:$oVal1 | actual:$aSignature2[$sKey]";
$aDiffOuput[$sKey] = "expected:$oVal1 | actual:$aSignature2[$sKey]";
}
}
return json_encode($aDiffOuput, true);
}
function recurseMkdir($dir)
public static function RecurseMkdir($dir)
{
if (is_dir($dir))
{
@@ -98,7 +117,7 @@ class ThemeHandlerTest extends ItopTestCase
}
$sParentDir = dirname($dir);
if (!$this->recurseMkdir($sParentDir))
if (!static::RecurseMkdir($sParentDir))
{
return false;
}
@@ -141,6 +160,8 @@ JSON;
*/
public function testCompileThemeWithoutCssFile_FocusOnParamAttribute($readFromParamAttributeFromJson=false)
{
static::InitCSSDirectory();
$sExpectJsonFilePath = APPROOT.'tests/php-unit-tests/unitary-tests/application/theme-handler/expected/themes/basque-red/theme-parameters.json';
$sExpectedThemeParamJson = file_get_contents($sExpectJsonFilePath);
$aThemeParameters = json_decode($sExpectedThemeParamJson, true);
@@ -160,11 +181,11 @@ JSON;
if($readFromParamAttributeFromJson)
{
copy($sExpectJsonFilePath, $this->sJsonThemeParamFile);
$this->assertTrue(ThemeHandler::CompileTheme('basque-red', true, "COMPILATIONTIMESTAMP", null, [$this->sTmpDir.'/branding/themes/'], $this->sTmpDir));
$this->assertTrue(ThemeHandler::CompileTheme('basque-red', true, "COMPILATIONTIMESTAMP", null, [static::$sTmpDir.'/branding/themes/'], static::$sTmpDir));
}
else
{
$this->assertTrue(ThemeHandler::CompileTheme('basque-red', true, "COMPILATIONTIMESTAMP", $aThemeParameters, [$this->sTmpDir.'/branding/themes/'], $this->sTmpDir));
$this->assertTrue(ThemeHandler::CompileTheme('basque-red', true, "COMPILATIONTIMESTAMP", $aThemeParameters, [static::$sTmpDir.'/branding/themes/'], static::$sTmpDir));
}
$this->assertTrue(is_file($this->sCssAbsPath));
$this->assertEquals($sExpectedThemeParamJson, file_get_contents($this->sJsonThemeParamFile));
@@ -189,14 +210,14 @@ JSON;
*/
public function testCompileThemesEmptyArray($ThemeParametersJson, $CompileCount=0)
{
$sCssPath = $this->sTmpDir . '/branding/themes/basque-red/main.css';
$sCssPath = static::$sTmpDir . '/branding/themes/basque-red/main.css';
copy(APPROOT . 'tests/php-unit-tests/unitary-tests/application/theme-handler/expected/themes/basque-red/main.css', $sCssPath);
$this->oCompileCSSServiceMock->expects($this->exactly($CompileCount))
->method("CompileCSSFromSASS")
->willReturn("====CSSCOMPILEDCONTENT====");
$this->assertEquals($CompileCount!=0,ThemeHandler::CompileTheme('basque-red', true, "COMPILATIONTIMESTAMP", json_decode($ThemeParametersJson, true), [$this->sTmpDir.'/branding/themes/'], $this->sTmpDir));
$this->assertEquals($CompileCount!=0,ThemeHandler::CompileTheme('basque-red', true, "COMPILATIONTIMESTAMP", json_decode($ThemeParametersJson, true), [static::$sTmpDir.'/branding/themes/'], static::$sTmpDir));
}
public function CompileThemesProviderEmptyArray()
@@ -213,6 +234,14 @@ JSON;
/**
* @return array
* mixed $ThemeParametersJson
* int $iCompileCSSFromSASSCount
* boolean $bMissingFile
* boolean $bFilesTouchedRecently
* boolean $bFileMd5sumModified
* null $sFileToTest
* null $sExpectedMainCssPath
* bool $bSetup
*/
public function CompileThemesProvider()
{
@@ -260,26 +289,24 @@ JSON;
*/
public function testCompileThemes($ThemeParametersJson, $iCompileCSSFromSASSCount, $bMissingFile=false, $bFilesTouchedRecently=false, $bFileMd5sumModified=false, $sFileToTest=null, $sExpectedMainCssPath=null, $bSetup=true)
{
static::InitCSSDirectory();
$sAfterReplacementCssVariableMd5sum='';
if (is_file($this->sTmpDir.'/'.$sFileToTest))
if (is_file(static::$sTmpDir.'/'.$sFileToTest))
{
$sFileToTest = $this->sTmpDir.'/'.$sFileToTest;
$sFileToTest = static::$sTmpDir.'/'.$sFileToTest;
} else {
$sFileToTest = APPROOT.'/'.$sFileToTest;
}
//copy images in test dir
$sAbsoluteImagePath = APPROOT.'tests/php-unit-tests/unitary-tests/application/theme-handler/copied/testimages/';
$this->recurseMkdir($sAbsoluteImagePath);
$this->aDirsToCleanup[] = dirname($sAbsoluteImagePath);
$this->RecurseCopy(APPROOT.'tests/php-unit-tests/unitary-tests/application/theme-handler/expected/testimages/', $sAbsoluteImagePath);
// Backup the file to test
copy($sFileToTest, static::$sTmpDir.'/file-to-test-backup');
//change approot-relative in css-variable to use absolute path
$sCssVarPath = $this->sTmpDir."/branding/css/DO_NOT_CHANGE.css-variables.scss";
$sCssVarPath = static::$sTmpDir."/branding/css/DO_NOT_CHANGE.css-variables.scss";
$sBeforeReplacementCssVariableMd5sum = md5_file($sCssVarPath);
echo 'BEFORE :'.$sBeforeReplacementCssVariableMd5sum.' '.$sCssVarPath.' ';
$sCssVariableContent = file_get_contents($sCssVarPath);
$sLine = '$approot-relative: "'.$sAbsoluteImagePath.'" !default;';
$sLine = '$approot-relative: "'.static::$sAbsoluteImagePath.'" !default;';
$sCssVariableContent = preg_replace("/\\\$approot-relative: \"(.*)\"/", $sLine, $sCssVariableContent);
file_put_contents($sCssVarPath, $sCssVariableContent);
if ($bMissingFile)
@@ -296,30 +323,23 @@ JSON;
//change cssvar md5sum + image absolute paths
$sMainCssContent = file_get_contents(APPROOT."tests/php-unit-tests/unitary-tests/application/theme-handler/expected/themes/basque-red/main_testcompilethemes.css");
$sMainCssContent = preg_replace('/MD5SUM/', $sAfterReplacementCssVariableMd5sum, $sMainCssContent);
$sReplacement = rtrim($sAbsoluteImagePath, '/');
$sReplacement = rtrim(static::$sAbsoluteImagePath, '/');
$sReplacement=preg_replace('|\/|', '\/', $sReplacement);
$sMainCssContent = preg_replace(static::PATTERN, $sReplacement, $sMainCssContent);
$cssPath = $this->sTmpDir . '/branding/themes/basque-red/main.css';
echo 'PUT md5sum: '.$sAfterReplacementCssVariableMd5sum.' in '.$cssPath.' ';
$cssPath = static::$sTmpDir . '/branding/themes/basque-red/main.css';
file_put_contents($cssPath, $sMainCssContent);
//should be after main.css modification to make sure precompilation check will be performed
if ($bFilesTouchedRecently)
{
sleep(1);
touch($sFileToTest);
touch($sFileToTest, time() + 2, time() + 2);
}
//same: it should be after main.css modification
if ($bFileMd5sumModified)
{
$sMd5sum = md5_file($sFileToTest);
echo ' BEFORE touch: ' . $sMd5sum .' ' . $sFileToTest;
sleep(1);
file_put_contents($sFileToTest, "###\n".file_get_contents($sFileToTest));
$sMd5sum = md5_file($sFileToTest);
echo ' AFTER touch: ' . $sMd5sum .' ' . $sFileToTest;
touch($sFileToTest, time() + 2, time() + 2);
}
if (is_file($sCssVarPath))
@@ -332,7 +352,7 @@ JSON;
->willReturn("====CSSCOMPILEDCONTENT====");
$aThemeParameters = json_decode($ThemeParametersJson, true);
$this->assertEquals($iCompileCSSFromSASSCount!=0, ThemeHandler::CompileTheme('basque-red', $bSetup, "COMPILATIONTIMESTAMP", $aThemeParameters, [$this->sTmpDir.'/branding/themes/'], $this->sTmpDir));
$this->assertEquals($iCompileCSSFromSASSCount!=0, ThemeHandler::CompileTheme('basque-red', $bSetup, "COMPILATIONTIMESTAMP", $aThemeParameters, [static::$sTmpDir.'/branding/themes/'], static::$sTmpDir));
if ($iCompileCSSFromSASSCount==1)
{
@@ -347,9 +367,11 @@ JSON;
$aReplacements = [$sReplacement, $sAfterReplacementCssVariableMd5sum];
$aReplacements[] = md5(json_encode($aThemeParameters['variables']));
$aReplacements[] = $sAfterReplacementCssVariableMd5sum;
var_dump($aReplacements);
$this->DoInnerJsonValidation($sExpectedMainCssFile, $cssPath, $aPatterns, $aReplacements);
}
// Restore the file to test (possible improvement: do that in tearDown)
copy(static::$sTmpDir.'/file-to-test-backup', $sFileToTest);
}
public function DoInnerJsonValidation($sExpectedCssFile, $sActualCssFile, $aPatterns, $aReplacements)
@@ -396,8 +418,8 @@ JSON;
'\'abc/\'+ $approot-relative + "css/ui-lightness/images/toutou.png?v=" + $version',
"\$approot-relative + \"css/ui-lightness/images/toto.png?v=\" + \$version",
'$approot-relative + \'css/ui-lightness/images/titi.gif?v=\' + $version1',
'"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7?v=" + $version',
'$approot-relative + \'node_modules/raleway-webfont/fonts/Raleway-Thin.jpeg\'',
'"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7?v=" + $version',
'$approot-relative + \'node_modules/raleway-webfont/fonts/Raleway-Thin.jpeg\'',
];
$aIncludedUrls['aCompleteUrls'];
@@ -432,13 +454,13 @@ SCSS;
$aExpectedFoundVariables = [
'gabu' => 'zomeu',
'toto' => 'titi',
'approot-relative' => '../../../../../',
'approot-relative2' => '../../',
'gray-base' => '#000',
'a' => 'b',
'content-color' => '#eeeeee',
'default-font-family' => 'Trebuchet MS,Tahoma,Verdana,Arial,sans-serif',
'icons-filter' => 'hue-rotate(0deg)',
'approot-relative' => '../../../../../',
'approot-relative2' => '../../',
'gray-base' => '#000',
'a' => 'b',
'content-color' => '#eeeeee',
'default-font-family' => 'Trebuchet MS,Tahoma,Verdana,Arial,sans-serif',
'icons-filter' => 'hue-rotate(0deg)',
'toto' => 'titi',
];
$this->assertEquals($aExpectedFoundVariables, $aFoundVariables);
@@ -460,10 +482,10 @@ $icons-filter: hue-rotate(0deg) !default;
$toto : titi;
SCSS;
file_put_contents($this->sTmpDir . DIRECTORY_SEPARATOR . 'css-variable.scss', $sContent);
file_put_contents(static::$sTmpDir . DIRECTORY_SEPARATOR . 'css-variable.scss', $sContent);
$aVariables = ThemeHandler::GetVariablesFromFile(
[ 'css-variable.scss' ],
[ $this->sTmpDir ]
[ static::$sTmpDir ]
);
$aExpectedVariables = [
@@ -512,8 +534,10 @@ SCSS;
public function testGetIncludedImages()
{
$aStylesheetFile=glob($this->sTmpDir."/branding/css/*.scss");
$aStylesheetFile[]=$this->sTmpDir."/branding/css/ui-lightness/DO_NOT_CHANGE.jqueryui.scss";
static::InitCSSDirectory();
$aStylesheetFile=glob(static::$sTmpDir."/branding/css/*.scss");
$aStylesheetFile[]=static::$sTmpDir."/branding/css/ui-lightness/DO_NOT_CHANGE.jqueryui.scss";
$expectJsonFilePath = APPROOT.'tests/php-unit-tests/unitary-tests/application/theme-handler/expected/themes/basque-red/theme-parameters.json';
$expectedThemeParamJson = file_get_contents($expectJsonFilePath);
$aThemeParametersVariables = json_decode($expectedThemeParamJson, true);
@@ -538,7 +562,7 @@ SCSS;
* @throws \Exception
*/
public function testFindStylesheetFile(string $sFileToFind, array $aAllImports){
$sImportsPath = $this->sTmpDir.'branding/';
$sImportsPath = static::$sTmpDir.'branding/';
// Windows compat O:)
$sFileToFind = $this->UpdateDirSep($sFileToFind);

View File

@@ -36,20 +36,18 @@ class ApplicationExtensionTest extends ItopCustomDatamodelTestCase
* - 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)
public function testExtensionAPIRegisteredAndCalled()
{
if ($sCallMethod === static::ENUM_API_CALL_METHOD_ENUMPLUGINS) {
$iExtendingClassesCount = count(MetaModel::EnumPlugins($sAPIFQCN));
} else {
$iExtendingClassesCount = count(utils::GetClassesForInterface($sAPIFQCN, '', ['[\\\\/]lib[\\\\/]', '[\\\\/]node_modules[\\\\/]', '[\\\\/]test[\\\\/]', '[\\\\/]tests[\\\\/]']));
foreach ($this->ExtensionAPIRegisteredAndCalledProvider() as list($sAPIFQCN, $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");
}
$this->assertGreaterThan(0, $iExtendingClassesCount, "Found no class extending the $sAPIFQCN API");
}
public function ExtensionAPIRegisteredAndCalledProvider(): array

View File

@@ -77,9 +77,6 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
self::assertSame($aExpectedLinkStack, $aLinkModificationsStack);
}
/**
* @runInSeparateProcess
*/
public function testProcessClassIdDeferredUpdate()
{
// Create the team
@@ -157,7 +154,6 @@ 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)
*
@@ -201,8 +197,6 @@ 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
@@ -238,7 +232,6 @@ 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
@@ -281,7 +274,6 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
}
/**
* @runInSeparateProcess
* Check that when a link changes from an object to another, then both objects are notified
*
* @return void
@@ -325,7 +317,6 @@ 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

View File

@@ -25,7 +25,6 @@ use Combodo\iTop\Test\UnitTest\ItopTestCase;
use utils;
/**
* @runClassInSeparateProcess
* @covers utils
*/
class utilsTest extends ItopTestCase
@@ -245,7 +244,6 @@ class utilsTest extends ItopTestCase
}
/**
* @runInSeparateProcess
* @dataProvider GetAbsoluteUrlAppRootPersistency
*/
public function testGetAbsoluteUrlAppRootPersistency($bBehindReverseProxy,$bForceTrustProxy1 ,$sExpectedAppRootUrl1,$bForceTrustProxy2 , $sExpectedAppRootUrl2,$bForceTrustProxy3 , $sExpectedAppRootUrl3)
@@ -274,6 +272,9 @@ class utilsTest extends ItopTestCase
$this->assertEquals($sExpectedAppRootUrl2, utils::GetAbsoluteUrlAppRoot($bForceTrustProxy2));
$this->assertEquals($sExpectedAppRootUrl3, utils::GetAbsoluteUrlAppRoot($bForceTrustProxy3));
// Leave the place clean
static::SetNonPublicStaticProperty('utils', 'm_sAppRootUrl', null);
}

View File

@@ -2,8 +2,10 @@
namespace Combodo\iTop\Test\UnitTest\Core;
use Change;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use MetaModel;
use UserRequest;
class AttributeDefinitionTest extends ItopDataTestCase {
const CREATE_TEST_ORG = true;
@@ -55,6 +57,23 @@ class AttributeDefinitionTest extends ItopDataTestCase {
{
// Note: This is test is not great as we are datamodel dependent and don't have a class with all the attribute types
return [
'AttributeDateTime' => [
Change::class,
'start_date', // no default value on this field
<<<PHP
\$oObject->Set('start_date', '2023-09-06 12:26:00');
PHP
,
false,
true,
],
'AttributeFrienlyName' => [
UserRequest::class,
'friendlyname',
'',
true,
true,
],
'AttributeDashboard' => [
'Organization',
'overview',
@@ -158,9 +177,9 @@ PHP
'AttributeSubItem' => [
'UserRequest',
'tto_escalation_deadline',
'',
true,
true,
'', // read-only attribute
false,
false,
],
'AttributeOneWayPassword' => [
'UserLocal',

View File

@@ -6,10 +6,8 @@ use CMDBSource;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use MetaModel;
/**
* @runClassInSeparateProcess
*/
class BulkChangeTest extends ItopDataTestCase {
class BulkChangeTest extends ItopDataTestCase
{
const CREATE_TEST_ORG = true;
protected function setUp(): void
@@ -57,8 +55,7 @@ class BulkChangeTest extends ItopDataTestCase {
true // localize
);
$oChange = \CMDBObject::GetCurrentChange();
$aRes = $oBulk->Process($oChange);
$aRes = $oBulk->Process();
static::assertNotNull($aRes);
foreach ($aRes as $aRow) {
@@ -94,8 +91,7 @@ class BulkChangeTest extends ItopDataTestCase {
true // localize
);
$oChange = \CMDBObject::GetCurrentChange();
$aRes = $oBulk->Process($oChange);
$aRes = $oBulk->Process();
static::assertNotNull($aRes);
foreach ($aRes as $aRow) {
@@ -175,7 +171,6 @@ class BulkChangeTest extends ItopDataTestCase {
* @param $aReconcilKeys
*/
public function testCas1BulkChangeIssue($aInitData, $aCsvData, $aAttributes, $aExtKeys, $aReconcilKeys, $aResult) {
CMDBSource::Query('START TRANSACTION');
//change value during the test
$db_core_transactions_enabled=MetaModel::GetConfig()->Get('db_core_transactions_enabled');
MetaModel::GetConfig()->Set('db_core_transactions_enabled',false);
@@ -193,8 +188,6 @@ class BulkChangeTest extends ItopDataTestCase {
$aResult["id"]=$oServer->GetKey();
$this->debug("oServer->GetKey():".$oServer->GetKey());
}
$this->debug("aCsvData:".json_encode($aCsvData[0]));
$this->debug("aReconcilKeys:".$aReconcilKeys[0]);
$oBulk = new \BulkChange(
"Server",
$aCsvData,
@@ -206,13 +199,8 @@ class BulkChangeTest extends ItopDataTestCase {
"Y-m-d H:i:s", // date format
true // localize
);
$this->debug("BulkChange:");
$oChange = \CMDBObject::GetCurrentChange();
$this->debug("GetCurrentChange:");
$aRes = $oBulk->Process($oChange);
$this->debug("Process:");
$aRes = $oBulk->Process();
static::assertNotNull($aRes);
$this->debug("assertNotNull:");
foreach ($aRes as $aRow) {
if (array_key_exists('__STATUS__', $aRow)) {
$sStatus = $aRow['__STATUS__'];
@@ -232,7 +220,6 @@ class BulkChangeTest extends ItopDataTestCase {
$this->assertEquals( $aResult[0], $aRow[0]->GetDisplayableValue());
}
}
CMDBSource::Query('ROLLBACK');
MetaModel::GetConfig()->Set('db_core_transactions_enabled',$db_core_transactions_enabled);
}
@@ -378,7 +365,6 @@ class BulkChangeTest extends ItopDataTestCase {
* @param $aReconcilKeys
*/
public function testCas2BulkChangeIssue($aInitData, $aCsvData, $aAttributes, $aExtKeys, $aReconcilKeys, $aResult) {
CMDBSource::Query('START TRANSACTION');
//change value during the test
$db_core_transactions_enabled=MetaModel::GetConfig()->Get('db_core_transactions_enabled');
MetaModel::GetConfig()->Set('db_core_transactions_enabled',false);
@@ -409,8 +395,7 @@ class BulkChangeTest extends ItopDataTestCase {
"Y-m-d H:i:s", // date format
true // localize
);
$oChange = \CMDBObject::GetCurrentChange();
$aRes = $oBulk->Process($oChange);
$aRes = $oBulk->Process();
static::assertNotNull($aRes);
foreach ($aRes as $aRow) {
foreach ($aRow as $i => $oCell) {
@@ -429,7 +414,6 @@ class BulkChangeTest extends ItopDataTestCase {
}
$this->assertEquals($aResult[0], $aRow[0]->GetDisplayableValue());
}
CMDBSource::Query('ROLLBACK');
MetaModel::GetConfig()->Set('db_core_transactions_enabled',$db_core_transactions_enabled);
}

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