Compare commits

..

343 Commits

Author SHA1 Message Date
v-dumas
b919adf4d8 N°9763 - Fix error in Model sample data 2026-08-21 11:57:01 +02:00
v-dumas
4ebd03eebb Ensure extensions menus in Service Management end up after Catalog 2026-08-20 17:43:55 +02:00
odain-cbd
4f891a9170 N°9935 - Disable data compatibility check when env-production empty (#1004)
* N°9935 - Disable data compatibility check when env-production empty

* N°9935 - redundant test cleanup
2026-08-20 15:27:50 +02:00
odain
7583ede3b3 N°9949 - code style 2026-08-20 11:59:56 +02:00
odain-cbd
95cb21cff7 N°9949 - Cannot uninstall IPAM from setup wizard screen (#1015)
* N°9949 - Cannot uninstall IPAM from setup wizard screen

* N°9949 - add test coverage

* N°9949 - handle list in compilation
2026-08-20 10:03:25 +02:00
Molkobain
cdd7bbedcb N°9625 - Update welcome popup introduction message and add message for new portal look 2026-08-20 09:39:51 +02:00
Molkobain
ea1ed03d0c N°9948 - Fix core dashlets after AttributeDefinition split (#1014) 2026-08-20 08:53:09 +02:00
Lenaick
2073e8c10c N°9861 - Check user can go to audit step only when keep current choice button has been clicked (#1013)
* N°9861 - Check user can go to audit step only when keep current choice button has been clicked

* Rename variable to improve clarity
2026-08-19 16:03:25 +02:00
odain
1a41349d7b N°8806 - fix setup tests 2026-08-19 15:54:39 +02:00
odain
fdc1ef40e6 N°9875 - error msg typo 2026-08-19 15:27:49 +02:00
odain-cbd
4722551a16 N°9875 - Error deprecated on component version object (#1010)
* N°9875 - Error deprecated on component version object - propose boilerplate methods to handle ZipArchive

* N°9875 - Error deprecated on component version object - use ZipArchive boilerplate methods

* N°9875 - code style

* N°9875 - review feedbacks

* N°9875 - simplify return type of ZipArchiveCreateWithTempNam

* N°9875 - add @since
2026-08-19 15:14:32 +02:00
odain
a2934ef05b N°8806 - Installation multiple extension with dependence via ITSM Designer - fix check compatibity when new dictionaries not present yet in iTop core like SV-SV 2026-08-19 15:13:30 +02:00
Anne-Catherine
1cdc2c5648 N°9934 - OQL with magic field (#1011) 2026-08-19 11:46:28 +02:00
Lenaick
31eddad029 N°9876 - A non installed extension should not be selected (#992)
* N°9876 - A non installed extension should not be selected if it is not mandatory

* N°9876 - A non installed extension should not be selected if it is not mandatory

* N°9876 - A non installed extension should not be selected if it is not mandatory

* N°9876 Streamline and refactor ComputeChoiceFlags

* N°9876 - Fix several conditions :
* Installed extension with dependency issue must be checked and disabled without force-uninstall
* Mandatory extension with force-uninstall must be checked
* Mandatory or installed extension must keep unchecked status when move to another step

* N°9876 - remove mandatory condition for extensions not included in package

* N°9876 - Undo remove ComputeChoiceFlags method return type

---------

Co-authored-by: Timmy38 <timothee.callet@combodo.com>
2026-08-19 10:47:31 +02:00
Molkobain
b43d71f324 N°824 - Fix typo in notification filling 2026-08-19 09:54:23 +02:00
Anne-Catherine
26033774b8 N°9622 - Error in the user story life cycle (#915) 2026-08-19 08:22:35 +02:00
Lenaick
27c0fe191b N°9904 - Add explanation message when "Check compatibility" button is not available (#1003) 2026-08-18 16:40:18 +02:00
Stephen Abello
fac132f707 Merge branch 'support/3.2' into develop 2026-08-18 08:52:38 +02:00
Stephen Abello
6f9a4cb55f N°9937 - Restore modal icon spins around the text (#1005) 2026-08-18 08:46:17 +02:00
Anne-Catherine
c07317dc71 N°6071 - Fix prefilled Tagset displayed (in transition or clone) but not saved (#751)
* N°6071 - Prefill Tagset in transition displayed but not saved

* Apply suggestions from code review

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

---------

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2026-08-17 18:57:44 +02:00
Eric Espie
a3fcc6fdf0 Merge branch 'feature/9902-record_mtp_infos' into develop 2026-08-17 16:45:01 +02:00
Eric Espie
b13065f23f N°9902 - Store MTP info in DB 2026-08-17 16:42:51 +02:00
odain
2eaca40632 N°9936 - Designer - can't upload package 2026-08-17 16:18:42 +02:00
odain
7f3a1811d7 N°9888 - test coverage enhancement regarding php version checks during setup 2026-08-17 13:51:50 +02:00
Lenaick
87b48c638c N°9861 - Keep current choice button is too permissive (#989)
* N°9861 - Keep current choice button is too permissive

* N°9861 - Changes from review

* Add GetLatestWizardStateFromStepClass test and improve CanMoveForwardFromChoiceFlags readability

* Remove $bPopStep parameter from GetLatestWizardStateFromStepClass function
2026-08-17 11:52:24 +02:00
jf-cbd
54a1221e4a N°8123 - Improve on mention data parsing (#749) 2026-08-17 10:37:40 +02:00
v-dumas
97c589c440 N°9643 - Dataflow: fix duplicated format 2026-08-17 10:20:45 +02:00
v-dumas
68e799c0fd N°9643 - Dataflow: add other formats and protocols 2026-08-17 09:34:48 +02:00
Molkobain
658825b308 N°7385 - Fix regression from b21a02dcb8 which made mentions on sub classes no longer working 2026-08-15 12:26:07 +02:00
Molkobain
af08824866 N°8313 - Revert parts of merge that actually loose changes from previous commits of 2026-08-15 12:25:57 +02:00
Eric Espie
8693951aa9 N°9902 - Store MTP info in DB 2026-08-14 16:26:15 +02:00
Eric Espie
a28e481ee4 N°9455 - Feature removal - Access rights 2026-08-14 14:48:48 +02:00
Vincent Dumas
6032628a70 N°9460 - Ticket ITIL : Add Child Incidents tab on UserRequest (#1000) 2026-08-14 10:08:10 +02:00
Benjamin Dalsass
2892949e33 N°8726 - Portal deprecated removal (#990) 2026-08-14 09:14:51 +02:00
Molkobain
ba643ba2ab N°8699 - Add comment about attribute definition class aliases and there non deprecation 2026-08-13 19:30:23 +02:00
Vincent Dumas
3711e8a5a1 N°9666 & N°9658 - Localize sample data (User and Delivery Model) (#999) 2026-08-13 10:57:33 +02:00
Stephen Abello
a38fef5bbf Merge branch 'support/3.2' into develop
# Conflicts:
#	css/setup.css
#	datamodels/2.x/itop-portal-base/portal/public/css/portal.css
#	datamodels/2.x/itop-portal-base/portal/public/css/portal.scss
2026-08-13 09:35:42 +02:00
Stephen Abello
23e4ce7263 N°9128 - Add padding to fullscreen CKEditor (#998) 2026-08-13 09:29:41 +02:00
Eric Espie
1bba486ee8 N°9902 - Can't MTP with 3.3.0-dev 2026-08-12 16:58:00 +02:00
Stephen Abello
801090fddb N°9911 - Fix keyboard shortcuts no working anymore since 5b9e0a1d (#997) 2026-08-12 14:56:12 +02:00
Eric Espié
1d77b9faa2 N°9909 - Fix setup token unexpectedly deleted during setup due to concurrent access (#996) 2026-08-12 14:00:54 +02:00
Benjamin Dalsass
5282913791 N°9908 - GetAllowedValues from AttributeClassAttCodeSet return no value (#995) 2026-08-12 08:43:51 +02:00
Benjamin Dalsass
36fae27355 N°8860 - Régression Export CSV clé externe, plus de critères (#971) 2026-08-11 15:28:30 +02:00
Vincent Dumas
812f0b573d N°9643 - Dataflow: split protocol & type, add url and last change (#994) 2026-08-11 11:44:13 +02:00
Eric Espie
c7c16f091b N°9902 - Can't MTP with 3.3.0-dev 2026-08-11 10:10:57 +02:00
v-dumas
3739067473 N°9900 - The plural of Software is... Software ! 2026-08-10 17:09:33 +02:00
Molkobain
fd553b86dd N°4693 - Fix iBackofficeSassExtension API returning only 1 file 2026-08-09 19:50:52 +02:00
Purple Grape
d7625d9650 N°9891 - Update chinese translations (#937)
* update zh_cn translation

rebase on the latest develop branch

* update zh_cn translation

rebase on the latest develop branch

* update zh_cn translation

* Update zh_cn.dict.itop-oauth-client.php

* Update zh_cn.dictionary.itop.core.php

* Update zh_cn.dict.itop-request-mgmt.php

* update zh_cn translation

rebase on the latest develop branch

* update zh_cn translation

rebase on the latest develop branch

* update zh_cn translation

* Update zh_cn.dict.itop-oauth-client.php

* Update zh_cn.dict.itop-request-mgmt.php

* Update zh_cn.dict.itop-portal-base.php

* Update zh_cn.dictionary.itop.datatable.php

* Update zh_cn.dictionary.itop.welcome-popup.php

* Update zh_cn.dictionary.itop.ui.php
2026-08-09 18:41:18 +02:00
Timmy38
893a1d7f71 N°9888 Add config parameter mention in error message when php cli version is lower than required 2026-08-07 12:37:00 +02:00
Anne-Cath
fda1d3dd16 N°3789 - Bulk actions on replicas: (Resync, Force Update, Delete,...) - Deletes the fake message left by mistake 2026-08-07 11:38:39 +02:00
Thomas Casteleyn
61464fc819 N°9881 - Update dutch translations of itop-config-mgmt module (#986)
* dict(itop-config-mgmt): Update NL translations

* Update datamodels/2.x/itop-config-mgmt/dictionaries/nl.dict.itop-config-mgmt.php
2026-08-07 10:35:40 +02:00
v-dumas
c281677768 N°8142 - Ticket menus label/tooltip standardisation 2026-08-07 08:57:51 +02:00
Molkobain
e280251623 Merge remote-tracking branch 'origin/support/3.2' into develop
# Conflicts:
#	dictionaries/ui/application/links/zh_cn.dictionary.itop.links.php
#	dictionaries/zh_cn.dictionary.itop.core.php
#	dictionaries/zh_cn.dictionary.itop.ui.php
2026-08-06 16:36:15 +02:00
Molkobain
f657308137 Fix PHP code styles for Chinese dictionaries 2026-08-06 16:33:31 +02:00
Molkobain
4596404666 Merge remote-tracking branch 'origin/support/3.2' into develop
# Conflicts:
#	.doc/itop-version-history.md
2026-08-06 13:38:39 +02:00
Molkobain
64394099f7 📝 Add 3.2.3-1 and 3.2.3-2 to version history 2026-08-06 13:37:44 +02:00
Stephen Abello
67e2899a60 N°9787 - Fix skipped test 2026-08-06 10:30:37 +02:00
Molkobain
8d75531d95 Merge remote-tracking branch 'origin/support/3.2' into develop
# Conflicts:
#	datamodels/2.x/combodo-backoffice-fullmoon-high-contrast-theme/dictionaries/zh_cn.dict.combodo-backoffice-fullmoon-high-contrast-theme.php
#	datamodels/2.x/combodo-backoffice-fullmoon-protanopia-deuteranopia-theme/dictionaries/zh_cn.dict.combodo-backoffice-fullmoon-protanopia-deuteranopia-theme.php
#	datamodels/2.x/combodo-backoffice-fullmoon-tritanopia-theme/dictionaries/zh_cn.dict.combodo-backoffice-fullmoon-tritanopia-theme.php
#	datamodels/2.x/itop-attachments/dictionaries/zh_cn.dict.itop-attachments.php
#	datamodels/2.x/itop-backup/dictionaries/zh_cn.dict.itop-backup.php
#	datamodels/2.x/itop-bridge-cmdb-services/dictionaries/zh_cn.dict.itop-bridge-cmdb-services.php
#	datamodels/2.x/itop-bridge-cmdb-ticket/dictionaries/zh_cn.dict.itop-bridge-cmdb-ticket.php
#	datamodels/2.x/itop-change-mgmt-itil/dictionaries/zh_cn.dict.itop-change-mgmt-itil.php
#	datamodels/2.x/itop-change-mgmt/dictionaries/zh_cn.dict.itop-change-mgmt.php
#	datamodels/2.x/itop-config-mgmt/dictionaries/zh_cn.dict.itop-config-mgmt.php
#	datamodels/2.x/itop-config/dictionaries/zh_cn.dict.itop-config.php
#	datamodels/2.x/itop-core-update/dictionaries/zh_cn.dict.itop-core-update.php
#	datamodels/2.x/itop-hub-connector/dictionaries/zh_cn.dict.itop-hub-connector.php
#	datamodels/2.x/itop-incident-mgmt-itil/dictionaries/zh_cn.dict.itop-incident-mgmt-itil.php
#	datamodels/2.x/itop-knownerror-mgmt/dictionaries/zh_cn.dict.itop-knownerror-mgmt.php
#	datamodels/2.x/itop-portal-base/dictionaries/zh_cn.dict.itop-portal-base.php
#	datamodels/2.x/itop-problem-mgmt/dictionaries/zh_cn.dict.itop-problem-mgmt.php
#	datamodels/2.x/itop-request-mgmt-itil/dictionaries/zh_cn.dict.itop-request-mgmt-itil.php
#	datamodels/2.x/itop-request-mgmt/dictionaries/zh_cn.dict.itop-request-mgmt.php
#	datamodels/2.x/itop-service-mgmt-provider/dictionaries/zh_cn.dict.itop-service-mgmt-provider.php
#	datamodels/2.x/itop-service-mgmt/dictionaries/zh_cn.dict.itop-service-mgmt.php
#	datamodels/2.x/itop-storage-mgmt/dictionaries/zh_cn.dict.itop-storage-mgmt.php
#	datamodels/2.x/itop-structure/dictionaries/zh_cn.dict.itop-structure.php
#	datamodels/2.x/itop-themes-compat/dictionaries/zh_cn.dict.itop-themes-compat.php
#	datamodels/2.x/itop-tickets/dictionaries/zh_cn.dict.itop-tickets.php
#	datamodels/2.x/itop-virtualization-mgmt/dictionaries/zh_cn.dict.itop-virtualization-mgmt.php
#	dictionaries/core/orm-value/orm-document/zh_cn.dictionary.itop.orm-document.php
#	dictionaries/core/uniqueness-rule/zh_cn.dictionary.itop.uniqueness-rule.php
#	dictionaries/ui/application/links/zh_cn.dictionary.itop.links.php
#	dictionaries/ui/application/newsroom/zh_cn.dictionary.itop.newsroom.php
#	dictionaries/ui/application/welcome-popup/zh_cn.dictionary.itop.welcome-popup.php
#	dictionaries/ui/layouts/activity-panel/zh_cn.dictionary.itop.activity-panel.php
#	dictionaries/ui/layouts/navigation-menu/zh_cn.dictionary.itop.navigation-menu.php
#	dictionaries/ui/pages/notifications-center/zh_cn.dictionary.itop.notifications-center.php
#	dictionaries/ui/pages/preferences/zh_cn.dictionary.itop.preferences.php
#	dictionaries/zh_cn.dictionary.itop.core.php
#	dictionaries/zh_cn.dictionary.itop.ui.php
#	setup/wizardsteps/WizStepLicense2.php
2026-08-04 18:00:19 +02:00
Benjamin DALSASS
1e023e5431 Merge branch 'support/3.2' into develop
# Conflicts:
#	datamodels/2.x/itop-portal-base/portal/public/css/portal.css
2026-08-04 17:03:32 +02:00
Anne-Cath
e2a1c3e79e reformat file for php cs fixer 2026-08-04 15:47:59 +02:00
Purple Grape
c7570d62c1 N°9889 - Update zh_cn translation for 3.2 branch (#938)
* rebase on the latest 3.2 branch

rebase

Co-Authored-By: Thomas Casteleyn <thomas.casteleyn@me.com>

* Update zh_cn.dict.authent-cas.php

* Update zh_cn.dict.authent-external.php

* Update zh_cn.dict.authent-ldap.php

* Update zh_cn.dict.authent-local.php

* Update zh_cn.dict.combodo-backoffice-darkmoon-theme.php

* Update zh_cn.dict.itop-backup.php

* Update zh_cn.dict.itop-change-mgmt.php

* Update zh_cn.dict.itop-faq-light.php

* Update zh_cn.dict.itop-config.php

* Update zh_cn.dict.itop-config-mgmt.php

* Update zh_cn.dict.itop-hub-connector.php

* Update zh_cn.dict.itop-knownerror-mgmt.php

* Update zh_cn.dictionary.itop.core.php

* Update zh_cn.dict.authent-cas.php

* Update zh_cn.dict.authent-external.php

* Update zh_cn.dict.authent-ldap.php

* Update zh_cn.dict.authent-local.php

* Update zh_cn.dict.combodo-backoffice-darkmoon-theme.php

* Update zh_cn.dict.itop-backup.php

* Update zh_cn.dict.itop-config-mgmt.php

* Update zh_cn.dict.itop-config.php

* Update zh_cn.dict.itop-faq-light.php

* update  license  url

* Update datamodels/2.x/authent-ldap/dictionaries/zh_cn.dict.authent-ldap.php

Co-authored-by: Thomas Casteleyn <thomas.casteleyn@me.com>

* Update datamodels/2.x/authent-cas/dictionaries/en.dict.authent-cas.php

---------

Co-authored-by: Thomas Casteleyn <thomas.casteleyn@me.com>
Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2026-08-04 15:26:11 +02:00
Anne-Cath
a30ed63a62 N°3789 - Bulk actions on replicas: (Resync, Force Update, Delete,...) - Resynchronization fix + label improvements 2026-08-04 15:15:37 +02:00
Benjamin DALSASS
dd77124b07 N°4720 - Remove old unreferenced methods or pages that are @deprecated
- deleted function GetTileTemplatePath called in aggragte brick
2026-08-04 14:36:09 +02:00
Lenaick
9bfe0430f5 N°9118 - No Safeguard when hub installation fail (#988)
* N°9118 - No Safeguard when hub installation fail

* Apply suggestion from @greptile-apps[bot]

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-08-04 09:01:34 +02:00
Anne-Cath
3fe91ed0b4 N°8397 - Suppress export V1 from iTop packages 2026-08-03 16:39:19 +02:00
Stephen Abello
ccb9633226 Align portal ui_version default value with XML ui_version 2026-08-03 10:14:00 +02:00
Thomas Casteleyn
82b50dc6cd N°9880 - Add dutch translations to itop-flow-map module (#985) 2026-08-03 09:22:14 +02:00
Vladimir Kunin
d3451f8c30 N°9879 - Update russian translations (#983) 2026-08-03 09:17:43 +02:00
Benjamin DALSASS
b5c8a98695 N°9807 - Polishing CKEditor for iTop 3.3.0-beta1
- fix setup css broken unit test
2026-07-30 09:21:51 +02:00
Lenaick
1403914572 N°9116 - Installing modules in extension folder or production-modules doesn't have the same behavior (#981)
* N°9116 - Installing modules in extension folder or production-modules doesn't have the same behavior

* N°9116 - Installing modules in extension folder or production-modules doesn't have the same behavior

* N°9116 - Installing modules in extension folder or production-modules doesn't have the same behavior
2026-07-28 16:40:23 +02:00
Stephen Abello
b976471a0d N°9385 - Action button is not displaying its dropdown in a filtered BrowseBrick 2026-07-28 10:24:02 +02:00
Benjamin DALSASS
fa66a09104 N°9838 - Polishing CKEditor for iTop 3.2.4
- fix php unit test
2026-07-28 09:39:58 +02:00
Benjamin DALSASS
47981f78e3 Merge remote-tracking branch 'origin/support/3.2' into develop
# Conflicts:
#	css/setup.css
#	datamodels/2.x/itop-portal-base/portal/public/css/portal.css
#	datamodels/2.x/itop-portal-base/portal/public/css/portal.scss
2026-07-28 08:41:18 +02:00
Benjamin DALSASS
476ec75a2e N°9838 - Polishing CKEditor for iTop 3.2.4 2026-07-28 08:39:24 +02:00
Benjamin DALSASS
10c000ddfe N°9807 - Polishing CKEditor for iTop 3.3.0-beta1
- update with last ckeditor changes
2026-07-28 08:23:20 +02:00
Vincent Dumas
7e55b9ccfc N°9703 - Updates italian dictionnary 2026-07-27 18:51:53 +02:00
v-dumas
5bb288ffca N°9800 - broken setup in CI 2026-07-27 18:36:03 +02:00
Vincent Dumas
56d7e40f4c N°9800 New sample data for KnownError and FAQ (#982)
* N°9800 - Fix typos in sample data in User and Service in FR
* N°9800 - typo in sample Server ids and Rack id
2026-07-27 18:16:58 +02:00
Benjamin Dalsass
e0dc1fa571 N°8886 - Deprecation warning in DashetGroupBy in 3.3-dev (#972) 2026-07-27 08:26:37 +02:00
Stephen Abello
d6466c0bd6 Update setup.css 2026-07-24 14:30:17 +02:00
Stephen Abello
eb16e6cbc8 Merge branch 'support/3.2' into develop 2026-07-24 14:29:24 +02:00
Stephen Abello
42007ffd95 N°9702 - Fix user rights matrix display when an accessibility theme displayed a visual cue 2026-07-24 14:28:59 +02:00
Molkobain
6329c157e8 📝 Update releases history with 3.3.0-beta1 2026-07-24 11:31:07 +02:00
Benjamin Dalsass
9ee0548767 N°9807 - Polishing CKEditor for iTop 3.3.0-beta1 (#976) 2026-07-24 09:22:24 +02:00
Lenaick
620a6b9a51 N°9827 - Unable to launch check compatibility when a module with bad dependencies is in production-modules (#978)
* N°9827 - Unable to launch check compatibility when a module with bad dependencies is in production-modules

* Update unit tests

* N°9827 - Unable to launch check compatibility when a module with bad dependencies is in production-modules
2026-07-23 16:23:06 +02:00
Stephen Abello
968c56745a Merge branch 'support/3.2' into develop
# Conflicts:
#	core/attributedef.class.inc.php
2026-07-23 16:08:45 +02:00
Anne-Catherine
04a5e5463a N°9759 - Truncating AttributeText doesn't work as expected in case of multibytes characters (#964)
* N°9759 - Truncate AttributeText don't work as expected in case of multibytes characters

* WIP

* Move TrimValue() base definition to AttributeDefinition class, assign DBObject::SetTrim old behavior to it

* Fix variable types

* Fix variable types

* Apply code review fixes, add tests

* Define GetSize in AttributeDefinition to easy polymorphic calls instead of obscure AttributeDBFieldVoid

* Update php with right since version and ticket, remove useless method overload

* Apply suggestions from code review

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

* Update core/attributedef.class.inc.php

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

* Update core/attributedef.class.inc.php

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

* Apply suggestion from @Molkobain code review

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

* Apply suggestion from @Molkobain code review

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

* Apply suggestion from @Molkobain code review

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

* Apply suggestion from @Molkobain code review

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

* Apply suggestion from @Molkobain code review

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

* Correctly handle null values in new methods

---------

Co-authored-by: Stephen Abello <stephen.abello@combodo.com>
Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2026-07-23 16:07:42 +02:00
Benjamin Dalsass
006dd3d6c2 N°9829 - Add unit tests for parent ticket changes (#979) 2026-07-23 15:32:53 +02:00
lenaick.moreira
a808e89c30 Merge branch 'support/3.3-beta1' into develop 2026-07-23 11:43:00 +02:00
lenaick.moreira
c6e50a0ed7 N°9830 - Fix setup error when trying to remove class with a TagSet attribute 2026-07-23 11:39:42 +02:00
Molkobain
d88687527e Merge remote-tracking branch 'origin/support/3.2.3' into support/3.2 2026-07-23 10:49:09 +02:00
Molkobain
2cc071b07a Merge remote-tracking branch 'origin/support/3.3-beta1' into develop 2026-07-23 10:47:12 +02:00
Molkobain
76a410e00d Revert "N°9541 itop-virtualization-mgmt cannot currently be uninstalled" now that N°9831 has been fixed
This reverts commit 52fca750b0.
2026-07-23 10:45:44 +02:00
Molkobain
c68887c2bc N°9831 - Fix ext. management deletion plan analysis by avoiding overwriting entity for a previously processed class 2026-07-23 10:21:31 +02:00
Molkobain
837b433622 N°9831 - Fix DataCleanupService so it handles external keys the same way as \Combodo\iTop\DataFeatureRemoval\Service\StaticDeletionPlan::DeletionPlanForReferencingClasses and \DBObject::MakeDeletionPlan
- Check if ext. key is nullable first
- Then check if its a DEL_MANUAL option
2026-07-23 10:21:31 +02:00
Timmy38
52fca750b0 N°9541 itop-virtualization-mgmt cannot currently be uninstalled 2026-07-22 17:37:19 +02:00
Benjamin Dalsass
a5c14e4870 N°9723 - IsActionAllowed should work with non instantiated users objects (#973) 2026-07-21 17:41:40 +02:00
Timmy38
0e72b090ee Merge branch 'support/3.3-beta1' into develop 2026-07-21 17:06:30 +02:00
Timmy38
d1aedee245 N°9692 - Fix flag name 2026-07-21 17:04:37 +02:00
Timmy38
1b70b4bf70 N°9692 - Fix flag name 2026-07-21 12:02:31 +02:00
odain
41f51a2c0c Merge branch 'support/3.2' into develop 2026-07-21 08:03:21 +02:00
odain-cbd
73ede13443 N°9746 - Synchro execution time change fixes (#951)
* N°9746 - Synchro execution can make damage during time change (object deletions)

* N°9746 - handle timechange during synchro execution + cleanup m_bIsImportPhaseDateKnown

* N°9746 - PR fix

* N°9746 - add corner test case
2026-07-21 08:01:31 +02:00
odain-cbd
40dd72ef82 N°9746 - Synchro execution time change fixes (#951)
* N°9746 - Synchro execution can make damage during time change (object deletions)

* N°9746 - handle timechange during synchro execution + cleanup m_bIsImportPhaseDateKnown

* N°9746 - PR fix

* N°9746 - add corner test case
2026-07-21 07:59:50 +02:00
Lenaick
c8aea56171 N°9565 - Extension Mgmt : display progression during analysis (#975)
* N°9565 - Extension Mgmt : display progression during analysis

* N°9565 - Extension Mgmt : display progression during analysis

* N°9565 - Extension Mgmt : display progression during analysis

* N°9565 - Extension Mgmt : display progression during analysis

* N°9565 - Extension Mgmt : display progression during analysis

* N°9565 - Validate transaction id

* N°9565 - Move unset bForceCompilation
2026-07-20 11:38:30 +02:00
lenaick.moreira
041437cbeb Merge branch 'support/3.3-beta1' into develop 2026-07-17 17:05:46 +02:00
lenaick.moreira
85d50f31bd N°9692 - Fix null coalescing for extension installation status and clean up test cases 2026-07-17 17:05:06 +02:00
Anne-Catherine
5715e0484f N°9759 - Truncating AttributeText doesn't work as expected in case of multibytes characters (#964)
* N°9759 - Truncate AttributeText don't work as expected in case of multibytes characters

* WIP

* Move TrimValue() base definition to AttributeDefinition class, assign DBObject::SetTrim old behavior to it

* Fix variable types

* Fix variable types

* Apply code review fixes, add tests

* Define GetSize in AttributeDefinition to easy polymorphic calls instead of obscure AttributeDBFieldVoid

* Update php with right since version and ticket, remove useless method overload

* Apply suggestions from code review

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

* Update core/attributedef.class.inc.php

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

* Update core/attributedef.class.inc.php

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

* Apply suggestion from @Molkobain code review

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

* Apply suggestion from @Molkobain code review

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

* Apply suggestion from @Molkobain code review

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

* Apply suggestion from @Molkobain code review

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

* Apply suggestion from @Molkobain code review

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

* Correctly handle null values in new methods

---------

Co-authored-by: Stephen Abello <stephen.abello@combodo.com>
Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2026-07-17 16:40:09 +02:00
lenaick.moreira
109fdfa06b Merge branch 'support/3.3-beta1' into develop
^ Conflicts:
^	datamodels/2.x/combodo-data-feature-removal/src/Controller/DataFeatureRemovalController.php
2026-07-17 14:45:50 +02:00
lenaick.moreira
3c301affb3 N°9679 - Add remote extensions as uninstallable extension in "Extension management" 2026-07-17 14:32:46 +02:00
Lenaick
147fae7f8c N°9679 - Forced uninstallation is not logged when done by extensions management (#956) 2026-07-17 11:25:36 +02:00
odain-cbd
a5aa8c279f fix broken cache warmup after setup (#974)
* fix broken cache warmup after setup

* clear all caches after setup
2026-07-17 10:40:55 +02:00
Molkobain
13fad9efc1 N°9625 - Add Session::UnsetAll() method to ensure that even the "welcome" session value is correctly resetted (#970)
* N°9625 - Add Session::UnsetAll() method to ensure that even the "welcome" session value is correctly resetted

* N°9625 - Add unit tests

* N°9625 - Improve robustness

* N°9625 - Code review updates

* N°9625 - Fix logoff page to ensure all authentication plugins are called before emptying the session
2026-07-17 10:28:58 +02:00
Lenaick
9fbc53cf00 N°9679 - Forced uninstallation is not logged when done by extensions management (#956) 2026-07-17 09:42:58 +02:00
lenaick.moreira
04f91ab4a2 Fix error CI 2026-07-16 09:40:33 +02:00
lenaick.moreira
918f110dc9 Merge branch 'support/3.3-beta1' into develop 2026-07-15 14:46:48 +02:00
lenaick.moreira
8e0246601a Fix unit tests failed 2026-07-15 14:33:55 +02:00
lenaick.moreira
bd9cc9c15e Fix unit tests failed 2026-07-15 14:21:17 +02:00
Eric Espie
60ce21509f Merge branch 'support/3.3-beta1' into develop 2026-07-15 12:10:56 +02:00
Eric Espie
e6cdcd0a5a N°9808 - Wrong dependency when module is in production-modules 2026-07-15 12:09:55 +02:00
Lenaick
2ab3081926 N°9692 - Uninstall / Disable install when extension has module with missing dependencies (#967)
* N°9692 - Uninstall / Disable install when extension has module with missing dependencies

* N°9692 - Uninstall / Disable install when extension has module with missing dependencies

* N°9692 - Uninstall / Disable install when extension has module with missing dependencies

* N°9692 - Refactor dependency handling for extensions to use HasDependencyIssue method

* Rename cannot-be-installed to dependency_issue

* Changes following the review
2026-07-15 09:20:25 +02:00
Molkobain
5cabac63e7 N°7984 - Complete previous revert 2026-07-14 23:32:52 +02:00
Molkobain
3918dcf830 N°7984 - Update setup compiled SCSS 2026-07-14 23:08:40 +02:00
Molkobain
489300552a Revert "Issue/7984 (#688)"
The ticket was not discussed.
2026-07-14 23:07:11 +02:00
Timmy38
8507c606ba N°9788 Adapt html for behat 2026-07-13 10:48:01 +02:00
Molkobain
128989450b 🌐 Translated missing french entries 2026-07-12 21:25:17 +02:00
Molkobain
1bd404af6f 💚 Add executable flag to scripts in .make/release folder 2026-07-12 21:10:10 +02:00
Molkobain
6c770d3e16 📝 Update third-party libraries licenses 2026-07-12 21:05:54 +02:00
Molkobain
a74dbb3a73 💚 Add executable flag to scripts in .make/license folder 2026-07-12 20:57:06 +02:00
Stephen Abello
b365aa0971 N°9628 - Fix issue following 5b9e0a1d being applied to non-jQuery elements (#961) 2026-07-10 15:57:04 +02:00
v-dumas
56aa50c9b1 N°9553 - Skip test due to unexplained CI failure 2026-07-10 15:53:37 +02:00
Vincent Dumas
eacb7f3401 N°9553 - Allow loading of localized sample and structural data based on iTop default language
* Add English, French and German structural and sample data in iTop Community

* Add helper methods to allow :
  * Checking of supported module version format
  * Finding a localized file name based on a provided en_us file name and a language
  * Loading a file if the module upgrade crossed a given version
2026-07-10 14:21:56 +02:00
Eric Espie
b14045af29 N°9648 - Better dependencies error message 2026-07-10 12:10:21 +02:00
Eric Espie
7321eeb23f N°9648 - Better dependencies error message 2026-07-10 12:02:12 +02:00
odain
05b8c529fe Fix cache-warmup after any succesfull setup 2026-07-10 11:14:59 +02:00
Timmy38
a68c017225 N°9681 Fixing edge case where an installed extension may have lost its dependencies 2026-07-09 16:40:43 +02:00
Timmy38
82b52b95a1 N°9681 Update badge tooltip 2026-07-09 14:17:42 +02:00
Molkobain
556f612f7e N°9625 - Update welcome popup messages for iTop 3.3.0 (#955)
* N°9625 - Update welcome popup messages for iTop 3.3.0

* N°9625 - Update illustrations
2026-07-09 11:42:27 +02:00
Eric Espie
edc69bebba Refactor form action URL construction in WizStepDone.php 2026-07-09 08:59:44 +02:00
odain-cbd
dce1994b85 N°9675 - Set up crash - cannot get class from production (#968)
* N°9675 - Set up crash - cannot get class from production

* N°9675 - add some comments
2026-07-09 08:59:33 +02:00
Molkobain
fde6527057 N°3124 - Update PHPDoc 2026-07-08 21:48:42 +02:00
Molkobain
0de0592c18 N°3124 - Update PHPDoc 2026-07-08 21:37:47 +02:00
Vincent Dumas
c9d512cc31 N°9699 - Enable AddMeToContact without write access on Ticket (#949)
* N°9699 - Enable AddMeToContact without write access on Ticket
* exit loop as soon as Lnk found
Co-authored-by: Stephen Abello <stephen.abello@combodo.com>
2026-07-08 16:52:23 +02:00
Eric Espié
a3ef049063 N°9722 - Setup - extension dependencies must be computed with installation choice change (#965)
* WIP compileFrom

* WIP compileFrom

* WIP

* Refactor DataFeatureRemovalController to improve setup redirection and add logging for selected extensions and modules

* Refactor DataFeatureRemovalController to streamline parameter handling for extension management

* Refactor DataFeatureRemovalController and related classes for improved session management and cleanup processes

* N°9722 - replace GetMFModulesToCompile API by GetAdditionalMFModulesBeforeFinalDeltaToCompile + fix test

* N°9722 - Refacto

* N°9722 - remove extensions effectively

* N°9722 - After review

* N°9722 - After review

* N°9722 - Analyze installation before compilation in extension management

*  N°9722 - Refactor and unit tests

* N°9722 - 

* N°9722 - 

* N°9722 - cleanup + geptileia PR feedback

---------

Co-authored-by: odain <olivier.dain@combodo.com>
2026-07-08 16:41:25 +02:00
Benjamin DALSASS
87ec702c5e N°9333 - Logo selection in dashboard is broken
- test broken
2026-07-08 13:29:55 +02:00
Vincent Dumas
127be1933d N°9629 - New fields Tags and Owner on Query class (#963)
* N°9629 - Add Usages EnumSet and Owner on Query class

* N°9629 - Add Usages on structural Queries

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-08 10:37:00 +02:00
Timmy38
2c14030fc5 N°9681 Fix missing data-id 2026-07-08 09:50:35 +02:00
odain
7825c08069 N°9765 - Designer - can't upload extensions 2026-07-08 09:34:37 +02:00
Benjamin DALSASS
4ddf17233f N°9635 - Twig security issues CVE-2026-46633 and CVE-2026-46639 and CVE-2026-46640 2026-07-07 13:52:00 +02:00
Benjamin DALSASS
9493bb03d5 Merge remote-tracking branch 'origin/support/3.2' into develop
# Conflicts:
#	lib/composer/installed.php
2026-07-07 13:51:20 +02:00
Benjamin DALSASS
8f87c759b5 N°9635 - Twig security issues CVE-2026-46633 and CVE-2026-46639 and CVE-2026-46640 2026-07-07 13:47:15 +02:00
Benjamin DALSASS
348c90145d N°9635 - Twig security issues CVE-2026-46633 and CVE-2026-46639 and CVE-2026-46640 2026-07-07 13:43:51 +02:00
Eric Espié
c37143176a N°9753 - Extension management - Run setup display installation and not upgrade (#962)
* 🚧 Refactor installation mode parameter to unify naming convention

* Changed comments on wizard steps and example to clarify upgrade mode handling
2026-07-07 11:40:59 +02:00
Benjamin DALSASS
0c8229a68e N°9333 - Logo selection in dashboard is broken 2026-07-07 09:07:04 +02:00
Timmy38
fda5fe5ded N°9681 Create "Cannot be installed" badge 2026-07-06 14:58:53 +02:00
Eric Espié
6b396adeea N°9571 - Do not allow DB change or install when upgrading iTop (#958)
* N°9571 - Do not allow DB change or install when upgrading iTop

* N°9571 - Fix unit tests

* N°9711 - Removed step Install or Upgrade

* N°9711 - After revue
2026-07-06 10:31:43 +02:00
odain
c448f8411c Merge branch 'support/3.2' into develop 2026-07-03 11:52:36 +02:00
odain
4b4fe55060 N°9689 - Fix itop-portal-base dependancy 2026-07-03 11:49:47 +02:00
Eric Espie
dedcd4af19 N°9711 - Update WizStepSummary to conditionally display database parameters based on return_application parameter 2026-07-03 09:23:13 +02:00
Eric Espie
0556a03839 N°9711 - Clear setup parameters on menu node rendering in UI.php (default page display) 2026-07-03 09:10:11 +02:00
Molkobain
64df429715 Merge remote-tracking branch 'origin/support/3.2' into develop 2026-07-02 21:12:38 +02:00
Molkobain
9ae813db2b Merge remote-tracking branch 'origin/support/3.2.3' into support/3.2 2026-07-02 21:08:45 +02:00
Molkobain
768423a6aa N°7371 - Simplify initial approach by using common API, and revert modification of unnecessary js/utils.js API 2026-07-02 20:51:30 +02:00
odain-cbd
921e7c88fe N°9675 - Set up crash - cannot get class from production (#953)
* N°9675 - raise first exception encountered when starting metamodel

* N°9675 - enhance setup data consistency error feedback

* N°9675 - raise PHP CLI version issue as well

* N°9675 - geptile PR fixes + enhance regexp for cli php version

* cleanup

* N°9675 - remove maintenance mode before any setup compilation

* N°9675 - share php cli check in SetupUtils

* N°9675 - exit maintenance only before audit
2026-07-02 16:59:04 +02:00
Molkobain
d6ef4fb7bb N°9687 - Fix incorrect return type for the DBObjectSearch::ApplyDataFilter() function (#939)
* N°9687 - Fix incorrect return type for the DBObjectSearch::ApplyDataFilter() function

* N°9687 - Add unit test

* N°9687 - Improve unit test after code review

* N°9687 - Improve unit test after code review
2026-07-02 16:11:32 +02:00
Eric Espié
a335e1004b N°9711 - Block user from going back to the setup (#954)
* N°9711 - Store setup parameters in session

* N°9711 - Restrict database parameters display to administrators in setup summary
2026-07-02 12:00:22 +02:00
Molkobain
e882df96d9 📝 Remove duplicated table in PR template 2026-07-02 11:55:34 +02:00
Eric Espié
3a7439e970 N°9639 - Uninstallation Analysis result doesn't list all the object (#950)
* N°9639 - Uninstallation Analysis better count method

* N°9639 - Fix tests

* N°9639 - Refactor deletion plan handling with new entity classes for improved structure and clarity

* N°9639 - Refactor deletion plan methods to use class names instead of table names and add TotalCount method

* N°9639 - Add messages to tests

* N°9639 - Refactor data feature removal logic and update database queries for consistency

* N°9639 - Refactor SQL queries to use backticks for table and field names

* N°9639 - Enhance deletion logic and add circular reference handling in cleanup process

* N°9639 - Add filtering methods to DeletionPlan classes for improved data cleanup

* N°9639 - Add filtering methods to DeletionPlan classes for improved data cleanup

* N°9639 - Refactor DeletionPlanItem to remove unused queries and simplify constructor

* N°9639 - Refactor variable name for clarity in hierarchical key update process

* Update tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/simulate-audit-from-setup.php

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* N°9639 - Clarify comment on deletion plan to indicate prevention of infinite loops

* N°9639 - Remove unused deletion plan from DataFeatureRemovalController

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-06-30 09:37:30 +02:00
jf-cbd
7fca6424ab N°9723 - IsActionAllowed should work with non instantiated users objects (#947)
* Replace references to user objects keys since objects can be non-instantiated (so no key available) and replace them by equivalent code

* Rename test + use self variable for profile

---------

Co-authored-by: jf-cbd <jf-cbd@users.noreply.github.com>
2026-06-25 17:45:43 +02:00
jf-cbd
f09a73c45a N°7060 - create ReadOnly profiles (#935) 2026-06-25 17:38:48 +02:00
Vincent Dumas
5a174dd8fa N°9658 - Fix Profiles retrieval in User sample data loading (#948) 2026-06-25 12:08:21 +02:00
v-dumas
01f32086ec Fix tests for CI with 3.0 sample data and for commit on dev 2026-06-24 11:57:58 +02:00
v-dumas
5c59fdebaf Fix tests for CI with 3.0 sample data and for commit on dev 2026-06-24 11:01:48 +02:00
Vincent Dumas
363d0767d5 N°8142 user request and incident menu label (#903)
* N°8142 - User Request, Incident and Change menu standardization
2026-06-24 09:11:17 +02:00
odain-cbd
5202adb902 N°9717 - Bring unattended error feedbacks back (#943)
* Enhance unattended CLI to log fatal error just like setup wizardinstead of silent failure

* 💚 change unattended output in case of painless FATAL

* 💚 capture FATALs only

* Update setup/unattended-install/unattended-install.php

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-06-23 17:16:04 +02:00
Eric Espie
f77dd7b668 🎨 PHP CS Fixer 2026-06-23 09:07:29 +02:00
Eric Espié
f1af3d5c5f N°9711 - Block user from going back to the setup (#944) 2026-06-22 17:12:57 +02:00
v-dumas
614d3586c8 N°9656 - remove useless old sample data files 2026-06-22 15:37:19 +02:00
Vincent Dumas
f9dc3ee73a N°9656 enrich sample data (#942) 2026-06-22 14:41:51 +02:00
Molkobain
1d9e7f9b55 Update test so it works on both ITIL and standard request management 2026-06-20 17:47:19 +02:00
Molkobain
99a9c8ef40 Merge remote-tracking branch 'origin/support/3.2' into develop 2026-06-20 13:19:49 +02:00
Molkobain
ff2357473b Add comment on dictionary tests to explain how uncomplete they are compared to whet they seem to do 2026-06-20 10:27:58 +02:00
Molkobain
9a93bcb06f PHP Code Styles: Fix composer.lock to be compatible with platform requirements (PHP 8.1) 2026-06-20 10:11:45 +02:00
Anne-Catherine
9b4f3290e8 N°7914 - REST: Allow class based output fields + Allow OQL with multiclass result (#708) 2026-06-19 17:07:14 +02:00
Eric Espié
67792155dd N°9710 - Add fast setup button at the beginning of Setup (#941) 2026-06-19 15:28:03 +02:00
Eric Espie
a23b9aa596 N°9709 - XML load failing on empty one way password 2026-06-19 11:53:02 +02:00
Anne-Cath
017481af21 ci: fix setup css 2026-06-18 17:48:42 +02:00
Eric Espie
4f70441618 N°9678 - Fix extension map initialization 2026-06-18 17:31:13 +02:00
Eric Espie
fb6a332bd0 N°9684 - Add setup page class inclusion in Controller.php 2026-06-18 17:22:05 +02:00
Timmy38
a9af4fc060 N°9701 Add id to badges 2026-06-18 11:05:09 +02:00
odain
6a8dc159c1 revert 75433d3390: Enhance unattended CLI to log fatal error 2026-06-17 15:38:22 +02:00
odain
2930a122c6 N°9144 - fix commit operation rmdir conf/production-build failed as not always empty 2026-06-17 15:21:30 +02:00
odain
75433d3390 Enhance unattended CLI to log fatal error just like setup wizardinstead of silent failure 2026-06-17 15:12:46 +02:00
odain
d4d6fa149d N°9412 - revert fresh-install.xml changes 2026-06-17 14:31:50 +02:00
odain-cbd
207ac373d0 N°9678 - Datamodel not added after extension management setup (#936)
* N°9678 - Datamodel not added after extension management setup

* N°9678 - fix selecting extensions from extensions folder in extension mgt view

* cleanup: remove cached stuff in runtimeenv

* N°9678 - Refactor iTopExtensionsMap instantiation to use GetExtensionsMap method

* N°9678 - Refactor data model compilation and improve directory scanning logic

* N°9678 - Handle MySQLException in ModuleInstallationRepository and remove unnecessary error logging in WizStepModulesChoice

---------

Co-authored-by: Eric Espie <eric.espie@combodo.com>
2026-06-17 10:22:05 +02:00
Timmy38
0240d12007 N°9690 Fix multiple added_extensions 2026-06-16 10:13:22 +02:00
odain
1123a83490 💚 Fix PHP code styles 2026-06-15 22:23:19 +02:00
Molkobain
8ccf832bdd 💚 Fix PHP code styles 2026-06-15 19:10:25 +02:00
Anne-Cath
caa1b8652d FIxes unit tests 2026-06-15 16:51:02 +02:00
Anne-Catherine
ed1cfc3b08 N°7289 - Read-only attribute, dynamically read-write, entry silently ignored on submission (#724) 2026-06-15 16:01:06 +02:00
Anne-Catherine
dd6b03cccc N°3789 - Bulk actions on replicas: (Resync, Force Update, Delete,...) (#546) 2026-06-15 15:58:36 +02:00
Timmy38
3e38e349e5 N°9690 Fix code style 2026-06-15 11:53:14 +02:00
Timmy38
65b58ec4e2 N°9690 Removed and added modules/extensions are now read from GET parameters 2026-06-15 11:49:52 +02:00
Timmy38
6a20e36434 N°9683 Fix Code Style 2026-06-12 11:23:35 +02:00
Vincent Dumas
3805a322c1 N°9160 Copy log entry from parent ticket (#927)
* N°9160 - Refactor UpdateChildxxxxxLog and add test
2026-06-12 09:09:02 +02:00
odain
0bf773a23c N°9454 - fix unattended regression when fresh install and no conf yet 2026-06-11 18:47:48 +02:00
Timmy38
28a09068a0 N°9683 Add markdown in UIBlock for behat 2026-06-11 17:14:48 +02:00
lenaick.moreira
db7fd6b3c0 N°9638 - Rollback undesired changes 2026-06-11 09:41:45 +02:00
odain-cbd
cf76c23ac6 N°9670 - Extension management - even if not selected, an extension is analysed during setup check compatibility (#933)
* N°9670 - Extension management - even if not selected, an extension is analysed during setup check compatibility

* N°9670 - enhance log msg

* N°9670 - PR feedback
2026-06-11 07:09:44 +02:00
lenaick.moreira
b36b0de93c N°9654 - fix typo in param name 2026-06-10 16:52:34 +02:00
Lenaick
a95125e8bf N°9638 - Add setup ContextTag for async operation (#934) 2026-06-10 16:03:09 +02:00
lenaick.moreira
6e536e7dbe N°9597 - Fix CI 2026-06-10 10:53:16 +02:00
Lenaick
733eea8a78 N°9654 - Reduce surface attack on config file rights during setup (#932) 2026-06-09 16:56:58 +02:00
Lenaick
f48efbbd03 N°9597 - Switch environment to a build environment must not be available (#925)
* N°9597 - Switch environment to a build environment must not be available

* N°9597 - Switch environment to a build environment must not be available
2026-06-09 16:49:47 +02:00
Lenaick
1a57a427c0 N°9454 - Make MTT compatible with the dry run (#931)
* N°9454 - Make MTT compatible with the dry run

* N°9454 - Make MTT compatible with the dry run

* N°9454 - Cleaning up configuration handling

* N°9454 - Cleanup code

* N°9454 - Remove writable directories

* Update setup/wizardsteps/WizStepLandingBeforeAudit.php

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update setup/runtimeenv.class.inc.php

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* N°9454 - Secure target environment wizard step done form submission

---------

Co-authored-by: Eric Espie <eric.espie@combodo.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-06-09 14:29:06 +02:00
odain
a419914236 N°9412 - fix uninstall option and others when going to setup from extension mgt 2026-06-08 17:13:49 +02:00
jf-cbd
3426b92768 Fix unit test 2026-06-08 14:04:01 +02:00
jf-cbd
7c44c94de1 Fix unit and CS fixer tests 2026-06-08 13:50:14 +02:00
Purple Grape
4371ddfbbd N°9663 - improved translation for zh_cn (#913) 2026-06-08 13:21:12 +02:00
Timmy38
66bf1dc6fa N°9179 Fix tests for unattended install backward compatibility 2026-06-08 11:45:11 +02:00
Eric Espie
6999000d69 N°9179 - Fix CI 2026-06-08 09:01:20 +02:00
Eric Espié
eedcdf32fe N°9169 - adapt itsm designer connector to the new setup (#928)
* N°9169 - Adapt ITSM designer connector to the new setup - Button Back to designer

* N°9169 - Enhance ITSM designer connector with return button functionality

* N°9169 - Adapt ITSM designer connector to the new setup - Button Back to designer (removed URL in post params)

* N°9169 - Enhance ITSM designer connector with return button functionality and update button styles

---------

Co-authored-by: lenaick.moreira <lenaick.moreira@gmail.com>
2026-06-05 09:59:14 +02:00
Timmy38
2321d89981 N°9179 Fix missing optional steps when using older install-xxxx.xml file with unattended install 2026-06-04 09:38:48 +02:00
odain
3bf47d1e8c Merge branch 'support/3.2' into develop 2026-06-02 17:47:33 +02:00
odain
feb89650e4 curl_close deprecated in PHP 8.5 2026-06-02 17:47:10 +02:00
Eric Espie
b672dfc9f2 Fix PHPStan warning 2026-06-02 15:10:28 +02:00
Eric Espie
210e2cefca Merge commit '07afcdcec6c42a5a9c5d56badc50dbc3401a98d3' into develop 2026-06-01 09:01:52 +02:00
Eric Espie
07afcdcec6 remove not meaningful localized data test 2026-05-29 15:38:20 +02:00
Eric Espie
24fa5d3bbd N°9169 - Adapt ITSM designer connector to the new setup (FIX some unit tests) 2026-05-29 10:54:39 +02:00
Timmy38
65de991853 N°9167 Fix visible menu in analysis result 2026-05-29 10:13:29 +02:00
Eric Espie
a1a82a02c8 Merge remote-tracking branch 'origin/develop' into feature/uninstallation 2026-05-29 09:59:51 +02:00
Eric Espie
6266d4ce15 N°9169 - Adapt ITSM designer connector to the new setup (FIX some unit tests) 2026-05-29 09:56:00 +02:00
lenaick.moreira
367ae456eb N°9169 - Adapt ITSM designer connector to the new setup (WIP) 2026-05-28 15:43:54 +02:00
Eric Espie
c80dbf9bb4 N°9169 - Adapt ITSM designer connector to the new setup (FIX some unit tests) 2026-05-28 11:42:59 +02:00
Timmy38
683b075bf5 N°9172 Modify ext mgmt labels 2026-05-28 11:19:30 +02:00
v-dumas
cbbcc489e0 N°9553 - Fix setup issue due to wrong method params testing 2026-05-28 10:49:45 +02:00
Eric Espie
406f688394 N°9169 - Adapt ITSM designer connector to the new setup (FIX CI) 2026-05-28 10:35:29 +02:00
Eric Espie
06bff3f9e4 N°9169 - Adapt ITSM designer connector to the new setup (WIP) 2026-05-27 17:38:19 +02:00
Vincent Dumas
7c49909dd9 N°9553 upload localised structural data (#917)
* Helper for loading XML localized data during Setup
* Refactor existing code
* Add new localized structural data
2026-05-27 16:32:14 +02:00
Vincent Dumas
51180c9529 N°9604 - Add Flux tab on Server, VM and ContainerApplication (#918) 2026-05-27 16:17:23 +02:00
lenaick.moreira
643ec3bc43 N°9169 - Adapt ITSM designer connector to the new setup (WIP) 2026-05-27 14:58:25 +02:00
lenaick.moreira
b3f984b0e1 N°9169 - Adapt ITSM designer connector to the new setup (WIP) 2026-05-27 14:55:41 +02:00
Eric Espie
cbc02dce52 N°9638 - Regression itop-time-tracking due to iBackofficeDictEntriesExtension UI 2026-05-27 11:30:24 +02:00
Anne-Cath
f236dd6c4d N°1681 - Add new trigger for attachment creation - Add deleted attachment to notification email and fix dictionaries 2026-05-26 16:55:30 +02:00
odain
955ce5fc2c ci: fix setup css 2026-05-25 00:07:56 +02:00
odain
e7ce3fcf8a N°9567 - Make extension mgt to setup work
N°9567 - Make extension mgt to setup work
2026-05-22 14:36:54 +02:00
odain
c156c09be0 N°9567 - ci fix Implicitly marking parameter $oConfig as nullable is deprecated 2026-05-22 07:50:00 +02:00
odain
3df12bba46 Merge branch 'feature/uninstallation2' into feature/uninstallation 2026-05-21 13:56:40 +02:00
odain
0912cc21cf N°9564 - fix WizardChoice test 2026-05-21 13:56:24 +02:00
odain
3aa59ae0b7 N°9564 - fix extension mgt flow from 1st to 2nd screen 2026-05-21 13:54:16 +02:00
odain
a5b5d22e2f N°9564 - enhance simulate testing script to use local settings 2026-05-21 13:52:35 +02:00
odain
08a3044cd1 N°9564 - fix first screen
log cleanup

after merge adaptations
2026-05-21 13:52:30 +02:00
Vincent Dumas
ba2af7ed63 N°9604 - add "flowmaps" as a new neighbour (#914)
* Set DataFlow as FunctionalCI
* Use class name for Badge ID on DataFlow and Container classes including Typology
* Improve DataFlow summary
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-05-21 11:22:10 +02:00
odain
00f1c7498d N°8760 - raise module dependency errors in all setups ecept wizard/unattended 2026-05-21 10:10:10 +02:00
odain
a4345d0311 N°9567 - fix back to setup from extension management - extension screen missing when back after audit
N°9567 - fix again
2026-05-21 10:10:10 +02:00
Timmy38
77a820105f N°9167 Use ExtensionDetails UIBlocks instead of table (#910) 2026-05-21 09:48:33 +02:00
odain
03e1806f9a N°9567 - fix back to setup from extension management - extension screen missing when back after audit 2026-05-20 14:56:55 +02:00
lenaick.moreira
eef06ee032 Merge branch 'support/3.2.3' into support/3.2 2026-05-20 14:20:43 +02:00
Lenaick
d6ce202fa8 N°9614 - Add a specific route to display the icons for an EventNotificationNewsroom (#911)
* N°9614 - Add a specific route to display the icons for an EventNotificationNewsroom

* N°9614 - Display the icon in EventNotificationNewsroom only for events relating to the logged-in user

* N°9614 - Refactor icon download logic in EventNotificationNewsroomService and add unit tests

* Add unit test to verify icon download functionality for users without read access
2026-05-20 14:18:13 +02:00
Eric Espie
b6c48f4bba Removed duplicated itop-flow-map 2026-05-20 13:48:45 +02:00
Eric Espie
e9dd2dc3e6 N°9567 - Fix CI 2026-05-20 10:43:20 +02:00
odain-cbd
ba6cc9dcba N°9567 - Extension Mgmt : Run setup (#912)
* N°9144 - correct next button in audit page

* N°9567 - WIP

code style

* N°9412 - Screen Analysis results wip + test endpoint

* N°9412 - next button label

* N°9412 - Mask CI pbs

* N°9412 - Analysis results screen wip

* N°9412 - Analysis results screen wip

* N°9567 - fix extension map init of installation choices

* N°9567 - fix test

* N°9567 - link from ext mgt to setup WIP

* N°9567 - add enc_type in UIForm to be able to change content type in twigs

* N°9412 - wip

* N°9567 - Extension Mgmt : Run setup

* N°9567 - Extension Mgmt : Run setup

* N°9567 - Extension Mgmt : Run setup (fix post-deletion button inputs)

* N°9567 - Extension Mgmt : Run setup

* N°9567 - Extension Mgmt : Run setup

* N°9567 - Extension Mgmt : Run setup

---------

Co-authored-by: Eric Espie <eric.espie@combodo.com>
2026-05-20 10:16:32 +02:00
Stephen Abello
1c38d989e4 N°9603 - Fix transitions applied when editing only caselog are not working (#908)
* N°9603 - Fix transitions applied when editing only caselog are not working

* Remove unused variable
2026-05-18 17:28:46 +02:00
Lenaick
fb7a38c83f N°9134 Validate extension codes before installation and refine progress message styling (#909)
* N°9134 Validate extension codes before installation and refine progress message styling

* Add test for compile failure when extension code is missing
2026-05-18 10:11:37 +02:00
odain
b29a3b5df8 Merge branch 'develop' into feature/uninstallation 2026-05-15 08:54:38 +02:00
Benjamin DALSASS
48e6203869 N°9044 - Application token and Impersonate (Log in as this user) 2026-05-13 11:14:58 +02:00
Molkobain
e65542f978 Merge remote-tracking branch 'origin/support/3.2.3' into support/3.2 2026-05-07 18:54:39 +02:00
jf-cbd
ab6c50d52d 👷 Update action.yml 2026-05-07 16:24:40 +02:00
jf-cbd
51e7ef32dc 👷 Update action.yml 2026-05-07 16:23:11 +02:00
Stephen Abello
829857ec85 N°9461 - Fix activity panel send options hidden by CKEditor 2026-05-07 15:55:02 +02:00
Lenaick
29502a4297 N°9205 - Show progress messages in setup sequencer (#906)
* N°9205 - Show progress messages in setup sequencer

* N°9205 - Fix unit tests
2026-05-07 10:27:31 +02:00
Eric Espie
c5b678b4cf Merge remote-tracking branch 'origin/feature/uninstallation' into feature/9412-uninstallationchoices 2026-05-06 15:04:24 +02:00
Molkobain
5ac675a587 Merge remote-tracking branch 'origin/support/3.2' into develop 2026-05-06 14:53:01 +02:00
Molkobain
769afb2715 Merge remote-tracking branch 'origin/support/3.2.3' into support/3.2 2026-05-06 14:52:10 +02:00
Eric Espie
cb46889e92 N°9412 - Display Choices on extensions in analysis results 2026-05-06 14:35:33 +02:00
Molkobain
b529a61bc5 Fix PHP code styles 2026-05-06 13:50:58 +02:00
odain
2fe487df3c Merge branch 'feature/9412-uninstallationchoices' into feature/uninstallation 2026-05-06 13:30:12 +02:00
odain
ae1f9391f9 N°9144 - PR review feedbacks 2026-05-06 13:22:46 +02:00
v-dumas
c501280f53 N°7771 - Align Dataflow option with product-itop installation.xml 2026-05-06 11:55:25 +02:00
Eric Espie
f4a1aca3f9 Changed display of modules and installation choices 2026-05-06 11:54:45 +02:00
Vincent Dumas
e662370c32 N°7771 New module for DataFlow management (#905) 2026-05-06 11:28:21 +02:00
Molkobain
3c39c6d8d1 Merge remote-tracking branch 'origin/support/3.2' into develop 2026-05-06 11:19:30 +02:00
Molkobain
a7d0262b21 Merge remote-tracking branch 'origin/support/3.2.3' into support/3.2 2026-05-06 11:18:36 +02:00
Molkobain
c56617abf5 N°8579 - Update PHPDoc 2026-05-06 10:55:39 +02:00
odain
4d2722654a N°9144 - no audit in unattended + tests 2026-05-06 10:53:26 +02:00
odain
73ebf663d6 N°9144 - setup robustness 2026-05-06 10:53:19 +02:00
odain
99a5b47899 N°9144 - adapt unattended XMLs 2026-05-06 10:53:10 +02:00
v-dumas
ff94639a61 N°9471 - Rack can contain Enclosure 2026-05-05 10:46:34 +02:00
Molkobain
7676115725 N°9584 - Fix "Unable to connect with STARTTLS" error when sending emails (#901)
* N°9584 - Fix "Unable to connect with STARTTLS" error when sending emails

* N°9584 - Refactor EMailSymfony transport to add unit tests
2026-05-05 09:51:09 +02:00
Molkobain
43ceaeb5a3 Merge remote-tracking branch 'origin/support/3.2.3' into support/3.2 2026-05-04 16:42:30 +02:00
Håkon Harnes
7cac280b83 N°9574 - Fix CKEditor CSS displayed as part of the email message in Gmail (#898)
* fix(email): generate plain text before inlining HTML CSS

* N°9574 - Add unit tests

* Apply suggestions from code review

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

---------

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2026-05-04 16:33:38 +02:00
odain
109aa03901 N°9412 - Backend cleanup must work on setup settings that differ from runtime - WIP 2026-05-04 16:16:29 +02:00
Eric Espie
80813dba22 N°9503 - Setup wizard : fix symlinks for data/production-modules 2026-04-30 14:35:29 +02:00
Eric Espie
da282e6c24 N°9009 - fix removing sources of a mandatory module 2026-04-30 12:03:38 +02:00
Eric Espie
c4eaeeeb5f fix setup button in develop mode for page core-update 2026-04-30 11:01:26 +02:00
odain
e605b147d3 fix merge from develop: broken setup style 2026-04-30 10:49:53 +02:00
Molkobain
bfcff9b9c8 Merge remote-tracking branch 'origin/develop' into feature/uninstallation 2026-04-30 10:39:14 +02:00
Eric Espie
cd9a755162 N°9165 - fix greptile mistake 2026-04-30 10:28:25 +02:00
Eric Espie
841ea9bb11 N°9165 - fix greptile mistake 2026-04-30 10:26:34 +02:00
Eric Espié
4582256f01 N°9165 - secure data cleanup 2026-04-30 10:20:25 +02:00
Stephen Abello
8de5c2525b Merge branch 'support/3.2' into develop
# Conflicts:
#	css/setup.css
#	datamodels/2.x/itop-portal-base/portal/public/css/portal.css
#	package-lock.json
2026-04-29 17:10:05 +02:00
Stephen Abello
de4ec9d9ce N°9128 - Update CKEditor5 to v48 (#896)
* N°9128 - Update CKEditor5 to v48

* Move some CSS vars to ckeditor, align variable overload in portal and backoffice

* Add specifics to 3.2 portal

* Update CKEditor5 build files

* Update scssphp with patched version that handles pseudo selectors and attribute selectors

* Update scssphp patched version to fix missing $this

* Fix translations specific language file import

* Remove unused maximize parameters

* Update CKEditor5 build files

* Fix font size/color for 3.2 portal

* Avoid CKEditor styles from bleeding into fields
2026-04-29 16:56:20 +02:00
Molkobain
ea35ad48d2 📝 Update itop-version-hisotry.md and fix README.md link 2026-04-29 16:49:11 +02:00
Molkobain
90b7c3ac6a Merge remote-tracking branch 'origin/support/3.2' into develop
# Conflicts:
#	.doc/itop-version-history.md
2026-04-29 16:48:34 +02:00
Molkobain
8f2c990065 📝 Fix itop-version-history.md display due to new branch 2026-04-29 16:47:03 +02:00
Benjamin Dalsass
03437c3453 📝 Restore itop-version-history.md on support/3.2 branch 2026-04-29 16:37:38 +02:00
Purple Grape
1113788d84 N°9518 - fix 2 typo bug (#882) 2026-04-29 14:29:24 +02:00
Benjamin DALSASS
0fc7434794 Merge remote-tracking branch 'origin/develop' into feature/uninstallation 2026-04-29 11:08:03 +02:00
Benjamin DALSASS
eee57b25fa N°8699 - attributedef.class.inc.php to PSR4
- fix CI failed
2026-04-29 09:54:16 +02:00
Benjamin Dalsass
7be19c713b N°8699 - attributedef.class.inc.php to PSR4 - fixs (#890) 2026-04-28 10:46:09 +02:00
Timmy38
42fdffe25b N°9503 Fix phpunit test 2026-04-28 10:43:05 +02:00
odain
309f8f8727 fix merge 2026-04-28 10:27:28 +02:00
odain
16108a80fc Merge branch 'develop' into feature/uninstallation 2026-04-28 10:24:50 +02:00
Lenaick
befe294986 N°3472 - Refactor LDAP debug level handling to ensure proper logging during connection attempts (#892) 2026-04-27 14:26:38 +02:00
lenaick.moreira
d271dda54f N°9341 - Update Turbo third-party lib to v8.0.21 min. to fix security issue 2026-04-27 12:21:17 +02:00
Molkobain
04bb07fc59 Merge remote-tracking branch 'origin/support/3.2' into develop 2026-04-27 11:41:27 +02:00
Molkobain
33a51b47bf N°9549 - Fix emails with mime type different than text/plain or text/html are no longer displayed correctly (#895)
* N°9549 - Fix emails with mime type different than text/plain or text/html are no longer displayed correctly

* N°9549 - Fix mime type comparisons to use the primary mime type instead of the whole string
2026-04-27 11:31:56 +02:00
Timmy38
7e0e357713 N°9503 Fix broken symlink in setup wizard (#881) 2026-04-27 09:14:41 +02:00
jf-cbd
1772a9f73b 📝 Improve issues template 2026-04-24 16:56:38 +02:00
jf-cbd
ed45c3bbb0 📝 Improve issues template 2026-04-24 15:18:03 +02:00
jf-cbd
a49ebd531e Make issues forms despite unformal rules (.tar.gz is supposed to be supported but is not, etc.) 2026-04-23 17:13:08 +02:00
Molkobain
c5ca5f9176 Merge remote-tracking branch 'origin/support/3.2' into develop
# Conflicts:
#	core/event.class.inc.php
2026-04-23 15:54:17 +02:00
Molkobain
e768cc1c0c N°9537 - Update "details" zlist on EventNotificationEmail to display cc and cci attributes 2026-04-23 15:49:30 +02:00
jf-cbd
12b1339542 Update CONTRIBUTING.md and reduce size of related picture 2026-04-23 14:57:26 +02:00
jf-cbd
278254c955 Merge remote-tracking branch 'origin/support/3.2' into develop
# Conflicts:
#	.github/pull_request_template.md
#	CONTRIBUTING.md
2026-04-23 14:43:38 +02:00
jf-cbd
f6741a6306 Update contributions guidelines (#888) 2026-04-23 13:48:23 +02:00
jf-cbd
19386191aa N°8397 - remove empty file (probably re-added by mistake in develop in a merge conflict) 2026-04-22 15:32:43 +02:00
Stephen Abello
d76752a988 N°9491 - Rename new enclosure icon 2026-04-22 14:11:56 +02:00
Vincent Dumas
8c2c4e5ff0 N°9491 - Add OS Family on OS Patch and OS Licence (#886)
* N°9492 - Add icon on OS Family, OS Version and IOS Version classes
* Change enclosure icon
* Update NL dictionnaries
  * Update datamodels/2.x/itop-config-mgmt/dictionaries/nl.dict.itop-config-mgmt.php
  * Update datamodels/2.x/itop-config-mgmt/dictionaries/nl.dict.itop-config-mgmt.php
  * Update datamodels/2.x/itop-config-mgmt/dictionaries/de.dict.itop-config-mgmt.php
  * Update datamodels/2.x/itop-config-mgmt/dictionaries/cs.dict.itop-config-mgmt.php
  * Update datamodels/2.x/itop-config-mgmt/dictionaries/da.dict.itop-config-mgmt.php
Co-authored-by: Thomas Casteleyn <thomas.casteleyn@super-visions.com>
2026-04-22 09:56:12 +02:00
Stephen Abello
4bc28ea38a 💚 Fix CI by recompiling setup.css 2026-04-22 09:05:23 +02:00
jf-cbd
805087a01b N°9319 increase php min. version to 8.2 (#887)
* Update minimum PHP version to 8.2
* Fix previous wrong resolution of merge conflict
2026-04-20 14:47:44 +02:00
Stephen Abello
1ec8aca12d Remove ~~ from French dict entry 2026-04-20 10:23:03 +02:00
Molkobain
f439490bfc Merge remote-tracking branch 'origin/support/3.2' into develop
# Conflicts:
#	approot.inc.php
#	core/config.class.inc.php
#	datamodels/2.x/authent-cas/module.authent-cas.php
#	datamodels/2.x/authent-external/module.authent-external.php
#	datamodels/2.x/authent-ldap/module.authent-ldap.php
#	datamodels/2.x/authent-local/module.authent-local.php
#	datamodels/2.x/combodo-backoffice-darkmoon-theme/module.combodo-backoffice-darkmoon-theme.php
#	datamodels/2.x/combodo-backoffice-fullmoon-high-contrast-theme/module.combodo-backoffice-fullmoon-high-contrast-theme.php
#	datamodels/2.x/combodo-backoffice-fullmoon-protanopia-deuteranopia-theme/module.combodo-backoffice-fullmoon-protanopia-deuteranopia-theme.php
#	datamodels/2.x/combodo-backoffice-fullmoon-tritanopia-theme/module.combodo-backoffice-fullmoon-tritanopia-theme.php
#	datamodels/2.x/combodo-db-tools/module.combodo-db-tools.php
#	datamodels/2.x/itop-attachments/module.itop-attachments.php
#	datamodels/2.x/itop-backup/module.itop-backup.php
#	datamodels/2.x/itop-bridge-cmdb-services/module.itop-bridge-cmdb-services.php
#	datamodels/2.x/itop-bridge-cmdb-ticket/module.itop-bridge-cmdb-ticket.php
#	datamodels/2.x/itop-bridge-datacenter-mgmt-services/module.itop-bridge-datacenter-mgmt-services.php
#	datamodels/2.x/itop-bridge-endusers-devices-services/module.itop-bridge-endusers-devices-services.php
#	datamodels/2.x/itop-bridge-storage-mgmt-services/module.itop-bridge-storage-mgmt-services.php
#	datamodels/2.x/itop-bridge-virtualization-mgmt-services/module.itop-bridge-virtualization-mgmt-services.php
#	datamodels/2.x/itop-bridge-virtualization-storage/module.itop-bridge-virtualization-storage.php
#	datamodels/2.x/itop-change-mgmt-itil/module.itop-change-mgmt-itil.php
#	datamodels/2.x/itop-change-mgmt/module.itop-change-mgmt.php
#	datamodels/2.x/itop-config-mgmt/module.itop-config-mgmt.php
#	datamodels/2.x/itop-config/module.itop-config.php
#	datamodels/2.x/itop-core-update/module.itop-core-update.php
#	datamodels/2.x/itop-datacenter-mgmt/module.itop-datacenter-mgmt.php
#	datamodels/2.x/itop-endusers-devices/module.itop-endusers-devices.php
#	datamodels/2.x/itop-faq-light/module.itop-faq-light.php
#	datamodels/2.x/itop-files-information/module.itop-files-information.php
#	datamodels/2.x/itop-full-itil/module.itop-full-itil.php
#	datamodels/2.x/itop-hub-connector/module.itop-hub-connector.php
#	datamodels/2.x/itop-incident-mgmt-itil/module.itop-incident-mgmt-itil.php
#	datamodels/2.x/itop-knownerror-mgmt/module.itop-knownerror-mgmt.php
#	datamodels/2.x/itop-oauth-client/module.itop-oauth-client.php
#	datamodels/2.x/itop-portal-base/module.itop-portal-base.php
#	datamodels/2.x/itop-portal/module.itop-portal.php
#	datamodels/2.x/itop-problem-mgmt/module.itop-problem-mgmt.php
#	datamodels/2.x/itop-profiles-itil/module.itop-profiles-itil.php
#	datamodels/2.x/itop-request-mgmt-itil/module.itop-request-mgmt-itil.php
#	datamodels/2.x/itop-request-mgmt/module.itop-request-mgmt.php
#	datamodels/2.x/itop-service-mgmt-provider/module.itop-service-mgmt-provider.php
#	datamodels/2.x/itop-service-mgmt/module.itop-service-mgmt.php
#	datamodels/2.x/itop-sla-computation/module.itop-sla-computation.php
#	datamodels/2.x/itop-storage-mgmt/module.itop-storage-mgmt.php
#	datamodels/2.x/itop-structure/module.itop-structure.php
#	datamodels/2.x/itop-themes-compat/module.itop-themes-compat.php
#	datamodels/2.x/itop-tickets/module.itop-tickets.php
#	datamodels/2.x/itop-virtualization-mgmt/module.itop-virtualization-mgmt.php
#	datamodels/2.x/itop-welcome-itil/module.itop-welcome-itil.php
#	datamodels/2.x/version.xml
2026-04-20 09:24:02 +02:00
Molkobain
39011faedd Update ITOP_VERSION constant version for 3.2.3 release 2026-04-20 09:17:18 +02:00
Stephen Abello
46f8f5faeb Update modules and constants version for 3.2.3 release 2026-04-17 15:24:10 +02:00
Stephen Abello
170d24d0ad Update licenses for 3.2.3 release 2026-04-17 15:17:55 +02:00
v-dumas
9683b8d96a N°9499 - Ticket ITIL: remove 'request type' from UserRequest details 2026-04-17 09:51:01 +02:00
Molkobain
a6171c896b Merge remote-tracking branch 'origin/support/3.2' into develop 2026-04-17 09:20:18 +02:00
Benjamin DALSASS
89231976f8 Revert N°8638 - Adapt mysqldump calls to follow iTop SSL configuration 2026-04-16 15:00:01 +02:00
Stephen Abello
4e24ac508d Merge branch 'support/3.2' into develop 2026-04-15 17:01:58 +02:00
Molkobain
e1c804139f 👥 Add Lénaick to the sample data, welcome! 🙌 2026-04-15 16:55:19 +02:00
Stephen Abello
f0a95cbb3d N°9447 - Harmonize title and panel logo rules 2026-04-15 16:49:52 +02:00
Stephen Abello
265acb4a1e Merge branch 'support/3.2' into develop
# Conflicts:
#	css/setup.css
2026-04-15 15:16:01 +02:00
Stephen Abello
30f720b9ad N°9447 - Setup logo is repeating vertically (#884) 2026-04-15 15:13:28 +02:00
1252 changed files with 58010 additions and 17286 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

View File

@@ -2,12 +2,17 @@
```mermaid
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'themeVariables': {
'git0': 'lawngreen',
'git3': 'dodgerblue',
'git4': 'grey',
'git5': 'grey',
'git6': 'grey',
'git7': 'grey'
'git0': '#7ccf00',
'git1': '#7ccf00',
'git2': '#99a1af',
'git3': '#99a1af',
'git4': '#99a1af',
'git5': '#99a1af',
'git6': '#99a1af',
'git7': '#99a1af',
'git8': '#99a1af',
'git9': '#99a1af',
'git10': '#99a1af'
}, 'gitGraph': {'showBranches': true,'mainBranchName': 'develop','rotateCommitLabel': true}} }%%
gitGraph
commit id: "2016-07-06" tag: "2.3.0" type: HIGHLIGHT
@@ -86,24 +91,30 @@ gitGraph
commit id: "2024-01-17a" tag: "2.7.10"
checkout support/3.0
commit id: "2024-01-17b" tag: "3.0.4"
checkout support/2.7
commit id: "2024-09-28" tag: "2.7.11"
checkout support/3.1
commit id: "2024-09-27" tag: "3.1.2"
checkout support/3.2
commit id: "2024-06-25" tag: "3.2.0-beta1" type: REVERSE
commit id: "2024-08-07" tag: "3.2.0"
commit id: "2024-09-13" tag: "3.2.0-2"
checkout support/3.1
commit id: "2024-09-27" tag: "3.1.2"
checkout support/2.7
commit id: "2024-09-28" tag: "2.7.11"
checkout support/2.7
commit id: "2025-02-25" tag: "2.7.12"
commit id: "2025-02-07a" tag: "2.7.12"
checkout support/3.1
commit id: "2025-02-25 " tag: "3.1.3"
commit id: "2025-02-07b" tag: "3.1.3"
checkout support/3.2
commit id: "2025-02-25 " tag: "3.2.1"
commit id: "2025-04-08" tag: "3.2.1-1"
commit id: "2025-08-19" tag: "3.2.2-1"
commit id: "2025-02-07c " tag: "3.2.1"
commit id: "2025-03-31 " tag: "3.2.1-1"
commit id: "2025-07-28 " tag: "3.2.2"
checkout support/2.7
commit id: "2025-10-07" tag: "2.7.13"
commit id: "2025-09-25" tag: "2.7.13"
checkout support/3.2
commit id: "2026-04-27 " tag: "3.2.3"
checkout support/3.2.3
commit id: "2026-05-25 " tag: "3.2.3-1"
commit id: "2026-07-17 " tag: "3.2.3-2"
checkout develop
commit id: "2026-07-23" tag: "3.3.0-beta1"
```
To learn more, check the [iTop community versions history on the official wiki](https://www.itophub.io/wiki/page?id=latest:release:start).

60
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: "Bug report"
description: "Report a bug that you identified in iTop"
type: bug
body:
- type: markdown
attributes:
value: |
Please explain why you're creating this issue :
- Are you willing to create a PR for the bug fix ? If so, we'll indicate in the issue if we're interested in it.
- Then, please describe how to reproduce the issue.
- type: dropdown
id: willing_to_pr
attributes:
label: Are you willing to create (at a later stage) a PR for that?
options:
- 'Yes'
- 'No'
validations:
required: true
- type: input
id: itop_version
attributes:
label: iTop version
description: "Complete iTop version (e.g., 3.2.3)"
validations:
required: false
- type: input
id: php_version
attributes:
label: PHP version
description: "Complete PHP version (e.g., 8.4.20)"
validations:
required: false
- type: textarea
id: reproduction_steps
attributes:
label: Reproduction procedure
description: |
Please explain step by step how to reproduce the issue on a standard iTop Community.
If it requires a custom datamodel, provide the minimal XML delta to reproduce it.
placeholder: |
1. First go there
2. Then do that
3. ...
4. Finally, see that... (what is expected and what is actually happening)
validations:
required: false
- type: upload
id: additional_info
attributes:
label: Additional information (if needed)
description: "Add/drag and drop screenshots, logs or any files that can be relevant for your issue."
validations:
required: false
accept: ".png, .jpg, .jpeg, .gif, .webp, .log, .txt, .json, .csv, .xml, .zip"

47
.github/ISSUE_TEMPLATE/enhancement.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: "Enhancement suggestion"
description: "Suggest an improvement to iTop"
type: feature
body:
- type: markdown
attributes:
value: |
Please explain why you're creating this issue :
- Please describe what's your improvement proposition.
- Then tell us if you're willing to create a PR for this enhancement ? If so, we'll indicate in the issue if we're interested in it.
- type: textarea
id: enhancement_details
attributes:
label: Enhancement details
description: |
Please explain what you want to improve, and your proposition to make it better.
If it requires a custom datamodel, provide the minimal XML delta to reproduce it.
validations:
required: false
- type: input
id: itop_version
attributes:
label: iTop version (if appropriate)
description: "Complete iTop version (e.g., 3.2.3)"
validations:
required: false
- type: dropdown
id: willing_to_pr
attributes:
label: Are you willing to create (at a later stage) a PR for that?
options:
- 'Yes'
- 'No'
validations:
required: true
- type: upload
id: additional_info
attributes:
label: Additional information (if needed)
description: "Add/drag and drop screenshots, logs or any files that can be relevant for your issue."
validations:
required: false
accept: ".png, .jpg, .jpeg, .gif, .webp, .log, .txt, .json, .csv, .xml, .zip"

View File

@@ -1,83 +1,76 @@
<!--
IMPORTANT: Before creating your PR, please create an issue first to know if Combodo is interested in your contribution (not needed for translations PR).
Since we may refuse a PR, it's preferable to create an issue first, to avoid spending time coding something that won't be accepted.
IMPORTANT: Please follow the guidelines within this PR template before submitting it, it will greatly help us process your PR. 🙏
Once you've done it, and we confirmed we're interested in it, please follow the guidelines within this PR template before submitting it, it will greatly help us process your PR. 🙏
Any PRs not following the guidelines or with missing information will not be considered.
-->
## Base information
| Question | Answer
|---------------------------------------------------------------|--------
| Related to a SourceForge thread / Another PR / Combodo ticket? | <!-- Put the URL -->
| Type of change? | Bug fix / Enhancement / Translations
| Question | Answer |
|---------------------------------------------------------------------------------|--------------------------------------|
| Related to a SourceForge thread / Another PR / A GitHub Issue / Combodo ticket? | <!-- Put the URL --> |
| Type of change? | Bug fix / Enhancement / Translations |
## Symptom (bug) / Objective (enhancement)
<!--
If it's a bug
- Explain the symptom in details
- If possible put error messages, logs or screenshots (you can paste image directly in this editor).
If it's an enhancement
- Describe what is blocking you, what is the objective with as much details as possible.
- Describe what is blocking you, what is the objective with as many details as possible.
- Add screenshots if it's related to UI.
-->
## Reproduction procedure (bug)
<!--
Remove this section only if it's NOT a bug.
Otherwise, explain step by step how to reproduce the issue on a standard iTop Community.
Please explain step by step how to reproduce the issue on a standard iTop Community.
If it requires a custom datamodel, provide the minimal XML delta to reproduce it on a standard iTop Community.
-->
1. On iTop x.y.z <!-- Put complete iTop version (eg. 3.1.0-2) -->
2. With PHP x.y.z <!-- Put complete PHP version (eg. 8.1.24) -->
2. First go there
2. Then do that
3. ...
4. Finally, see that...
3. First go there
4. Then do that
5. ...
6. Finally, see that... (what is expected and what is actually happening)
## Reproduction procedure (enhancement - if needed)
<!--
Please explain how we can reproduce the feature/behavior you want to improve, and what's your proposition to make it better.
Add screenshots if it's related to UI.
If it requires a custom datamodel, provide the minimal XML delta to reproduce it on a standard iTop Community.
-->
## Cause (bug)
<!--
Remove this section only if it's NOT a bug.
Otherwise, explain what is the cause of the issue (where in the code and why)
-->
## Proposed solution (bug and enhancement)
<!--
Explain in details how you are proposing to solve this:
- What did you do in the code and why
- If you changed something in the UI, put before / after screenshots (you can paste image directly in this editor)
-->
## Checklist before requesting a review
<!--
Don't remove these lines, check them once done.
-->
- [ ] I have performed a self-review of my code
- [ ] I have tested all changes I made on an iTop instance
- [ ] I have added a unit test, otherwise I have explained why I couldn't
- [ ] Is the PR clear and detailed enough so anyone can understand digging in the code?
## Checklist of things to do before PR is ready to merge
<!--
Things that needs to be done in the PR before it can be considered as ready to be merged
Examples:
- Changes requested in the review
- Unit test to add
- Dictionary entries to translate
- ...
-->
- [ ] ...
- [ ] ...
- [ ] ...
- [ ] Is the PR clear and detailed enough so anyone can understand without digging in the code?

View File

@@ -1,9 +1,13 @@
name: Add PRs to Combodo PRs Dashboard
on:
pull_request_target:
pull_request:
types:
- opened
issues:
types:
- opened
workflow_call:
jobs:
add-to-project:
@@ -31,23 +35,22 @@ jobs:
run: |
curl -X POST -H "Authorization: token ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/Combodo/iTop/issues/${{ github.event.pull_request.number }}/labels \
https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels \
-d '{"labels":["internal"]}'
- name: Set PR author as assignee if member of the organization
if: env.is_member == 'true'
if: env.is_member == 'true' && github.event_name == 'pull_request'
run: |
curl -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}" \
https://api.github.com/repos/Combodo/iTop/issues/${{ github.event.pull_request.number }}/assignees \
https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/assignees \
-d '{"assignees":["${{ github.event.pull_request.user.login }}"]}'
env:
is_member: ${{ env.is_member }}
- name: Add PR to the appropriate project
uses: actions/add-to-project@v1.0.2
uses: actions/add-to-project@v2
with:
project-url: ${{ env.project_url }}
github-token: ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}

0
.make/license/gen-community-license.sh Normal file → Executable file
View File

0
.make/license/sortLicenceXml.php Normal file → Executable file
View File

0
.make/license/updateLicenses.php Normal file → Executable file
View File

0
.make/release/changelog.php Normal file → Executable file
View File

0
.make/release/update-versions.php Normal file → Executable file
View File

0
.make/release/update-xml.php Normal file → Executable file
View File

0
.make/release/update.classes.inc.php Normal file → Executable file
View File

View File

@@ -4,30 +4,33 @@ You want to contribute to iTop? Many thanks to you! 🎉 👍
Here are some guidelines that will help us integrate your work!
## Contributions
### Subjects
You are welcome to create pull requests on any of those subjects:
* 🐛 bug fix
* 🌐 translation / i18n / l10n
* 🚸 enhancement
If you want to implement a **new feature**, please [create a corresponding ticket](https://sourceforge.net/p/itop/tickets/new/) for review.
If you ever want to begin implementation, do so in a fork, and add a link to the corresponding commits in the ticket.
But before creating a PR, please [create a corresponding issue][itop-issues] for review.
We should review within two weeks, and get back to you to indicate if we're interested in your proposal or not.
If you don't create an issue, you won't know if we're interested in your contribution, and you may spend time coding something that won't be accepted.
If you ever want to begin implementation, do so in a fork, and add a link to the corresponding commits in the issue.
For all **security related subjects**, please see our [security policy](SECURITY.md).
All **datamodel modification** should be done in an extension. Beware that such change would
impact all existing customers, and could prevent them from
upgrading!
Combodo has a long experience of datamodel changes: they are very disruptive!
All **datamodel modification** should be done in an extension. Beware that such change would
impact all existing customers, and could prevent them from upgrading!
Combodo has a long experience of datamodel changes: they are very disruptive!
This is why we avoid them in iTop core, especially the changes on existing objects/fields.
If you have an idea you're sure would benefit to all of iTop users, you may
[create a corresponding ticket](https://sourceforge.net/p/itop/tickets/new/) to submit it, but be warned that there are lots of good
If you have an idea you're sure would benefit to all of iTop users, you may
[create a corresponding issue][itop-issues] to submit it, but be warned that there are lots of good
reasons to refuse such changes.
### 📄 License and copyright
iTop is distributed under the AGPL-3.0 license (see the [license.txt] file).
The iTop repository is divided in three parts: iTop (mainly PHP/JS/XML sources and dictionaries), images, and third-party libraries.
@@ -37,48 +40,33 @@ Anyhow, you are encouraged to signal your contribution by the mean of `@author`
If you want to use another license or keep the code ownership (copyright), you may [create an extension][wiki new ext].
[license.txt]: https://github.com/Combodo/iTop/blob/develop/license.txt
[wiki new ext]: https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Astart#by_writing_your_own_extension
[itop-issues]: https://github.com/Combodo/iTop/issues
[wiki new ext]: https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Astart#by_writing_your_own_extension
## 🔀 iTop branch model
When we first start with Git, we were using the [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/) branch model. As
there was some confusions about branches to use for current developed release and previous maintained release, and also because we were
using just a very few of the GitFlow commands, we decided to add just a little modification to this branch model : since april 2020
we don't have a `master` branch anymore.
there was some confusions about branches to use for current developed release and previous maintained release, and also because we were
using just a very few of the GitFlow commands, we decided to add just a little modification to this branch model : since April 2020
we don't have a `master` branch anymore.
Here are the branches we use and their meaning :
Here are the branches we use and their meaning :
- `develop`: ongoing development version
- `release/*`: if present, that means we are working on a alpha/beta/rc version for shipping
- `support/*`: maintenance branches for older versions
For example, if no version is currently prepared for shipping we could have:
- `develop` containing future 3.1.0 version
- `support/3.0`: 3.0.x maintenance version
- `support/2.7`: 2.7.x maintenance version
- `support/2.6`: 2.6.x maintenance version
- `develop` containing future 3.3.0 version
- `support/3.2`: 3.2.x maintenance version
In this example, when 3.1.0-beta is shipped that will become:
- `develop`: future 3.2.0 version
- `release/3.1.0`: 3.1.0-beta
- `support/3.0`: 3.0.x maintenance version
- `support/2.7`: 2.7.x maintenance version
- `support/2.6`: 2.6.x maintenance version
And when 3.1.0 final will be out:
- `develop`: future 3.2.0 version
- `support/3.1`: 3.1.x maintenance version (will host developments for 3.1.1)
- `support/3.0`: 3.0.x maintenance version
- `support/2.7`: 2.7.x maintenance version
- `support/2.6`: 2.6.x maintenance version
Also note that we have a "micro-version" concept : each of those versions have a very small amount of modifications. They are made from
`support/*` branches as well. For example 2.6.2-1 and 2.6.2-2 were made from the `support/2.6.2` branch.
And when 3.3.0 will be out:
- `develop`: future 3.4.0 version
- `support/3.3`: 3.3.x maintenance version (will host developments for 3.3.1)
- `support/3.2`: 3.2.x maintenance version
## Coding
@@ -92,12 +80,11 @@ A [dedicated page](https://www.itophub.io/wiki/page?id=latest%3Acustomization%3A
2. Create a branch in this fork, based on the develop branch
3. Code !
Do create a dedicated branch for each modification you want to propose : if you don't it will be very hard to merge back your work !
Do create a dedicated branch for each modification you want to propose : if you don't, it will be very hard to merge back your work !
Most of the time you should based your developments on the develop branch.
Most of the time you should base your developments on the develop branch.
That may be different if you want to fix a bug, please use develop anyway and ask in your PR if rebase is possible.
### 🎨 PHP styleguide
Please follow [our guidelines](https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Acoding_standards).
@@ -106,7 +93,7 @@ Please follow [our guidelines](https://www.itophub.io/wiki/page?id=latest%3Acust
Please create tests that covers as much as possible the code you're submitting.
Our tests are located in the `test/` directory, containing a PHPUnit config file : `phpunit.xml.dist`.
Our tests are located in the `tests/` directory, containing a PHPUnit config file : `phpunit.xml.dist`.
### Git Commit Messages
@@ -138,14 +125,14 @@ When your code is working, please:
* Rebase your branch on our repo last commit,
* Create a pull request. _Detailed procedure to work on fork and create PR is available [in GitHub help pages](https://help.github.com/articles/creating-a-pull-request-from-a-fork/)_.
* Pull request description: mind to add all the information useful to understand why you're suggesting this modification and anything necessary to dive into your work. Especially:
- Bugfixes: exact steps to reproduce the bug (given/when/then), description of the bug cause and what solution is implemented
- Enhancements: use cases, implementation details if needed
* Mind to check the "[Allow edits from maintainers](https://docs.github.com/en/github-ae@latest/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork)" option ! (note that if you are working with an org fork, this option [won't be available](https://github.com/orgs/community/discussions/5634))
- Bugfixes: exact steps to reproduce the bug (given/when/then), description of the bug cause and what solution is implemented
- Enhancements: use cases, implementation details if needed
* Mind to check the "[Allow edits from maintainers](https://docs.github.com/en/github-ae@latest/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork)" option ! (note that if you are working with an org fork, this
option [won't be available](https://github.com/orgs/community/discussions/5634))
## 🙏 We are thankful
We are thankful for all your contributions to the iTop universe! As a thank you gift, we will send stickers to every iTop (& extensions) contributors!
We are thankful for all your contributions to the iTop universe! As a thank-you gift, we will send stickers to every iTop (& extensions) contributors!
We have one sticker per contribution type. You might get multiple stickers with one contribution though :)
@@ -159,6 +146,6 @@ We have one sticker per contribution type. You might get multiple stickers with
* Beta tester: Test and give feedback on beta releases
* Extension developer: Develop and publish an extension
Here is the design of each stickers for year 2024:
Here is the design of each sticker:
![iTop stickers 2025](.doc/contributing-guide/2025.contributing-stickers-side-by-side.png)
![iTop stickers](.doc/contributing-guide/contributing-stickers-side-by-side.png)

View File

@@ -53,7 +53,7 @@ iTop also offers mass import tools to help you become even more efficient.
[4]: https://www.itophub.io/wiki/page?id=latest:install:requirements
[5]: https://www.itophub.io/wiki
[6]: https://store.itophub.io/en_US/
[7]: itop-version-history.md
[7]: .doc/itop-version-history.md
[10]: https://www.itophub.io/wiki/page?id=latest%3Adatamodel%3Astart#configuration_management_cmdb
[11]: https://www.itophub.io/wiki/page?id=latest%3Adatamodel%3Astart#ticketing

View File

@@ -582,16 +582,30 @@ class UserRightsProfile extends UserRightsAddOnAPI
*/
public function ListProfiles($oUser)
{
$aRet = [];
$oSearch = new DBObjectSearch('URP_UserProfile');
$oSearch->AllowAllData();
$oSearch->NoContextParameters();
$oSearch->Addcondition('userid', $oUser->GetKey(), '=');
$oProfiles = new DBObjectSet($oSearch);
while ($oUserProfile = $oProfiles->Fetch()) {
$aRet[$oUserProfile->Get('profileid')] = $oUserProfile->Get('profileid_friendlyname');
if (!array_key_exists('profile_list', $oUser->ListChanges())) {
// Profiles list is not modified on the $oUser object, we can use DBObjectSearch with `all data` capabilities
// Note: This is the default behavior
$aRet = [];
$oSearch = new DBObjectSearch('URP_UserProfile');
$oSearch->AllowAllData();
$oSearch->NoContextParameters();
$oSearch->Addcondition('userid', $oUser->GetKey(), '=');
$oProfiles = new DBObjectSet($oSearch);
while ($oUserProfile = $oProfiles->Fetch()) {
$aRet[$oUserProfile->Get('profileid')] = $oUserProfile->Get('profileid_friendlyname');
}
return $aRet;
} else {
// Profiles list must be computed with memory changes.
// Note: this is a bit tricky because the user object may have been modified in memory (e.g. a profile added or removed) and we need to take that into account
$aRet = [];
$oProfilesSet = $oUser->Get('profile_list');
foreach ($oProfilesSet as $oUserProfile) {
$aRet[$oUserProfile->Get('profileid')] = $oUserProfile->Get('profileid_friendlyname');
}
return $aRet;
}
return $aRet;
}
public function GetSelectFilter($oUser, $sClass, $aSettings = [])
@@ -705,26 +719,23 @@ class UserRightsProfile extends UserRightsAddOnAPI
protected function GetUserActionGrant($oUser, $sClass, $iActionCode)
{
$this->LoadCache();
// load and cache permissions for the current user on the given class
//
$iUser = $oUser->GetKey();
if (isset($this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode])) {
$aTest = $this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode];
if (is_array($aTest)) {
return $aTest;
if (count($oUser->ListChanges()) === 0) {
// load and cache permissions for the current user on the given class
if (isset($this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iActionCode])) {
$aTest = $this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iActionCode];
if (is_array($aTest)) {
return $aTest;
}
}
}
$sAction = self::$m_aActionCodes[$iActionCode];
$bStatus = null;
// Cache user's profiles
if (false === array_key_exists($iUser, $this->aUsersProfilesList)) {
$this->aUsersProfilesList[$iUser] = UserRights::ListProfiles($oUser);
}
$aProfileList = $this->GetProfileList($oUser);
// Call the API of UserRights because it caches the list for us
foreach ($this->aUsersProfilesList[$iUser] as $iProfile => $oProfile) {
foreach ($aProfileList as $iProfile => $oProfile) {
$bGrant = $this->GetProfileActionGrant($iProfile, $sClass, $sAction);
if (!is_null($bGrant)) {
if ($bGrant) {
@@ -742,7 +753,9 @@ class UserRightsProfile extends UserRightsAddOnAPI
$aRes = [
'permission' => $iPermission,
];
$this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode] = $aRes;
if (count($oUser->ListChanges()) === 0) {
$this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iActionCode] = $aRes;
}
return $aRes;
}
@@ -824,18 +837,14 @@ class UserRightsProfile extends UserRightsAddOnAPI
{
$this->LoadCache();
// Note: this code is VERY close to the code of IsActionAllowed()
$iUser = $oUser->GetKey();
// Cache user's profiles
if (false === array_key_exists($iUser, $this->aUsersProfilesList)) {
$this->aUsersProfilesList[$iUser] = UserRights::ListProfiles($oUser);
}
$aProfileList = $this->GetProfileList($oUser);
// Note: The object set is ignored because it was interesting to optimize for huge data sets
// and acceptable to consider only the root class of the object set
$bStatus = null;
// Call the API of UserRights because it caches the list for us
foreach ($this->aUsersProfilesList[$iUser] as $iProfile => $oProfile) {
foreach ($aProfileList as $iProfile => $oProfile) {
$bGrant = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode);
if (!is_null($bGrant)) {
if ($bGrant) {
@@ -893,6 +902,25 @@ class UserRightsProfile extends UserRightsAddOnAPI
}
return $bHasSharing;
}
/**
* @param \User $oUser
*
* @return array
* @throws \Exception
*/
public function GetProfileList(User $oUser): array
{
if (count($oUser->ListChanges()) === 0) { // if user is already in db and not changed
$iUser = $oUser->GetKey();
if (false === array_key_exists($iUser, $this->aUsersProfilesList)) {
$aProfiles = UserRights::ListProfiles($oUser);
$this->aUsersProfilesList[$iUser] = $aProfiles;
}
return $this->aUsersProfilesList[$iUser];
}
return UserRights::ListProfiles($oUser);
}
}
UserRights::SelectModule('UserRightsProfile');

View File

@@ -1,7 +1,7 @@
<?php
/**
* Implement this interface to add sass file (SCSS) to the backoffice pages.
* Implement this interface to add sass files (SCSS) to the backoffice pages.
* example: return "css/setup.scss"
*
* @api
@@ -11,9 +11,9 @@
interface iBackofficeSassExtension
{
/**
* @return string
* @see \iTopWebPage::$a_styles
* @return array An array of relative paths (from loaded import paths) to the files to compile and include
* @see \iTopWebPage::$a_linked_stylesheets
* @api
*/
public function GetSass(): string;
public function GetSassRelPaths(): array;
}

View File

@@ -97,33 +97,125 @@ class RestUtils
* @throws Exception
* @api
*/
public static function GetFieldList($sClass, $oData, $sParamName)
public static function GetFieldList($sClass, $oData, $sParamName, $bFailIfNotFound = true)
{
$sFields = self::GetOptionalParam($oData, $sParamName, '*');
$aShowFields = [];
if ($sFields == '*') {
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) {
$aShowFields[$sClass][] = $sAttCode;
}
} elseif ($sFields == '*+') {
foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass) {
foreach (MetaModel::ListAttributeDefs($sRefClass) as $sAttCode => $oAttDef) {
$aShowFields[$sRefClass][] = $sAttCode;
}
}
} else {
foreach (explode(',', $sFields) as $sAttCode) {
$sAttCode = trim($sAttCode);
if (($sAttCode != 'id') && (!MetaModel::IsValidAttCode($sClass, $sAttCode))) {
throw new Exception("$sParamName: invalid attribute code '$sAttCode'");
}
$aShowFields[$sClass][] = $sAttCode;
}
}
return $aShowFields;
return match($sFields) {
'*' => self::GetFieldListForClass($sClass),
'*+' => self::GetFieldListForParentClass($sClass),
default => self::GetLimitedFieldListForClass($sClass, $sFields, $sParamName, $bFailIfNotFound),
};
}
/**
* Check if the requested field list asks for an extended output.
*
* Extended output is requested when using '*+' or class-scoped field definitions.
*
* @param string $sFields Requested field specification.
*
* @return bool
*/
public static function HasRequestedExtendedOutput(string $sFields): bool
{
return match($sFields) {
'*' => false,
'*+' => true,
default => substr_count($sFields, ':') > 1,
};
}
/**
* Check if the requested field list asks for all output fields.
*
* @param string $sFields Requested field specification.
*
* @return bool
*/
public static function HasRequestedAllOutputFields(string $sFields): bool
{
return match($sFields) {
'*', '*+' => true,
default => false,
};
}
protected static function GetFieldListForClass(string $sClass): array
{
return [$sClass => array_keys(MetaModel::ListAttributeDefs($sClass))];
}
/**
* Build a field list for all child classes of the given parent class.
*
* @param string $sClass Parent class name.
*
* @return array Array of class => list of attribute codes.
*/
protected static function GetFieldListForParentClass(string $sClass): array
{
$aFieldList = [];
foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass) {
$aFieldList = array_merge($aFieldList, self::GetFieldListForClass($sRefClass));
}
return $aFieldList;
}
/**
* Build a restricted field list for one class from a comma-separated attribute list.
*
* @param string $sClass Class name.
* @param string $sFields Comma-separated list of requested attribute codes.
* @param string $sParamName Input parameter name used in error messages.
* @param bool $bFailIfNotFound If true, throws when an attribute code is invalid.
*
* @return array Array containing one class => list of attribute codes.
* @throws Exception When an attribute code is invalid and $bFailIfNotFound is true.
*/
protected static function GetLimitedFieldListForSingleClass(string $sClass, string $sFields, string $sParamName, bool $bFailIfNotFound = true): array
{
$aFieldList = [$sClass => []];
foreach (explode(',', $sFields) as $sAttCode) {
$sAttCode = trim($sAttCode);
if (($sAttCode == 'id') || (MetaModel::IsValidAttCode($sClass, $sAttCode))) {
$aFieldList[$sClass][] = $sAttCode;
} else {
if ($bFailIfNotFound) {
throw new Exception("$sParamName: invalid attribute code '$sAttCode' for class '$sClass'");
}
}
}
return $aFieldList;
}
/**
* Build a restricted field list for one or several classes.
*
* Accepted formats are either "att1,att2" for a single class, or
* "ClassA:att1,att2;ClassB:att3" for class-scoped field definitions.
*
* @param string $sClass Default class name used when no class scope is specified.
* @param string $sFields Requested field specification.
* @param string $sParamName Input parameter name used in error messages.
* @param bool $bFailIfNotFound If true, throws when an attribute code is invalid.
*
* @return array Array of class => list of attribute codes.
* @throws Exception Propagated from GetLimitedFieldListForSingleClass.
*/
protected static function GetLimitedFieldListForClass(string $sClass, string $sFields, string $sParamName, bool $bFailIfNotFound = true): array
{
if (!str_contains($sFields, ':')) {
return self::GetLimitedFieldListForSingleClass($sClass, $sFields, $sParamName, $bFailIfNotFound);
}
$aFieldList = [];
$aFieldListParts = explode(';', $sFields);
foreach ($aFieldListParts as $sClassFields) {
list($sSubClass, $sSubClassFields) = explode(':', $sClassFields);
$aFieldList = array_merge($aFieldList, self::GetLimitedFieldListForSingleClass(trim($sSubClass), trim($sSubClassFields), $sParamName, $bFailIfNotFound));
}
return $aFieldList;
}
/**
* Read and interpret object search criteria from a Rest/Json structure
*

View File

@@ -175,6 +175,11 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
*/
protected $sDisplayMode;
protected $aFieldsMap;
/**
* @var array Store posted values in order to be used in GetAttributeFlags
* @since 3.3.0
*/
protected $aPostedValues = [];
/**
* If true, bypass IsActionAllowedOnAttribute when writing this object
@@ -524,6 +529,12 @@ JS
$sLabel = Dict::S('Tag:Synchronized');
$sSynchroTagId = 'synchro_icon-'.$this->GetKey();
$aTags[$sSynchroTagId] = ['title' => $sTip, 'css_classes' => 'ibo-object-details--tag--synchronized', 'decoration_classes' => 'fas fa-lock', 'label' => $sLabel];
if (UserRights::IsActionAllowed(SynchroReplica::class, UR_ACTION_READ)) {
$oDBSearch = DBObjectSearch::FromOQL('SELECT SynchroReplica WHERE dest_class=:sClass AND dest_id=:id');
$sFilter = rawurlencode($oDBSearch->serialize(false, ['sClass' => get_class($this),'id' => $this->GetKey()]));
$sUrlSearchReplica = 'UI.php?operation=search&filter='.$sFilter;
$oPage->add_ready_script("$('#$sSynchroTagId').on('click',function() {window.location = '$sUrlSearchReplica' });");
}
}
}
@@ -1035,7 +1046,7 @@ HTML
// Add extra data for markup generation
// - Attribute code and AttributeDef. class
$val['attcode'] = $sAttCode;
$val['atttype'] = $sAttDefClass;
$val['atttype'] = $oAttDef->GetTypeShortClassName();
$val['attlabel'] = $sAttLabel;
$val['attflags'] = ($bEditMode) ? $this->GetFormAttributeFlags($sAttCode) : OPT_ATT_READONLY;
@@ -1524,190 +1535,6 @@ HTML
return $sHtml;
}
/**
* @param WebPage $oPage
* @param \CMDBObjectSet $oSet
* @param array $aParams
*
* @throws \Exception
* only used in old and deprecated export.php
*
* @internal Only to be used by `/webservices/export.php` : this is a legacy method that produces wrong HTML (no TR on table body rows)
*/
public static function DisplaySetAsHTMLSpreadsheet(WebPage $oPage, CMDBObjectSet $oSet, $aParams = [])
{
$oPage->add(self::GetSetAsHTMLSpreadsheet($oSet, $aParams));
}
/**
* Spreadsheet output: designed for end users doing some reporting
* Then the ids are excluded and replaced by the corresponding friendlyname
*
* @param \DBObjectSet $oSet
* @param array $aParams
*
* @return string
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MissingQueryArgument
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
* @throws \Exception
*
* @internal Only to be used by `/webservices/export.php` : this is a legacy method that produces wrong HTML (no TR on table body rows)
*/
public static function GetSetAsHTMLSpreadsheet(DBObjectSet $oSet, $aParams = [])
{
$aFields = null;
if (isset($aParams['fields']) && (strlen($aParams['fields']) > 0)) {
$aFields = explode(',', $aParams['fields']);
}
$bFieldsAdvanced = false;
if (isset($aParams['fields_advanced'])) {
$bFieldsAdvanced = (bool)$aParams['fields_advanced'];
}
$bLocalize = true;
if (isset($aParams['localize_values'])) {
$bLocalize = (bool)$aParams['localize_values'];
}
$aList = [];
$aClasses = $oSet->GetFilter()->GetSelectedClasses();
$aAuthorizedClasses = [];
foreach ($aClasses as $sAlias => $sClassName) {
if (UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $oSet) != UR_ALLOWED_NO) {
$aAuthorizedClasses[$sAlias] = $sClassName;
}
}
$aHeader = [];
foreach ($aAuthorizedClasses as $sAlias => $sClassName) {
$aList[$sAlias] = [];
foreach (MetaModel::ListAttributeDefs($sClassName) as $sAttCode => $oAttDef) {
if (is_null($aFields) || (count($aFields) == 0)) {
// Standard list of attributes (no link sets)
if ($oAttDef->IsScalar() && ($oAttDef->IsWritable() || $oAttDef->IsExternalField())) {
$sAttCodeEx = $oAttDef->IsExternalField() ? $oAttDef->GetKeyAttCode().'->'.$oAttDef->GetExtAttCode() : $sAttCode;
$aList[$sAlias][$sAttCodeEx] = $oAttDef;
if ($bFieldsAdvanced && $oAttDef->IsExternalKey(EXTKEY_RELATIVE)) {
$sRemoteClass = $oAttDef->GetTargetClass();
foreach (MetaModel::GetReconcKeys($sRemoteClass) as $sRemoteAttCode) {
$aList[$sAlias][$sAttCode.'->'.$sRemoteAttCode] = MetaModel::GetAttributeDef(
$sRemoteClass,
$sRemoteAttCode
);
}
}
}
} else {
// User defined list of attributes
if (in_array($sAttCode, $aFields) || in_array($sAlias.'.'.$sAttCode, $aFields)) {
$aList[$sAlias][$sAttCode] = $oAttDef;
}
}
}
// Replace external key by the corresponding friendly name (if not already in the list)
foreach ($aList[$sAlias] as $sAttCode => $oAttDef) {
if ($oAttDef->IsExternalKey()) {
unset($aList[$sAlias][$sAttCode]);
$sFriendlyNameAttCode = $sAttCode.'_friendlyname';
if (!array_key_exists(
$sFriendlyNameAttCode,
$aList[$sAlias]
) && MetaModel::IsValidAttCode($sClassName, $sFriendlyNameAttCode)) {
$oFriendlyNameAtt = MetaModel::GetAttributeDef($sClassName, $sFriendlyNameAttCode);
$aList[$sAlias][$sFriendlyNameAttCode] = $oFriendlyNameAtt;
}
}
}
foreach ($aList[$sAlias] as $sAttCodeEx => $oAttDef) {
$sColLabel = $bLocalize ? MetaModel::GetLabel($sClassName, $sAttCodeEx) : $sAttCodeEx;
$oFinalAttDef = $oAttDef->GetFinalAttDef();
if (get_class($oFinalAttDef) == 'AttributeDateTime') {
$aHeader[] = $sColLabel.' ('.Dict::S('UI:SplitDateTime-Date').')';
$aHeader[] = $sColLabel.' ('.Dict::S('UI:SplitDateTime-Time').')';
} else {
$aHeader[] = $sColLabel;
}
}
}
$sHtml = "<table border=\"1\">\n";
$sHtml .= "<tr>\n";
$sHtml .= "<td>".implode("</td><td>", $aHeader)."</td>\n";
$sHtml .= "</tr>\n";
$oSet->Seek(0);
while ($aObjects = $oSet->FetchAssoc()) {
$aRow = [];
foreach ($aAuthorizedClasses as $sAlias => $sClassName) {
$oObj = $aObjects[$sAlias];
foreach ($aList[$sAlias] as $sAttCodeEx => $oAttDef) {
if (is_null($oObj)) {
$aRow[] = '<td></td>';
} else {
$oFinalAttDef = $oAttDef->GetFinalAttDef();
if (get_class($oFinalAttDef) == 'AttributeDateTime') {
$sDate = $oObj->Get($sAttCodeEx);
if ($sDate === null) {
$aRow[] = '<td></td>';
$aRow[] = '<td></td>';
} else {
$iDate = AttributeDateTime::GetAsUnixSeconds($sDate);
$aRow[] = '<td>'.date(
'Y-m-d',
$iDate
).'</td>'; // Format kept as-is for 100% backward compatibility of the exports
$aRow[] = '<td>'.date(
'H:i:s',
$iDate
).'</td>'; // Format kept as-is for 100% backward compatibility of the exports
}
} else {
if ($oAttDef instanceof AttributeCaseLog) {
$rawValue = $oObj->Get($sAttCodeEx);
$outputValue = str_replace(
"\n",
"<br/>",
utils::EscapeHtml($rawValue->__toString())
);
// Trick for Excel: treat the content as text even if it begins with an equal sign
$aRow[] = '<td x:str>'.$outputValue.'</td>';
} else {
$rawValue = $oObj->Get($sAttCodeEx);
// Due to custom formatting rules, empty friendlynames may be rendered as non-empty strings
// let's fix this and make sure we render an empty string if the key == 0
if ($oAttDef instanceof AttributeExternalField && $oAttDef->IsFriendlyName()) {
$sKeyAttCode = $oAttDef->GetKeyAttCode();
if ($oObj->Get($sKeyAttCode) == 0) {
$rawValue = '';
}
}
if ($bLocalize) {
$outputValue = utils::EscapeHtml($oFinalAttDef->GetEditValue($rawValue));
} else {
$outputValue = utils::EscapeHtml($rawValue);
}
$aRow[] = '<td>'.$outputValue.'</td>';
}
}
}
}
}
$sHtml .= implode("\n", $aRow);
$sHtml .= "</tr>\n";
}
$sHtml .= "</table>\n";
return $sHtml;
}
/**
* @param WebPage $oPage
* @param \CMDBObjectSet $oSet
@@ -3239,8 +3066,12 @@ EOF
}
$aAllowedValues = MetaModel::GetAllowedValues_att($sClass, $sAttCode, $aArgs);
if (is_array($aAllowedValues) && count($aAllowedValues) == 1) {
$aValues = array_keys($aAllowedValues);
$this->Set($sAttCode, $aValues[0]);
// linkset are ignored
// N°9622 - Error in the user story life cycle
if (!$oAttDef->IsLinkSet()) {
$aValues = array_keys($aAllowedValues);
$this->Set($sAttCode, $aValues[0]);
}
}
}
}
@@ -3986,6 +3817,7 @@ HTML;
$aErrors = [];
$aFinalValues = [];
$this->aPostedValues = $aValues; // Store the values for later use (e.g. in GetAttributeFlags)
foreach ($this->GetWriteableAttList(array_keys($aValues), $aErrors, $aAttFlags) as $sAttCode => $oAttDef) {
$aFinalValues[$sAttCode] = $aValues[$sAttCode];
}
@@ -4155,6 +3987,27 @@ HTML;
$sTagSetJson = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", null, 'raw_data');
if ($sTagSetJson !== null) { // bulk modify, direct linked set not handled
$value = json_decode($sTagSetJson, true);
if ($this->IsNew()) {
if (is_array($value['orig_value'])) {
foreach ($value['orig_value'] as $val) {
if (!in_array($val, $value['removed'])) {
$value['added'][] = $val;
}
}
}
} else {
$aCurrentValues = $this->Get($sAttCode)->GetValues();
foreach ($value['orig_value'] as $val) {
if (!in_array($val, $aCurrentValues) && !in_array($val, $value['removed']) && !in_array($val, $value['added'])) {
$value['added'][] = $val;
}
}
foreach ($aCurrentValues as $val) {
if (!in_array($val, $value['orig_value']) && !in_array($val, $value['removed']) && !in_array($val, $value['added'])) {
$value['removed'][] = $val;
}
}
}
}
break;
@@ -4495,7 +4348,7 @@ HTML;
$oDivField = FieldUIBlockFactory::MakeLarge("");
// UIContentBlockUIBlockFactory::MakeStandard(null,["field_container field_large"]);
$oDivField->AddDataAttribute("attribute-type", $sAttDefClass);
$oDivField->AddDataAttribute("attribute-type", $oAttDef->GetTypeShortClassName());
$oDivField->AddDataAttribute("attribute-label", $sAttMetaDataLabel);
$oDivField->AddDataAttribute("attribute-flag-hidden", false);
$oDivField->AddDataAttribute("attribute-flag-read-only", false);
@@ -5064,10 +4917,10 @@ EOF
} else {
if (count($aObjects) == 1) {
$oObj = $aObjects[0];
$sSubtitle = Dict::Format('UI:Delect:Confirm_Object', $oObj->GetHyperLink());
$sSubtitle = Dict::Format('UI:Delete:Confirm_Object', $oObj->GetHyperLink());
} else {
$sSubtitle = Dict::Format(
'UI:Delect:Confirm_Count_ObjectsOf_Class',
'UI:Delete:Confirm_Count_ObjectsOf_Class',
count($aObjects),
MetaModel::GetName($sClass)
);

View File

@@ -501,7 +501,7 @@ EOF
*/
public function Render($oPage, $bEditMode = false, $aExtraParams = [], $bCanEdit = true)
{
$aExtraParams['dashboard_div_id'] = utils::Sanitize($aExtraParams['dashboard_div_id'] ?? null, $this->GetId(), utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
$aExtraParams['dashboard_div_id'] = utils::Sanitize($aExtraParams['dashboard_div_id'] ?? $this->GetId(), $this->GetId(), utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
/** @var \DashboardLayoutMultiCol $oLayout */
$oLayout = new $this->sLayoutClass();

View File

@@ -25,6 +25,20 @@ use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
use Combodo\iTop\Application\UI\Base\iUIBlock;
use Combodo\iTop\Application\UI\Base\UIBlock;
use Combodo\iTop\Application\WebPage\WebPage;
use Combodo\iTop\Core\AttributeDefinition\AttributeDateTime;
use Combodo\iTop\Core\AttributeDefinition\AttributeDecimal;
use Combodo\iTop\Core\AttributeDefinition\AttributeDuration;
use Combodo\iTop\Core\AttributeDefinition\AttributeEncryptedString;
use Combodo\iTop\Core\AttributeDefinition\AttributeEnum;
use Combodo\iTop\Core\AttributeDefinition\AttributeExternalField;
use Combodo\iTop\Core\AttributeDefinition\AttributeFriendlyName;
use Combodo\iTop\Core\AttributeDefinition\AttributeInteger;
use Combodo\iTop\Core\AttributeDefinition\AttributeLinkedSet;
use Combodo\iTop\Core\AttributeDefinition\AttributeOneWayPassword;
use Combodo\iTop\Core\AttributeDefinition\AttributePassword;
use Combodo\iTop\Core\AttributeDefinition\AttributePercentage;
use Combodo\iTop\Core\AttributeDefinition\AttributeSubItem;
use Combodo\iTop\Core\AttributeDefinition\iAttributeNoGroupBy;
require_once(APPROOT.'application/forms.class.inc.php');
@@ -440,7 +454,7 @@ EOF
// For external fields, find the real type of the target
$sExtFieldAttCode = $sAttCode;
$sTargetClass = $sClass;
while (is_a($sAttType, 'AttributeExternalField', true)) {
while (is_a($sAttType, AttributeExternalField::class, true)) {
$sExtKeyAttCode = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'extkey_attcode');
$sTargetAttCode = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'target_attcode');
$sTargetClass = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtKeyAttCode, 'targetclass');
@@ -450,13 +464,13 @@ EOF
}
$aForbidenAttType = [
'AttributeLinkedSet',
'AttributeFriendlyName',
AttributeLinkedSet::class,
AttributeFriendlyName::class,
'iAttributeNoGroupBy', //we cannot only use iAttributeNoGroupBy since this method is also used by the designer who do not have access to the classes' PHP reflection API. So the known classes has to be listed altogether
'AttributeOneWayPassword',
'AttributeEncryptedString',
'AttributePassword',
iAttributeNoGroupBy::class, //we cannot only use iAttributeNoGroupBy since this method is also used by the designer who do not have access to the classes' PHP reflection API. So the known classes has to be listed altogether
AttributeOneWayPassword::class,
AttributeEncryptedString::class,
AttributePassword::class,
];
foreach ($aForbidenAttType as $sForbidenAttType) {
if (is_a($sAttType, $sForbidenAttType, true)) {
@@ -468,7 +482,7 @@ EOF
if (!in_array($sLabel, $aGroupBy)) {
$aGroupBy[$sAttCode] = $sLabel;
if (is_a($sAttType, 'AttributeDateTime', true)) {
if (is_a($sAttType, AttributeDateTime::class, true)) {
$aGroupBy[$sAttCode.':hour'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Hour', $sLabel);
$aGroupBy[$sAttCode.':month'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Month', $sLabel);
$aGroupBy[$sAttCode.':day_of_week'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-DayOfWeek', $sLabel);
@@ -1102,7 +1116,7 @@ abstract class DashletGroupBy extends Dashlet
$aAttributeTypes = $this->oModelReflection->ListAttributes($this->sClass);
if (isset($aAttributeTypes[$this->sGroupByAttCode])) {
$sAttributeType = $aAttributeTypes[$this->sGroupByAttCode];
if (is_subclass_of($sAttributeType, 'AttributeDateTime') || $sAttributeType == 'AttributeDateTime') {
if (is_subclass_of($sAttributeType, AttributeDateTime::class) || $sAttributeType == AttributeDateTime::class) {
$this->sOrderDirection = 'asc';
} else {
$this->sOrderDirection = 'desc';
@@ -1258,7 +1272,7 @@ abstract class DashletGroupBy extends Dashlet
if ($this->oModelReflection->IsValidAttCode($sClass, $this->sGroupByAttCode)) {
$aAttributeTypes = $this->oModelReflection->ListAttributes($sClass);
$sAttributeType = $aAttributeTypes[$this->sGroupByAttCode];
if (is_subclass_of($sAttributeType, 'AttributeDateTime') || $sAttributeType == 'AttributeDateTime') {
if (is_subclass_of($sAttributeType, AttributeDateTime::class) || $sAttributeType == AttributeDateTime::class) {
// Note: an alternative to this somewhat hardcoded way of doing things would be to implement...
//$oExpr = Expression::FromOQL($this->sGroupByExpr);
//$aTranslationData = array($oQuery->GetClassAlias() => array($this->sGroupByAttCode => new ScalarExpression(date('Y-m-d H:i:s', $iTime))));
@@ -1289,7 +1303,7 @@ abstract class DashletGroupBy extends Dashlet
foreach ($aValues as $sValue) {
$aDisplayValues[] = ['label' => $sValue, 'value' => (int)rand(1, 15)];
}
} elseif (is_subclass_of($sAttributeType, 'AttributeEnum') || $sAttributeType == 'AttributeEnum') {
} elseif (is_subclass_of($sAttributeType, AttributeEnum::class) || $sAttributeType == AttributeEnum::class) {
$aAllowed = $this->oModelReflection->GetAllowedValues_att($sClass, $this->sGroupByAttCode);
if ($aAllowed) { // null for non enums
foreach ($aAllowed as $sValue => $sValueLabel) {
@@ -1458,11 +1472,11 @@ abstract class DashletGroupBy extends Dashlet
}
foreach ($this->oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType) {
switch ($sAttType) {
case 'AttributeDecimal':
case 'AttributeDuration':
case 'AttributeInteger':
case 'AttributePercentage':
case 'AttributeSubItem': // TODO: Known limitation: no unit displayed (values in sec)
case AttributeDecimal::class:
case AttributeDuration::class:
case AttributeInteger::class:
case AttributePercentage::class:
case AttributeSubItem::class: // TODO: Known limitation: no unit displayed (values in sec)
$sLabel = $this->oModelReflection->GetLabel($sClass, $sAttCode);
$aFunctionAttributes[$sAttCode] = $sLabel;
break;

View File

@@ -1078,7 +1078,6 @@ JS
if ($aCounts[$sStateValue] == 0) {
$aCounts[$sStateValue] = ['link' => '-', 'label' => $aCounts[$sStateValue]];
;
} else {
$oSingleGroupByValueFilter = $this->m_oFilter->DeepClone();
$oSingleGroupByValueFilter->AddCondition($sStateAttrCode, $sStateValue, '=');
@@ -1708,7 +1707,7 @@ JS
$oBlock->bAdvancedMode = utils::ReadParam('advanced', false);
$oBlock->sCsvFile = strtolower($this->m_oFilter->GetClass()).'.csv';
$oBlock->sDownloadLink = utils::GetAbsoluteUrlAppRoot().'webservices/export.php?expression='.urlencode($this->m_oFilter->ToOQL(true)).'&format=csv&filename='.urlencode($oBlock->sCsvFile);
$oBlock->sDownloadLink = utils::GetAbsoluteUrlAppRoot().'webservices/export-v2.php?expression='.urlencode($this->m_oFilter->ToOQL(true)).'&format=csv&filename='.urlencode($oBlock->sCsvFile);
$oBlock->sLinkToToggle = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=search'.$oAppContext->GetForLink(true).'&filter='.rawurlencode($this->m_oFilter->serialize()).'&format=csv';
// Pass the parameters via POST, since expression may be very long
$aParamsToPost = [
@@ -1725,7 +1724,7 @@ JS
$oBlock->sLinkToToggle = $oBlock->sLinkToToggle.'&advanced=1';
$oBlock->sChecked = '';
}
$oBlock->sAjaxLink = utils::GetAbsoluteUrlAppRoot().'webservices/export.php';
$oBlock->sAjaxLink = utils::GetAbsoluteUrlAppRoot().'webservices/export-v2.php';
$oBlock->sCharsetNotice = false;
$oBlock->sJsonParams = json_encode($aParamsToPost);
@@ -1947,8 +1946,8 @@ class MenuBlock extends DisplayBlock
$sSelectedClassName = MetaModel::GetName($sSelectedClass);
// Check rights on class
$bIsBulkModifyAllowed = (!MetaModel::IsAbstract($sSelectedClass)) && UserRights::IsActionAllowed($sSelectedClass, UR_ACTION_BULK_MODIFY) && ($oReflectionClass->IsSubclassOf('cmdbAbstractObject'));
$bIsBulkDeleteAllowed = (bool) UserRights::IsActionAllowed($sSelectedClass, UR_ACTION_BULK_DELETE);
$bIsBulkModifyAllowed = (!MetaModel::IsAbstract($sSelectedClass)) && UserRights::IsActionAllowed($sSelectedClass, UR_ACTION_BULK_MODIFY) && (($oReflectionClass->IsSubclassOf(cmdbAbstractObject::class) || $sSelectedClass === SynchroReplica::class));
$bIsBulkDeleteAllowed = (bool)UserRights::IsActionAllowed($sSelectedClass, UR_ACTION_BULK_DELETE);
// Refine filter on selected class so bullk actions occur on the right class
$oSelectedClassFilter = $this->GetFilter()->DeepClone();
@@ -1958,7 +1957,15 @@ class MenuBlock extends DisplayBlock
// Action label dict code has a specific suffix for "Link" / "Remote" aliases to allow dedicated labels in linksets.
$sActionLabelCodeSuffix = in_array($sSelectedAlias, ['Link', 'Remote']) ? $sSelectedAlias : 'Class';
if ($bIsBulkModifyAllowed) {
$this->AddBulkModifyObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:ModifyAll:'.$sSelectedAlias, Dict::Format('UI:Menu:ModifyAll_'.$sActionLabelCodeSuffix, $sSelectedClassName));
if ($sSelectedClass === SynchroReplica::class) {
$this->AddBulkModifyObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:UnlinkAll:', Dict::S('Class:SynchroReplica/Action:unlink_all'), 'unlink');
$this->AddBulkModifyObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:UnLinkSynchroAll:', Dict::S('Class:SynchroReplica/Action:unlinksynchro_all'), 'unlinksynchro');
$this->AddBulkModifyObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:SynchroAll:', Dict::S('Class:SynchroReplica/Action:synchro_all'), 'synchro');
$this->AddBulkModifyObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:AllowDeleteAll:', Dict::S('Class:SynchroReplica/Action:allowdelete_all'), 'allowdelete');
$this->AddBulkModifyObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:DenyDeleteAll:', Dict::S('Class:SynchroReplica/Action:denydelete_all'), 'denydelete');
} else {
$this->AddBulkModifyObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:ModifyAll:'.$sSelectedAlias, Dict::Format('UI:Menu:ModifyAll_'.$sActionLabelCodeSuffix, $sSelectedClassName));
}
}
if ($bIsBulkDeleteAllowed) {
$this->AddBulkDeleteObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:BulkDelete:'.$sSelectedAlias, Dict::Format('UI:Menu:BulkDelete_'.$sActionLabelCodeSuffix, $sSelectedClassName));
@@ -2478,11 +2485,11 @@ class MenuBlock extends DisplayBlock
* @since 3.1.0
* @internal
*/
protected function AddBulkModifyObjectsMenuAction(array &$aActions, string $sClass, string $sFilter, string $sActionIdentifier = 'UI:Menu:ModifyAll', $sActionLabel = 'UI:Menu:ModifyAll'): void
protected function AddBulkModifyObjectsMenuAction(array &$aActions, string $sClass, string $sFilter, string $sActionIdentifier = 'UI:Menu:ModifyAll', $sActionLabel = 'UI:Menu:ModifyAll', $sOperationName = 'modify'): void
{
$aActions[$sActionIdentifier] = [
'label' => Dict::S($sActionLabel),
'url' => $this->PrepareUrlForStandardMenuAction($sClass, "operation=select_for_modify_all&class=$sClass&filter=".urlencode($sFilter)),
'label' => Dict::S($sActionLabel),
'url' => $this->PrepareUrlForStandardMenuAction($sClass, 'operation=select_for_'.$sOperationName.'_all&class='.$sClass.'&filter='.urlencode($sFilter)),
] + $this->GetDefaultParamsForMenuAction();
}

View File

@@ -14,6 +14,8 @@ class CoreException extends Exception
/**
* CoreException constructor.
*
* ATTENTION: Logging here will break the CI
*
* @param string $sIssue error message
* @param array|null $aContextData key/value array, value MUST implements _toString
* @param string $sImpact

View File

@@ -25,11 +25,11 @@
*/
use Combodo\iTop\Application\Branding;
use Combodo\iTop\Application\Helper\Session;
use Combodo\iTop\Application\WebPage\ErrorPage;
use Combodo\iTop\Application\WebPage\NiceWebPage;
use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Service\Events\EventService;
use Combodo\iTop\Service\Session\Session;
/**
* Web page used for displaying the login form
@@ -369,16 +369,7 @@ class LoginWebPage extends NiceWebPage
public static function ResetSession()
{
// Unset all of the session variables.
Session::Unset('auth_user');
Session::Unset('login_state');
Session::Unset('can_logoff');
Session::Unset('archive_mode');
Session::Unset('impersonate_user');
Session::Unset('PluginProperties');
Session::Unset('UrlMakerClass');
Session::Unset('itop_env');
Session::Unset('obj_messages');
Session::Unset('profile_list');
Session::UnsetAll();
UserRights::_ResetSessionCache();
// If it's desired to kill the session, also delete the session cookie.
// Note: This will destroy the session, and not just the session data!

View File

@@ -113,17 +113,43 @@ abstract class Query extends cmdbAbstractObject
]
));
MetaModel::Init_AddAttribute(new AttributeExternalKey(
"owner_id",
[
"targetclass" => 'Contact',
"allowed_values" => null,
"sql" => 'owner_id',
"is_null_allowed" => true,
"depends_on" => [],
"display_style" => 'select',
"always_load_in_tables" => false,
"on_target_delete" => DEL_AUTO,
]
));
MetaModel::Init_AddAttribute(new AttributeEnumSet(
"usages",
[
"allowed_values" => null,
"possible_values" => new ValueSetEnumPadded("export,reference,notif,draft,dashlet"),
"sql" => "usages",
"depends_on" => [],
"is_null_allowed" => true,
"max_items" => 12,
]
));
// Display lists
MetaModel::Init_SetZListItems(
'details',
['name', 'is_template', 'description']
['name', 'is_template', 'owner_id', 'usages', 'description']
); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('list', ['description']); // Attributes to be displayed for a list
MetaModel::Init_SetZListItems('list', ['description', 'usages']); // Attributes to be displayed for a list
// Search criteria
MetaModel::Init_SetZListItems('standard_search', ['name', 'description', 'is_template']); // Criteria of the std search form
MetaModel::Init_SetZListItems(
'default_search',
['name', 'description', 'is_template']
['name', 'description', 'is_template', 'owner_id', 'usages']
); // Criteria of the default search form
// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
}
@@ -143,6 +169,15 @@ abstract class Query extends cmdbAbstractObject
return parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState);
}
public function GetInitialStateAttributeFlags($sAttCode, &$aReasons = [])
{
// read only attribute
if (in_array($sAttCode, ['export_count', 'export_last_date', 'export_last_user_id'])) {
return OPT_ATT_READONLY;
}
return parent::GetInitialStateAttributeFlags($sAttCode, $aReasons);
}
/**
* Return export url.
*
@@ -214,15 +249,15 @@ class QueryOQL extends Query
MetaModel::Init_SetZListItems(
'details',
[
'col:col1' => ['fieldset:Query:baseinfo' => ['name', 'is_template', 'description', 'oql', 'fields']],
'col:col1' => ['fieldset:Query:baseinfo' => ['name', 'is_template', 'owner_id', 'usages', 'description', 'oql', 'fields']],
'col:col2' => ['fieldset:Query:exportInfo' => ['export_count', 'export_last_date', 'export_last_user_id', 'export_last_user_contact']],
]
); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('list', ['description']); // Attributes to be displayed for a list
MetaModel::Init_SetZListItems('list', ['description', 'usages']); // Attributes to be displayed for a list
// Search criteria
MetaModel::Init_SetZListItems(
'standard_search',
['name', 'description', 'is_template', 'fields', 'oql']
['name', 'description', 'is_template', 'owner_id', 'usages', 'oql']
); // Criteria of the std search form
}

View File

@@ -17,6 +17,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
use Combodo\iTop\Application\Helper\Session;
use Combodo\iTop\Service\Startup\StartupService;
require_once(APPROOT.'core/cmdbobject.class.inc.php');
require_once(APPROOT.'application/utils.inc.php');
@@ -69,26 +70,7 @@ $oKPI->ComputeAndReport("Session Start");
$sSwitchEnv = utils::ReadParam('switch_env', null);
$bAllowCache = true;
if (($sSwitchEnv != null) && file_exists(APPCONF.$sSwitchEnv.'/'.ITOP_CONFIG_FILE) && (Session::Get('itop_env') !== $sSwitchEnv)) {
Session::Set('itop_env', $sSwitchEnv);
$sEnv = $sSwitchEnv;
$bAllowCache = false;
// Reset the opcache since otherwise the PHP "model" files may still be cached !!
if (function_exists('opcache_reset')) {
// Zend opcode cache
opcache_reset();
}
if (function_exists('apc_clear_cache')) {
// APC(u) cache
apc_clear_cache();
}
// TODO: reset the credentials as well ??
} elseif (Session::IsSet('itop_env')) {
$sEnv = Session::Get('itop_env');
} else {
$sEnv = ITOP_DEFAULT_ENV;
Session::Set('itop_env', ITOP_DEFAULT_ENV);
}
$sEnv = (new StartupService())->SetItopEnvironment($sSwitchEnv, $bAllowCache);
$sConfigFile = APPCONF.$sEnv.'/'.ITOP_CONFIG_FILE;
try {
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, $bAllowCache, false /* $bTraceSourceFiles */, $sEnv);

View File

@@ -1886,7 +1886,7 @@ SQL;
CURLOPT_HEADER => false, // don't return the headers in the output
CURLOPT_FOLLOWLOCATION => true, // follow redirects
CURLOPT_ENCODING => "", // handle all encodings
CURLOPT_USERAGENT => "spider", // who am i
CURLOPT_USERAGENT => static::GetConfig()->Get('http.request.user_agent'), // who am i
CURLOPT_AUTOREFERER => true, // set referer on redirect
CURLOPT_CONNECTTIMEOUT => 120, // timeout on connect
CURLOPT_TIMEOUT => 120, // timeout on response
@@ -1917,7 +1917,9 @@ SQL;
$aResponseHeaders[$sName] = $sValue;
}
}
curl_close($ch);
if (PHP_VERSION_ID < 80000) {
curl_close($ch);
}
return $response;
}
@@ -3020,7 +3022,6 @@ TXT
* Note: Only works for backoffice URLs for now
*
* @param string $sText Text containing the mentioned objects to be found
* @param string $sFormat {@uses static::ENUM_TEXT_FORMAT_HTML, ...}
*
* @return array Array of object classes / IDs for the ones found in $sText
*
@@ -3035,13 +3036,45 @@ TXT
public static function GetMentionedObjectsFromText(string $sText): array
{
$aMentionedObjects = [];
$aMentionMatches = [];
$sText = html_entity_decode($sText);
$aMentionAllowedClasses = MetaModel::GetConfig()->Get('mentions.allowed_classes');
$oDom = new \DOMDocument();
$bPreviousUseInternalErrors = libxml_use_internal_errors(true); // to keep processing even in case of "invalid" HTML, cf. testGetMentionedObjectsFromText
preg_match_all('/<a\s*([^>]*)data-object-class="([^"]*)"\s.*data-object-key="([^"]*)"/Ui', $sText, $aMentionMatches);
foreach ($aMentionMatches[0] as $iMatchIdx => $sCompleteMatch) {
$sMatchedClass = $aMentionMatches[2][$iMatchIdx];
$sMatchedId = $aMentionMatches[3][$iMatchIdx];
try {
$oDom->loadHTML('<?xml encoding="UTF-8">'.$sText);
} finally {
libxml_clear_errors();
libxml_use_internal_errors($bPreviousUseInternalErrors);
}
$oXpath = new \DOMXPath($oDom);
$oNodes = $oXpath->query('//a[@data-object-class and @data-object-key]');
foreach ($oNodes as $oObjNode) {
$sMatchedClass = $oObjNode->getAttribute('data-object-class');
$sMatchedId = $oObjNode->getAttribute('data-object-key');
$sMatchedName = trim($oObjNode->textContent);
// Ensure that what we found is actually configured as a mention
$sMentionPrefix = array_search($sMatchedClass, $aMentionAllowedClasses);
// - No direct configuration for the matched class, let's look for a configuration on parent classes
if (false === $sMentionPrefix) {
$bHasMentionConfigurationForParentClass = false;
foreach (MetaModel::EnumParentClasses($sMatchedClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false) as $aMatchedParentClass) {
$sMentionPrefix = array_search($aMatchedParentClass, $aMentionAllowedClasses);
if (false !== $sMentionPrefix) {
$bHasMentionConfigurationForParentClass = true;
break;
}
}
if (false === $bHasMentionConfigurationForParentClass) {
continue;
}
}
// - Test if the name starts with $sMentionPrefix (e.g. '@' for 'Contact' class)
if (false === str_starts_with($sMatchedName, $sMentionPrefix)) {
continue;
}
// Prepare array for matched class if not already present
if (!array_key_exists($sMatchedClass, $aMentionedObjects)) {
@@ -3190,4 +3223,74 @@ TXT
}
}
}
/**
* Read memory limit from the php.ini file
*
* @return int Memory limit in bytes
*/
public static function GetMemoryLimit(): int
{
$sLimit = ini_get('memory_limit');
if ($sLimit == '-1') {
return 128 * 1048576;
}
switch (substr($sLimit, -1)) {
case 'M':
case 'm':
return (int)$sLimit * 1048576;
case 'K':
case 'k':
return (int)$sLimit * 1024;
case 'G':
case 'g':
return (int)$sLimit * 1073741824;
default:
return (int)$sLimit;
}
}
/**
* @since 3.3.0 N°9875
* Open archive and raise appropriate exception.
* Warning: do not forget to close archive afterwhile
* @param string $sArchiveFilePath
* @param int|null $flags
* @return ZipArchive
* @throws \Exception
*/
public static function ZipArchiveOpen(string $sArchiveFilePath, int|null $flags = null): ZipArchive
{
$oZip = new ZipArchive();
if (is_null($flags)) {
$code = $oZip->open($sArchiveFilePath);
} else {
$code = $oZip->open($sArchiveFilePath, $flags);
}
if (true !== $code) {
//ZipArchive::ZIP_ER_NOZIP : 19
if ($code === 19) {
throw new \Exception(sprintf('Cannot open zip file due to inconsistent or empty content'));
}
throw new \Exception(sprintf('Cannot open zip file due to error code %s', $code));
}
return $oZip;
}
/**
* @since 3.3.0 N°9875
* @param string $sDirectory
* @param string $sPrefix
* @return ZipArchive
* @throws \Exception
*/
public static function ZipArchiveCreateWithTempNam(string $sDirectory, string $sPrefix): ZipArchive
{
$sTempnam = tempnam($sDirectory, $sPrefix);
unlink($sTempnam);
$sArchiveName = $sTempnam.'.zip';
return self::ZipArchiveOpen($sArchiveName, ZipArchive::CREATE);
}
}

View File

@@ -4,7 +4,7 @@
"type": "project",
"license": "AGPL-3.0-only",
"require": {
"php": ">=8.1.0 <8.5.0",
"php": ">=8.2.0 <8.5.0",
"ext-ctype": "*",
"ext-dom": "*",
"ext-gd": "*",
@@ -15,18 +15,21 @@
"apereo/phpcas": "dev-master",
"guzzlehttp/guzzle": "^7.5.1",
"league/oauth2-google": "^4.0.1",
"nikic/php-parser": "^4.14.0",
"nikic/php-parser": "dev-master",
"pear/archive_tar": "~1.4.14",
"pelago/emogrifier": "^7.2.0",
"psr/log": "^3.0.0",
"scssphp/scssphp": "dev-combodo/1.x",
"symfony/console": "~6.4.0",
"symfony/dotenv": "~6.4.0",
"symfony/form": "^6.4",
"symfony/framework-bundle": "~6.4.0",
"symfony/http-foundation": "~6.4.0",
"symfony/http-kernel": "~6.4.0",
"symfony/runtime": "~6.4.0",
"symfony/security-csrf": "~6.4.0",
"symfony/twig-bundle": "~6.4.0",
"symfony/validator" : "~6.4.0",
"symfony/var-dumper": "~6.4.0",
"symfony/yaml": "~6.4.0",
"symfony/mailer": "~6.4.0",
@@ -40,6 +43,10 @@
"symfony/web-profiler-bundle": "~6.4.0"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/Combodo/PHP-Parser"
},
{
"type": "vcs",
"url": "https://github.com/EsupPortail/phpCAS"
@@ -60,7 +67,7 @@
},
"config": {
"platform": {
"php": "8.1.0"
"php": "8.2.0"
},
"vendor-dir": "lib",
"preferred-install": {
@@ -80,7 +87,6 @@
"sources"
],
"exclude-from-classmap": [
"application/twigextension.class.inc.php",
"core/oql/build/PHP/",
"core/apc-emulation.php",
"application/startup.inc.php",
@@ -100,7 +106,7 @@
"extra": {
"symfony": {
"allow-contrib": false,
"require": "3.4.*"
"require": "6.4.*"
},
"runtime": {
"dotenv_path": "resources/symfony/.env"

1249
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -916,7 +916,9 @@ class CMDBSource
$aColumn = [];
$aData = self::QueryToArray($sSql);
foreach ($aData as $aRow) {
@$aColumn[] = $aRow[$col];
if ($aRow[$col] !== null) {
$aColumn[] = $aRow[$col];
}
}
return $aColumn;
}
@@ -1579,8 +1581,6 @@ class CMDBSource
if (static::GetDBVendor() === static::ENUM_DB_VENDOR_MYSQL) {
//Mysql 5.7.0 and upper deprecated --ssl and uses --ssl-mode instead
return version_compare(static::GetDBVersion(), '5.7.11', '>=');
} elseif (static::GetDBVendor() === static::ENUM_DB_VENDOR_MARIADB) {
return version_compare(static::GetDBVersion(), '10.2.6', '>=');
}
return false;
}

View File

@@ -730,13 +730,8 @@ abstract class DBObject implements iDisplay
public function SetTrim($sAttCode, $sValue)
{
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
$iMaxSize = $oAttDef->GetMaxSize();
$sLength = mb_strlen($sValue);
if ($iMaxSize && ($sLength > $iMaxSize)) {
$sMessage = " -truncated ($sLength chars)";
$sValue = mb_substr($sValue, 0, $iMaxSize - mb_strlen($sMessage)).$sMessage;
}
$this->Set($sAttCode, $sValue);
$this->Set($sAttCode, $oAttDef->TrimValue($sValue));
}
/**
@@ -2038,7 +2033,7 @@ abstract class DBObject implements iDisplay
}
}
if (!is_null($iMaxSize = $oAtt->GetMaxSize())) {
$iLen = mb_strlen($toCheck);
$iLen = $oAtt->GetSize($toCheck);
if ($iLen > $iMaxSize) {
return "String too long (found $iLen, limited to $iMaxSize)";
}

View File

@@ -1935,9 +1935,8 @@ class DBObjectSearch extends DBSearch
/**
* @inheritDoc
* @return DBObjectSearch
*/
protected function ApplyDataFilters(): DBObjectSearch
protected function ApplyDataFilters(): DBSearch
{
if ($this->IsAllDataAllowed() || $this->IsDataFiltered()) {
return $this;

View File

@@ -676,9 +676,8 @@ class DBUnionSearch extends DBSearch
/**
* @inheritDoc
* @return DBUnionSearch
*/
protected function ApplyDataFilters(): DBUnionSearch
protected function ApplyDataFilters(): DBSearch
{
if ($this->IsAllDataAllowed() || $this->IsDataFiltered()) {
return $this;

View File

@@ -178,7 +178,7 @@ class EventNotificationEmail extends EventNotification
// Display lists
MetaModel::Init_SetZListItems('details', ['date', 'userinfo', 'message', 'trigger_id', 'action_id', 'object_class', 'object_id', 'to', 'cc', 'bcc', 'from', 'subject', 'body', 'attachments']); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('list', ['date', 'message', 'to', 'subject', 'attachments']); // Attributes to be displayed for a list
MetaModel::Init_SetZListItems('list', ['date', 'message', 'to', 'cc', 'bcc', 'subject', 'attachments']); // Attributes to be displayed for a list
// Search criteria
// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form

View File

@@ -434,7 +434,7 @@ JS
* Resize an image so that it fits the maximum width/height defined in the config file
* @param ormDocument $oImage The original image stored as an array (content / mimetype / filename)
* @return ormDocument The resampled image (or the original one if it already fit)
* @deprecated since 3.3.0 Replaced by ormDocument::ResizeImageToFit
* @deprecated 3.3.0 Use ormDocument::ResizeImageToFit instead
*/
public static function ResizeImageToFit(ormDocument $oImage, &$aDimensions = null)
{

View File

@@ -614,6 +614,13 @@ class LogChannels
* @since 3.2.0
*/
public const SECURITY = 'Security';
/**
* For Session parameters
*
* @Since 3.3.0
*/
public const SESSION_PARAMETERS = 'SessionParameters';
}
abstract class LogAPI

View File

@@ -22,7 +22,6 @@ use Combodo\iTop\Application\EventRegister\ApplicationEvents;
use Combodo\iTop\Core\MetaModel\FriendlyNameType;
use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Service\Events\EventService;
use Combodo\iTop\Setup\ModuleDependency\Module;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
require_once APPROOT.'core/modulehandler.class.inc.php';
@@ -1643,10 +1642,10 @@ abstract class MetaModel
if ($sFilterCode == 'id') {
return true;
}
if (array_key_exists($sClass, self::$m_aMagicFields) && array_key_exists($sFilterCode, self::$m_aMagicFields[$sClass])) {
if (array_key_exists($sClass, self::$m_aMagicFields) && in_array($sFilterCode, self::$m_aMagicFields[$sClass])) {
return true;
}
if (array_key_exists($sClass, self::$m_aFilterForbiddenAttributes) && array_key_exists($sFilterCode, self::$m_aFilterForbiddenAttributes[$sClass])) {
if (array_key_exists($sClass, self::$m_aFilterForbiddenAttributes) && in_array($sFilterCode, self::$m_aFilterForbiddenAttributes[$sClass])) {
return false;
}
return self::IsValidAttCode($sClass, $sFilterCode);
@@ -2964,8 +2963,9 @@ abstract class MetaModel
self::$m_aMagicFields[] = $sClass;
}
self::$m_aMagicFields[$sClass][] = $sCode;
self::$m_aAttribOrigins[$sClass][$sCode] = self::$m_aAttribOrigins[$sClass][$sAttCode];
}
if (!$oAttDef->IsSearchable()) {
if (!$oAttDef->IsValidFilterAttribute()) {
if (!array_key_exists($sClass, self::$m_aFilterForbiddenAttributes)) {
self::$m_aFilterForbiddenAttributes[] = $sClass;
}
@@ -4660,12 +4660,15 @@ abstract class MetaModel
* @throws \CoreException
* @throws \MySQLException
*/
public static function DBExists($bMustBeComplete = true)
public static function DBExists($bMustBeComplete = true, ?string $sDBName = null)
{
if (!CMDBSource::IsDB(self::$m_sDBName)) {
if (is_null($sDBName)) {
$sDBName = self::$m_sDBName;
}
if (!CMDBSource::IsDB($sDBName)) {
return false;
}
CMDBSource::SelectDB(self::$m_sDBName);
CMDBSource::SelectDB($sDBName);
$aFound = [];
$aMissing = [];
@@ -5759,36 +5762,37 @@ abstract class MetaModel
self::$m_sEnvironment = $sEnvironment;
try {
if (!defined('MODULESROOT')) {
define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/');
if (!defined('MODULESROOT')) {
define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/');
self::$m_bTraceSourceFiles = $bTraceSourceFiles;
self::$m_bTraceSourceFiles = $bTraceSourceFiles;
// $config can be either a filename, or a Configuration object (volatile!)
if ($config instanceof Config) {
self::LoadConfig($config, $bAllowCache);
} else {
self::LoadConfig(new Config($config), $bAllowCache);
}
if ($bModelOnly) {
return;
}
// $config can be either a filename, or a Configuration object (volatile!)
if ($config instanceof Config) {
self::LoadConfig($config, $bAllowCache);
} else {
self::LoadConfig(new Config($config), $bAllowCache);
}
CMDBSource::SelectDB(self::$m_sDBName);
foreach (MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) {
$oPHPClass::OnMetaModelStarted();
if ($bModelOnly) {
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
EventService::InitService();
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
return;
}
ExpressionCache::Warmup();
} finally {
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
EventService::InitService();
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
}
CMDBSource::SelectDB(self::$m_sDBName);
foreach (MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) {
$oPHPClass::OnMetaModelStarted();
}
ExpressionCache::Warmup();
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
EventService::InitService();
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
}
/**

View File

@@ -60,15 +60,13 @@ class OQLActualClassTreeResolver
$aTranslateFields = [];
foreach ($aExpectedAttributes as $sAttCode => $oExpression) {
// 'id' is managed later
if ($sAttCode == 'id') {
if ($sAttCode === 'id') {
continue;
}
// Attributes can be stored in attributes list or for magic ones into filter codes list.
$sOriginClass = null;
if (MetaModel::IsValidAttCode($sClass, $sAttCode)) {
if (MetaModel::IsValidAttCode($sClass, $sAttCode) || MetaModel::IsValidFilterCode($sClass, $sAttCode)) {
$sOriginClass = MetaModel::GetAttributeOrigin($sClass, $sAttCode);
} elseif ($sAttCode == 'id') {
$sOriginClass = $sClass;
} else {
continue;
}

View File

@@ -340,13 +340,14 @@ class ormDocument
* @param string $sContentDisposition Either 'inline' or 'attachment'
* @param string $sSecretField The attcode of the field containing a "secret" to be provided in order to retrieve the file
* @param string $sSecretValue The value of the secret to be compared with the value of the attribute $sSecretField
* @param bool $bAllowAllData If true, no rights filtering is applied
*
* @return void
*/
public static function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null)
public static function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null, $bAllowAllData = false)
{
try {
$oObj = MetaModel::GetObject($sClass, $id, false, false);
$oObj = MetaModel::GetObject($sClass, $id, false, $bAllowAllData);
if (!is_object($oObj)) {
// If access to the document is not granted, check if the access to the host object is allowed
$oObj = MetaModel::GetObject($sClass, $id, false, true);
@@ -407,7 +408,7 @@ class ormDocument
* @param int $iMaxImageHeight Maximum height for the resized image
* @param array|null $aFinalDimensions Image dimensions after resizing or null if unable to read the image
* @return ormDocument The resampled image
*
* @since 3.3.0 N°3124
*/
public function ResizeImageToFit(int $iMaxWidth, int $iMaxHeight, array|null &$aFinalDimensions = null): static
{

View File

@@ -52,7 +52,7 @@ class ormPassword
public function SetPassword($sClearTextPassword)
{
$iHashAlgo = MetaModel::GetConfig()->GetPasswordHashAlgo();
$this->m_sHashed = password_hash($sClearTextPassword, $iHashAlgo);
$this->m_sHashed = password_hash($sClearTextPassword ?? '', $iHashAlgo);
}
/**
@@ -99,10 +99,10 @@ class ormPassword
$aInfo = password_get_info($this->m_sHashed);
if (is_null($aInfo["algo"]) || $aInfo["algo"] === 0) {
// - Unknown algorithm, assume it's a legacy password
$sHashedPwd = $this->ComputeHash($sClearTextPassword);
$sHashedPwd = $this->ComputeHash($sClearTextPassword ?? '');
$bResult = hash_equals($this->m_sHashed, $sHashedPwd);
} else {
$bResult = password_verify($sClearTextPassword, $this->m_sHashed);
$bResult = password_verify($sClearTextPassword ?? '', $this->m_sHashed);
}
return $bResult;
}

View File

@@ -248,6 +248,45 @@ class RestResultWithObjects extends RestResult
}
}
/**
* @package RESTAPI
* @api
*/
class RestResultWithObjectSets extends RestResultWithObjects
{
private $current_object = null;
public function MakeNewObjectSet()
{
$arr = [];
$this->current_object = &$arr;
$this->objects[] = &$arr;
}
/**
* Report the given object
*
* @api
* @param string $sObjectAlias Name of the subobject, usually the OQL class alias
* @param int $iCode An error code (RestResult::OK is no issue has been found)
* @param string $sMessage Description of the error if any, an empty string otherwise
* @param DBObject $oObject The object being reported
* @param array|null $aFieldSpec An array of class => attribute codes (Cf. RestUtils::GetFieldList). List of the attributes to be reported.
* @param boolean $bExtendedOutput Output all of the link set attributes ?
*
* @return void
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function AppendSubObject($sObjectAlias, $iCode, $sMessage, $oObject, $aFieldSpec = null, $bExtendedOutput = false)
{
$oObjRes = ObjectResult::FromDBObject($oObject, $aFieldSpec, $bExtendedOutput, $iCode, $sMessage);
$this->current_object[$sObjectAlias] = $oObjRes;
}
}
/**
* @package RESTAPI
* @api
@@ -500,14 +539,21 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
break;
case 'core/get':
$sClass = RestUtils::GetClass($aParams, 'class');
$sClassParam = RestUtils::GetMandatoryParam($aParams, 'class');
$key = RestUtils::GetMandatoryParam($aParams, 'key');
$aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
$bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+');
$sShowFields = RestUtils::GetOptionalParam($aParams, 'output_fields', '*');
$iLimit = (int)RestUtils::GetOptionalParam($aParams, 'limit', 0);
$iPage = (int)RestUtils::GetOptionalParam($aParams, 'page', 1);
$oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key, $iLimit, self::getOffsetFromLimitAndPage($iLimit, $iPage));
// Validate the class(es)
$aClass = explode(',', $sClassParam);
foreach ($aClass as $sClass) {
if (!MetaModel::IsValidClass(trim($sClass))) {
throw new Exception("class '$sClass' is not valid");
}
}
$oObjectSet = RestUtils::GetObjectSetFromKey($sClassParam, $key, $iLimit, self::getOffsetFromLimitAndPage($iLimit, $iPage));
$sTargetClass = $oObjectSet->GetFilter()->GetClass();
if (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_READ) != UR_ALLOWED_YES) {
@@ -519,8 +565,56 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
} elseif ($iPage < 1) {
$oResult->code = RestResult::INVALID_PAGE;
$oResult->message = "The request page number is not valid. It must be an integer greater than 0";
} elseif (count($oObjectSet->GetSelectedClasses()) > 1) {
$oResult = new RestResultWithObjectSets();
$aCache = [];
$aShowFields = [];
foreach ($oObjectSet->GetSelectedClasses() as $sSelectedClass) {
$aShowFields = array_merge($aShowFields, RestUtils::GetFieldList($sSelectedClass, $aParams, 'output_fields', false));
}
while ($oObjects = $oObjectSet->FetchAssoc()) {
$oResult->MakeNewObjectSet();
foreach ($oObjects as $sAlias => $oObject) {
if (!$oObject) {
continue;
}
if (!array_key_exists($sAlias, $aCache)) {
$sClass = get_class($oObject);
$bExtendedOutput = RestUtils::HasRequestedExtendedOutput($sShowFields);
if (!RestUtils::HasRequestedAllOutputFields($sShowFields)) {
$aFields = $aShowFields[$sClass];
//Id is not a valid attribute to optimize
if ($aFields && in_array('id', $aFields)) {
unset($aFields[array_search('id', $aFields)]);
}
$aAttToLoad = [$sAlias => $aFields];
$oObjectSet->OptimizeColumnLoad($aAttToLoad);
}
$aCache[$sAlias] = [
'aShowFields' => $aShowFields,
'bExtendedOutput' => $bExtendedOutput,
];
} else {
$aShowFields = $aCache[$sAlias]['aShowFields'];
$bExtendedOutput = $aCache[$sAlias]['bExtendedOutput'];
}
$oResult->AppendSubObject($sAlias, 0, '', $oObject, $aShowFields, $bExtendedOutput);
}
}
$oResult->message = "Found: ".$oObjectSet->Count();
} else {
if (!$bExtendedOutput && RestUtils::GetOptionalParam($aParams, 'output_fields', '*') != '*') {
$aShowFields = [];
foreach ($aClass as $sSelectedClass) {
$sSelectedClass = trim($sSelectedClass);
$aShowFields = array_merge($aShowFields, RestUtils::GetFieldList($sSelectedClass, $aParams, 'output_fields', false));
}
if (!RestUtils::HasRequestedAllOutputFields($sShowFields) && count($aShowFields) == 1) {
$aFields = $aShowFields[$sClass];
//Id is not a valid attribute to optimize
if (in_array('id', $aFields)) {
@@ -531,7 +625,7 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
}
while ($oObject = $oObjectSet->Fetch()) {
$oResult->AddObject(0, '', $oObject, $aShowFields, $bExtendedOutput);
$oResult->AddObject(0, '', $oObject, $aShowFields, RestUtils::HasRequestedExtendedOutput($sShowFields));
}
$oResult->message = "Found: ".$oObjectSet->Count();
}

View File

@@ -8,6 +8,13 @@
use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
use Combodo\iTop\Application\WebPage\WebPage;
use Combodo\iTop\Core\AttributeDefinition\AttributeExternalField;
use Combodo\iTop\Core\AttributeDefinition\AttributeExternalKey;
use Combodo\iTop\Core\AttributeDefinition\AttributeFriendlyName;
use Combodo\iTop\Core\AttributeDefinition\AttributeHierarchicalKey;
use Combodo\iTop\Core\AttributeDefinition\AttributeLinkedSet;
use Combodo\iTop\Core\AttributeDefinition\AttributeStopWatch;
use Combodo\iTop\Core\AttributeDefinition\AttributeSubItem;
/**
* Bulk export: Tabular export: abstract base class for all "tabular" exports.
@@ -91,8 +98,8 @@ abstract class TabularBulkExport extends BulkExport
{
$aResult = [];
switch (get_class($oAttDef)) {
case 'AttributeExternalKey':
case 'AttributeHierarchicalKey':
case AttributeExternalKey::class:
case AttributeHierarchicalKey::class:
$bAddFriendlyName = true;
$oKeyAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
@@ -142,7 +149,7 @@ abstract class TabularBulkExport extends BulkExport
}
break;
case 'AttributeStopWatch':
case AttributeStopWatch::class:
foreach (MetaModel::ListAttributeDefs($sClass) as $sSubAttCode => $oSubAttDef) {
if ($oSubAttDef instanceof AttributeSubItem) {
if ($oSubAttDef->GetParentAttCode() == $sAttCode) {

View File

@@ -658,6 +658,16 @@ abstract class User extends cmdbAbstractObject
}
return false;
}
/**
* Allow a user to be impersonated by another one (generally an administrator) in order to troubleshoot rights issues.
*
* @return bool
*/
public function CanBeImpersonated(): bool
{
return true;
}
}
/**
@@ -1063,6 +1073,9 @@ class UserRights
$oUser = self::FindUser($sLogin);
if ($oUser) {
$bRet = true;
if (!$oUser->CanBeImpersonated()) {
throw new Exception($oUser->GetName().' cannot be impersonated');
}
if (is_null(self::$m_oRealUser)) {
// First impersonation
self::$m_oRealUser = self::$m_oUser;
@@ -1517,6 +1530,37 @@ class UserRights
}
}
/**
* @param User $oUser
* @param array $aExcludedProfilesId Administrator by default, but can also be other proofiles depending on needs (e.g. power portal user or REST profile)
* @return bool
* @throws ArchivedObjectException
* @throws CoreException
* @throws CoreUnexpectedValue
* @throws MySQLException
*/
public static function IsUserReadOnly(User $oUser, array $aExcludedProfilesId = [1]): bool
{
$oUserProfiles = $oUser->Get('profile_list');
$oUserRights = UserRights::GetModuleInstance();
while ($oUserProfile = $oUserProfiles->Fetch()) {
$iProfileId = $oUserProfile->Get('profileid');
if (in_array($iProfileId, $aExcludedProfilesId)) {
return false;
}
foreach (MetaModel::GetClasses('bizmodel,grant_by_profile') as $sClass) {
foreach (['w', 'bw', 'd', 'bd'] as $sWriteActionCode) {
$bIsGranted = $oUserRights->GetProfileActionGrant($iProfileId, $sClass, $sWriteActionCode);
if ($bIsGranted === true) {
return false;
}
}
}
}
return true;
}
/**
* @param string $sClass
* @param int $iActionCode see UR_ACTION_* constants

View File

@@ -4,7 +4,7 @@
*/
$ibo-user-rights--padding-x: $ibo-spacing-400 !default;
$ibo-user-rights--padding-y: $ibo-spacing-200 !default;
$ibo-user-rights--padding-y: $ibo-spacing-100 !default;
$ibo-user-rights--border-radius: $ibo-border-radius-400 !default;
$ibo-user-rights--is-success--background-color: $ibo-color-success-100 !default;
@@ -18,6 +18,8 @@ $ibo-user-rights--is-failure--border-color: $ibo-color-danger-500 !default;
$ibo-user-rights--is-failure--border: 1px solid $ibo-user-rights--is-failure--border-color !default;
.ibo-user-rights {
display: inline-flex;
align-items: baseline;
padding: $ibo-user-rights--padding-y $ibo-user-rights--padding-x;
border-radius: $ibo-user-rights--border-radius;
&.ibo-is-success {

View File

@@ -19,6 +19,8 @@ $ibo-badge-colors: (
'orange' : ($ibo-color-orange-100, $ibo-color-orange-900),
'red': ($ibo-color-red-100, $ibo-color-red-900),
'pink': ($ibo-color-pink-100, $ibo-color-pink-900),
'purple': ($ibo-color-purple-100, $ibo-color-purple-900),
'yellow': ($ibo-color-yellow-200, $ibo-color-yellow-900),
) !default;

View File

@@ -8,70 +8,74 @@
padding-bottom: 14px;
}
.help-text{
padding: 1px 5px;
background-color: #d7e3f8;
border: 1px solid #c6e7f5;
border-radius: 5px;
margin: 5px 0;
font-size: 0.9em;
}
.form-error ul{
padding: 1px 5px;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 5px;
margin: 5px 0;
font-size: 0.9em;
}
.subform{
background-color: #efefef;
border-radius: 5px;
padding: 10px;
}
.form-buttons{
margin: 20px 0;
}
.form select{
padding: 0;
overflow-y: auto;
}
.form select option{
height: 30px;
display: flex;
align-items: center;
}
.turbo-refreshing{
opacity: .5;
}
.ibo-field legend{
margin-top: 24px;
}
collection-entry-element {
margin-top: 8px;
display: block;
padding: 10px 10px;
background-color: #f5f5f5;
border-radius: 5px;
}
.ts-control{
height: auto;
min-height: 30px;
}
.ibo-form-actions > .ibo-button > span{
margin-right: 5px;
}
.ibo-form textarea{
resize: vertical;
}
// WIP SDK Forms
//form[is="itop-form-element"] {
//
// .help-text{
// padding: 1px 5px;
// background-color: #d7e3f8;
// border: 1px solid #c6e7f5;
// border-radius: 5px;
// margin: 5px 0;
// font-size: 0.9em;
// }
//
// .form-error ul{
// padding: 1px 5px;
// background-color: #f8d7da;
// border: 1px solid #f5c6cb;
// border-radius: 5px;
// margin: 5px 0;
// font-size: 0.9em;
// }
//
// .subform{
// background-color: #efefef;
// border-radius: 5px;
// padding: 10px;
// }
//
// .form-buttons{
// margin: 20px 0;
// }
//
// .form select{
// padding: 0;
// overflow-y: auto;
// }
//
// .form select option{
// height: 30px;
// display: flex;
// align-items: center;
// }
//
// .turbo-refreshing{
// opacity: .5;
// }
//
// .ibo-field legend{
// margin-top: 24px;
// }
//
// collection-entry-element {
// margin-top: 8px;
// display: block;
// padding: 10px 10px;
// background-color: #f5f5f5;
// border-radius: 5px;
// }
// .ts-control{
// height: auto;
// min-height: 30px;
// }
//
// .ibo-form-actions > .ibo-button > span{
// margin-right: 5px;
// }
//
// .ibo-form textarea{
// resize: vertical;
// }
//
//}

View File

@@ -185,16 +185,20 @@ $ibo-panel--is-selectable--body--after--font-size: $ibo-font-size-700 !default;
.ibo-panel--icon-img, .ibo-panel--icon-background { // second class is deprecated, remove it when dealing with N°9317
width: 100%;
height: 100%;
object-position: center;
object-fit: $ibo-panel--icon-img--size--must-contain;
background-position: center;
background-repeat: no-repeat;
background-size: $ibo-panel--icon-img--size--must-contain;
}
.ibo-panel--icon-img--must-contain, .ibo-panel--icon-background--must-contain { // second class is deprecated, remove it when dealing with N°9317
object-fit: $ibo-panel--icon-img--size--must-contain;
background-size: $ibo-panel--icon-img--size--must-contain;
}
.ibo-panel--icon-img--must-cover, .ibo-panel--icon-background--must-cover { // second class is deprecated, remove it when dealing with N°9317
object-fit: $ibo-panel--icon-img--size--must-cover;
background-size: $ibo-panel--icon-img--size--must-cover;
}

View File

@@ -247,7 +247,8 @@ $ibo-quick-create--compartment--placeholder-hint--text-color: $ibo-color-grey-70
&:hover {
cursor: pointer;
@extend a;
color: $ibo-hyperlink-color--on-hover;
text-decoration: $ibo-hyperlink-text-decoration--on-hover;
}
.highlight {

View File

@@ -51,19 +51,23 @@ $ibo-title--icon-img--size--must-zoomout: $ibo-title--icon-background--size--mus
width: 100%;
height: 100%;
object-position: center;
object-fit: $ibo-title--icon-img--size--must-contain;
background-size: $ibo-title--icon-img--size--must-contain;
}
.ibo-title--icon-img--must-contain, .ibo-title--icon-background--must-contain { // second class is deprecated, remove it when dealing with N°9317
object-fit: $ibo-title--icon-img--size--must-contain;
background-size: $ibo-title--icon-img--size--must-contain;
}
.ibo-title--icon-img--must-cover, .ibo-title--icon-background--must-cover { // second class is deprecated, remove it when dealing with N°9317
object-fit: $ibo-title--icon-img--size--must-cover;
background-size: $ibo-title--icon-img--size--must-cover;
}
.ibo-title--icon-img--must-zoomout, .ibo-title--icon-background--must-zoomout { // second class is deprecated, remove it when dealing with N°9317
background-size: $ibo-title--icon-img--size--must-zoomout;
width: $ibo-title--icon-img--size--must-zoomout;
height: $ibo-title--icon-img--size--must-zoomout;
}
.ibo-title--for-object-details {

View File

@@ -3,9 +3,9 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
$ibo-input-select-icon--menu--icon--max-height: 100% !default;
$ibo-input-select-icon--menu--icon--max-width: 100% !default;
$ibo-input-select-icon--icon--padding-right: $ibo-spacing-200 !default;
$ibo-input-select-icon--max-height: 100% !default;
$ibo-input-select-icon--max-width: 100% !default;
$ibo-input-select-icon--padding-right: $ibo-spacing-200 !default;
$ibo-input-select-icon--menu--z-index: 21 !default;
$ibo-input-select-icon--menu--max-height: 300px !default;
@@ -18,9 +18,9 @@ $ibo-input-select-icon--menu--icon--margin-right: 10px !default;
display: inline-flex;
text-align: left;
>img{
max-height: $ibo-input-select-icon--menu--icon--max-height;
max-width: $ibo-input-select-icon--menu--icon--max-width;
padding-right: $ibo-input-select-icon--icon--padding-right;
max-height: $ibo-input-select-icon--max-height;
max-width: $ibo-input-select-icon--max-width;
padding-right: $ibo-input-select-icon--padding-right;
}
>span{
overflow: hidden;

View File

@@ -62,4 +62,15 @@ $ibo-extension-details--actions--button--padding-x: $ibo-button--padding-x !defa
.ibo-extension-details--actions:has(.toggler-install:not(:disabled)) .ibo-popover-menu--section a[data-resource-id="force_uninstall"] {
display: none;
}
}
.ibo-extension-details .ibo-popover-menu ~ .ibo-button{
visibility: hidden;
}
.ibo-extension-details .ibo-popover-menu:has(.ibo-popover-menu--item) ~ .ibo-button{
visibility: visible;
}
.ibo-extension-details .ibo-toggler--wrapper:has(.ibo-toggler.ibo-is-hidden){
visibility: hidden;
}

View File

@@ -80,4 +80,14 @@ $ibo-data-synchro-source--synoptics--cell--arrow--border: 2px solid $ibo-data-sy
}
.ibo-data-synchro-source--replicas-status--warning{
margin: $ibo-data-synchro-source--replicas-status--warning--margin;
}
.ibo-page-header--replica-title{
display: table;
width: 100%;
>.ibo-toolbar--button{
display: table-cell;
vertical-align: middle;
}
}

View File

@@ -3,9 +3,29 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
$ibo-datamodel-viewer--viewer-empty--width: max(20%, #{$ibo-size-700}) !default;
$ibo-datamodel-viewer--parent--spacer--padding-y: $ibo-spacing-0 !default;
$ibo-datamodel-viewer--parent--spacer--padding-x: $ibo-spacing-300 !default;
$ibo-datamodel-viewer--breadcrumb--margin: $ibo-spacing-200 0 !default;
$ibo-datamodel-viewer--classname--font-family: monospace !default;
$ibo-datamodel-viewer--classname--color: $ibo-color-blue-grey-600 !default;
$ibo-datamodel-viewer--tag--category--color: $ibo-color-blue-grey-800 !default;
$ibo-datamodel-viewer--tag--category--icon--color: $ibo-color-blue-grey-600 !default;
$ibo-datamodel-viewer--abstract-class-icon--margin-right: $ibo-spacing-300 !default;
$ibo-datamodel-viewer--abstract-class-icon--after--content: 'A' !default;
$ibo-datamodel-viewer--abstract-class-icon--after--width: 15px !default;
$ibo-datamodel-viewer--abstract-class-icon--after--height: 15px !default;
$ibo-datamodel-viewer--abstract-class-icon--after--line-height: 14px !default;
$ibo-datamodel-viewer--abstract-class-icon--after--border-radius: 50% !default;
$ibo-datamodel-viewer--abstract-class-icon--after--border: 1px solid $ibo-color-cyan-500 !default;
$ibo-datamodel-viewer--abstract-class-icon--after--color: $ibo-color-white-100 !default;
$ibo-datamodel-viewer--abstract-class-icon--after--background-color: $ibo-color-cyan-500 !default;
$ibo-datamodel-viewer--classes-list--selectize-input--background-color: $ibo-color-white-100 !default;
$ibo-datamodel-viewer--classes-list--selectize-input--color: $ibo-color-grey-800 !default;
$ibo-datamodel-viewer--classes-list--selectize-input--border-color: $ibo-color-grey-500 !default;
@@ -16,8 +36,9 @@ $ibo-datamodel-viewer--origin-cell--diameter: 8px !default;
$ibo-datamodel-viewer--origin-cell--border-radius: $ibo-border-radius-full !default;
$ibo-datamodel-viewer--classes-list--height: 100% !default;
$ibo-datamodel-viewer--classes-list--width: 350px !default;
$ibo-datamodel-viewer--classes-list--width: $ibo-size-700 !default;
$ibo-datamodel-viewer--classes-list--padding-left: $ibo-spacing-600 !default;
$ibo-datamodel-viewer--classes-list--padding-top: $ibo-spacing-300 !default;
$ibo-datamodel-viewer--lifecycle--code--color: $ibo-color-grey-700 !default;
$ibo-datamodel-viewer--lifecycle--stimuli--color: $ibo-color-blue-900 !default;
@@ -45,6 +66,56 @@ $ibo-datamodel-viewer--lifecycle-image--margin-bottom: $ibo-spacing-500 !default
.ibo-panel--subtitle{
@extend %ibo-font-ral-nor-150;
}
.ibo-datamodel-viewer--empty{
display: flex;
flex-direction: column;
align-items: center;
svg{
width: $ibo-datamodel-viewer--viewer-empty--width;
}
}
}
.ibo-datamodel-viewer--empty--text {
@extend %ibo-font-ral-med-400
}
.ibo-datamodel-viewer--breadcrumb{
@extend %ibo-font-ral-med-150;
margin: $ibo-datamodel-viewer--breadcrumb--margin;
.ibo-button{
text-transform: none; // unset uppercase
}
}
.ibo-datamodel-viewer--classname{
color: $ibo-datamodel-viewer--classname--color;
@extend %ibo-font-code-100;
}
.ibo-datamodel-viewer--tag--category{
color: $ibo-datamodel-viewer--tag--category--color;
.ibo-object-details--tag-icon{
color:$ibo-datamodel-viewer--tag--category--icon--color;
}
}
.ibo-datamodel-viewer--icon--abstract{
&:after{
content: $ibo-datamodel-viewer--abstract-class-icon--after--content;
display: inline-flex;
justify-content: center;
align-items: center;
width: $ibo-datamodel-viewer--abstract-class-icon--after--width;
height: $ibo-datamodel-viewer--abstract-class-icon--after--height;
line-height: $ibo-datamodel-viewer--abstract-class-icon--after--line-height;
border-radius: $ibo-datamodel-viewer--abstract-class-icon--after--border-radius;
border: $ibo-datamodel-viewer--abstract-class-icon--after--border;
color: $ibo-datamodel-viewer--abstract-class-icon--after--color;
background-color: $ibo-datamodel-viewer--abstract-class-icon--after--background-color;
@extend %ibo-font-ral-bol-50;
}
}
.ibo-datamodel-viewer--parent--spacer{
@@ -80,6 +151,7 @@ $ibo-datamodel-viewer--lifecycle-image--margin-bottom: $ibo-spacing-500 !default
height: $ibo-datamodel-viewer--classes-list--height;
width: $ibo-datamodel-viewer--classes-list--width;
padding-left: $ibo-datamodel-viewer--classes-list--padding-left;
padding-top: $ibo-datamodel-viewer--classes-list--padding-top;
overflow-y: scroll;
}

View File

@@ -28,6 +28,15 @@ $ibo-vendors-ckeditor--ck-mentions--item--padding-x: $ibo-spacing-300 !default;
$ibo-vendors-ckeditor--ck-mentions--item--padding-y: $ibo-spacing-200 !default;
/* CSS3 variables */
:root {
--ck-content-font-family: inherit;
--ck-content-font-color: inherit;
--ck-content-font-size: inherit;
--ck-content-line-height: inherit;
--ck-content-word-break: inherit;
--ck-content-overflow-wrap: inherit;
}
.ck {
--ck-color-list-button-on-background: #{$ibo-vendors-ckeditor--ck-color-list-button-on-background};
--ck-color-list-button-on-background-focus: #{$ibo-vendors-ckeditor--ck-color-list-button-on-background-focus};
@@ -61,7 +70,7 @@ $ibo-vendors-ckeditor--ck-mentions--item--padding-y: $ibo-spacing-200 !default;
}
}
.ck-editor__editable_inline:not(.ck-comment__input *) {
.ck-editor__editable_inline:not(.ck-comment__input *), .ck-source-editing-area {
height: 200px;
}
@@ -75,11 +84,6 @@ $ibo-vendors-ckeditor--ck-mentions--item--padding-y: $ibo-spacing-200 !default;
}
}
// N°7552 Allow source editing area to be scrollable in full screen
.ck-maximize_editor_main .ck-source-editing-area textarea{
overflow: auto !important;
}
/* Mentions */
.ck-mentions {
.ck-button {

View File

@@ -4,7 +4,7 @@
*/
/* SCSS variables */
$common-has-description--content: "\1F6C8" !default;
$common-has-description--content: "?" !default;
$common-has-description--padding-left: $common-spacing-200 !default;
$common-has-description--color: $common-color-grey-600 !default;
$common-has-description--font-size: 0.7em !default; /* Font size is em on purpose as we want it to be proportional to its context */

View File

@@ -11,6 +11,9 @@
*
* Usage: @extend %common-font-size-XXX;
*/
%common-font-size-20 {
font-size: $common-font-size-20;
}
%common-font-size-50 {
font-size: $common-font-size-50;
}

File diff suppressed because one or more lines are too long

View File

@@ -611,32 +611,6 @@ body {
.setup-extension--missing .setup-extension--icon{
color:#a00000;
}
.setup-extension-tag {
display: inline-flex;
background-color: grey;
border-radius: 8px;
padding-left: 3px;
padding-right: 3px;
margin-right: 3px;
&.installed{
background-color:#9eff9e
}
&.notinstalled{
background-color:#ed9eff
}
&.tobeinstalled{
background-color:#9ef0ff
}
&.tobeuninstalled{
background-color:#ff9e9e
}
&.notuninstallable{
background-color:#ffc98c
}
&.removed{
background-color: #969594
}
}
.ibo-extension-details {
align-items: flex-start;
@@ -699,11 +673,10 @@ body {
}
}
#progress_content {
height: 200px;
overflow: auto;
text-align: center;
#progress_content *:not(.message) + .message {
margin-top: 1.5rem;
}
#fresh_content{
border: 0;
min-height: 300px;

View File

@@ -6,8 +6,9 @@
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*/
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'CAS:Error:UserNotAllowed' => '用户被禁止登录',
'CAS:Login:SignIn' => '使用CAS登录',
'CAS:Login:SignInTooltip' => '点击这里使用CAS服务器认证',
'CAS:Login:SignIn' => '使用 CAS 登录',
'CAS:Login:SignInTooltip' => '点击这里使用 CAS 服务器认证',
]);

View File

@@ -5,10 +5,8 @@
*
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
* @author Robert Deng <denglx@gmail.com>
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -23,6 +21,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with iTop. If not, see <http://www.gnu.org/licenses/>
*/
// Dictionnay conventions
// Class:<class_name>
// Class:<class_name>+
@@ -32,9 +31,11 @@
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>+
// Class:<class_name>/Stimulus:<stimulus_code>
// Class:<class_name>/Stimulus:<stimulus_code>+
//
// Class: UserExternal
//
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:UserExternal' => '外部用户',
'Class:UserExternal+' => '用户在'.ITOP_APPLICATION_SHORT.'外部验证身份',

View File

@@ -97,18 +97,19 @@
$bDebug = isset($aServerParams['debug']) ? $aServerParams['debug'] : false;
}
if (array_key_exists(LDAP_OPT_DEBUG_LEVEL, $aOptions))
{
// Set debug level before trying to connect, so that debug info appear in the PHP error log if ldap_connect goes wrong
$bRet = ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, $aOptions[LDAP_OPT_DEBUG_LEVEL]);
$this->LogInfo($bDebug, "ldap_set_option('LDAP_OPT_DEBUG_LEVEL', '{$aOptions[LDAP_OPT_DEBUG_LEVEL]}') returned ".($bRet ? 'true' : 'false'));
}
$hDS = @ldap_connect($sURI);
if ($hDS === false)
{
$this->LogIssue($bDebug, "ldap_authentication: can not connect to the LDAP server '$sURI'. Check the configuration file config-itop.php.");
return false;
}
if (array_key_exists(LDAP_OPT_DEBUG_LEVEL, $aOptions))
{
// Set debug level before trying to connect, so that debug info appear in the PHP error log if ldap_connect goes wrong
$bRet = ldap_set_option($hDS, LDAP_OPT_DEBUG_LEVEL, $aOptions[LDAP_OPT_DEBUG_LEVEL]);
$this->LogInfo($bDebug, "ldap_set_option('LDAP_OPT_DEBUG_LEVEL', '{$aOptions[LDAP_OPT_DEBUG_LEVEL]}') returned ".($bRet ? 'true' : 'false'));
}
foreach($aOptions as $name => $value)
{
$bRet = ldap_set_option($hDS, $name, $value);

View File

@@ -14,7 +14,7 @@
Dict::Add('RU RU', 'Russian', 'Русский', [
'Class:UserLDAP' => 'Пользователь LDAP',
'Class:UserLDAP+' => 'Пользователь, аутентифицируемый через LDAP',
'UserLDAP:server' => 'LDAP specifics~~',
'UserLDAP:server' => 'Особенности LDAP',
]);
//
@@ -22,6 +22,6 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
//
Dict::Add('RU RU', 'Russian', 'Русский', [
'Class:UserLDAP/Attribute:ldap_server' => 'Ldap server~~',
'Class:UserLDAP/Attribute:ldap_server+' => '~~',
'Class:UserLDAP/Attribute:ldap_server' => 'Сервер LDAP',
'Class:UserLDAP/Attribute:ldap_server+' => '',
]);

View File

@@ -6,7 +6,6 @@
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
* @author Robert Deng <denglx@gmail.com>
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
@@ -22,6 +21,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with iTop. If not, see <http://www.gnu.org/licenses/>
*/
// Dictionnay conventions
// Class:<class_name>
// Class:<class_name>+
@@ -31,13 +31,15 @@
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>+
// Class:<class_name>/Stimulus:<stimulus_code>
// Class:<class_name>/Stimulus:<stimulus_code>+
//
// Class: UserLDAP
//
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:UserLDAP' => 'LDAP用户',
'Class:UserLDAP+' => '用户身份由LDAP认证',
'UserLDAP:server' => 'LDAP详情',
'Class:UserLDAP' => 'LDAP 用户',
'Class:UserLDAP+' => '用户身份由 LDAP 认证',
'UserLDAP:server' => 'LDAP 详情',
]);
//
@@ -45,6 +47,6 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
//
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:UserLDAP/Attribute:ldap_server' => 'Ldap server~~',
'Class:UserLDAP/Attribute:ldap_server+' => '~~',
'Class:UserLDAP/Attribute:ldap_server' => 'LDAP 服务器',
'Class:UserLDAP/Attribute:ldap_server+' => '',
]);

View File

@@ -24,11 +24,11 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'Class:UserLocal/Attribute:expiration/Value:never_expire+' => '',
'Class:UserLocal/Attribute:expiration/Value:force_expire' => 'Истёкший',
'Class:UserLocal/Attribute:expiration/Value:force_expire+' => '',
'Class:UserLocal/Attribute:expiration/Value:otp_expire' => 'One-time Password~~',
'Class:UserLocal/Attribute:expiration/Value:otp_expire+' => 'Password cannot be changed by the user.~~',
'Class:UserLocal/Attribute:expiration/Value:otp_expire' => 'Одноразовый пароль',
'Class:UserLocal/Attribute:expiration/Value:otp_expire+' => 'Пароль не может быть изменён пользователем.',
'Class:UserLocal/Attribute:password_renewed_date' => 'Дата изменения пароля',
'Class:UserLocal/Attribute:password_renewed_date+' => 'Когда пароль был изменен в последний раз',
'Error:UserLocalPasswordValidator:UserPasswordPolicyRegex:ValidationFailed' => 'Пароль должен содержать не менее 12 символов и включать прописные, строчные, числовые и специальные символы.',
'UserLocal:password:expiration' => 'Поля требуют наличия доп. расширения',
'Class:UserLocal/Error:OneTimePasswordChangeIsNotAllowed' => 'Setting password expiration to "One-time password" is not allowed for your own User~~',
'Class:UserLocal/Error:OneTimePasswordChangeIsNotAllowed' => 'Установка срока действия пароля "Одноразовый пароль" для своей собственной учётной записи не разрешена',
]);

View File

@@ -6,7 +6,6 @@
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
* @author Robert Deng <denglx@gmail.com>
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
@@ -22,6 +21,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with iTop. If not, see <http://www.gnu.org/licenses/>
*/
// Dictionnay conventions
// Class:<class_name>
// Class:<class_name>+
@@ -31,16 +31,19 @@
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>+
// Class:<class_name>/Stimulus:<stimulus_code>
// Class:<class_name>/Stimulus:<stimulus_code>+
//
// Class: UserLocal
//
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:UserLocal' => ITOP_APPLICATION_SHORT.' 用户',
'Class:UserLocal+' => '用户由'.ITOP_APPLICATION_SHORT.'验证身份',
'Class:UserLocal/Attribute:password' => '密码',
'Class:UserLocal/Attribute:password+' => '用于验证用户身份的字符串',
'Class:UserLocal/Attribute:expiration' => '密码过期',
'Class:UserLocal/Attribute:expiration+' => '密码过期状态 (需要一个扩展才能生效)',
'Class:UserLocal/Attribute:expiration' => '密码过期时间',
'Class:UserLocal/Attribute:expiration+' => '密码过期状态 (需要扩展才能生效)',
'Class:UserLocal/Attribute:expiration/Value:can_expire' => '允许过期',
'Class:UserLocal/Attribute:expiration/Value:can_expire+' => '',
'Class:UserLocal/Attribute:expiration/Value:never_expire' => '永不过期',
@@ -49,8 +52,9 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:UserLocal/Attribute:expiration/Value:force_expire+' => '',
'Class:UserLocal/Attribute:expiration/Value:otp_expire' => '一次性密码',
'Class:UserLocal/Attribute:expiration/Value:otp_expire+' => '用户不允许修改密码.',
'Class:UserLocal/Attribute:password_renewed_date' => '密码更新',
'Class:UserLocal/Attribute:password_renewed_date' => '密码更新时间',
'Class:UserLocal/Attribute:password_renewed_date+' => '上次修改密码的时间',
'Error:UserLocalPasswordValidator:UserPasswordPolicyRegex:ValidationFailed' => '密码必须至少12个字符, 包含大小写, 数字和特殊字符.',
'UserLocal:password:expiration' => '下面的区域需要插件扩展',
'Class:UserLocal/Error:OneTimePasswordChangeIsNotAllowed' => '不允许用户为自己设置 "一次性密码" 的失效期限',

View File

@@ -11,5 +11,5 @@
*
*/
Dict::Add('RU RU', 'Russian', 'Русский', [
'theme:darkmoon' => 'Dark moon~~',
'theme:darkmoon' => 'Тёмная луна',
]);

View File

@@ -21,6 +21,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with iTop. If not, see <http://www.gnu.org/licenses/>
*/
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'theme:darkmoon' => '暗月',
'theme:darkmoon' => 'Dark moon',
]);

View File

@@ -23,5 +23,5 @@
*/
Dict::Add('RU RU', 'Russian', 'Русский', [
'theme:fullmoon-high-contrast' => 'Fullmoon (High contrast)~~',
'theme:fullmoon-high-contrast' => 'Fullmoon (высокая контрастность)',
]);

View File

@@ -23,5 +23,5 @@
*/
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'theme:fullmoon-high-contrast' => 'Fullmoon (High contrast)~~',
'theme:fullmoon-high-contrast' => 'Fullmoon (高对比度)',
]);

View File

@@ -23,5 +23,5 @@
*/
Dict::Add('RU RU', 'Russian', 'Русский', [
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (Protanopia & Deuteranopia)~~',
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (протанопия и дейтеранопия)',
]);

View File

@@ -23,5 +23,5 @@
*/
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (Protanopia & Deuteranopia)~~',
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (红绿色盲 & 绿色色盲)',
]);

View File

@@ -23,5 +23,5 @@
*/
Dict::Add('RU RU', 'Russian', 'Русский', [
'theme:fullmoon-tritanopia' => 'Fullmoon (Tritanopia)~~',
'theme:fullmoon-tritanopia' => 'Fullmoon (тританопия)',
]);

View File

@@ -23,5 +23,5 @@
*/
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'theme:fullmoon-tritanopia' => 'Fullmoon (Tritanopia)~~',
'theme:fullmoon-tritanopia' => 'Fullmoon (蓝黄色盲)',
]);

View File

@@ -4,8 +4,7 @@
},
"autoload": {
"psr-4": {
"Combodo\\iTop\\DataFeatureRemoval\\": "src",
"": "src/NoNamespace"
"Combodo\\iTop\\DataFeatureRemoval\\": "src"
}
},
"name": "combodo/combodo-data-feature-removal",

View File

@@ -1,127 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.3">
<classes>
<class id="DataFeatureRemovalBackgroundOperation" _delta="define">
<class id="ResourceDataFeatureRemoval" _delta="define">
<parent>AbstractResource</parent>
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>datafeatureremovalbackgroundoperation</db_table>
<style>
<icon>
<fileref ref="icons8-electricity_643cf7fd7a024968679dc0c35a710a03"/>
</icon>
<main_color>#0000ff</main_color>
</style>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
<uniqueness_rules>
<rule id="list_of_classes">
<attributes>
<attribute id="classes"/>
</attributes>
<filter><![CDATA[]]></filter>
<disabled>false</disabled>
<is_blocking>true</is_blocking>
</rule>
</uniqueness_rules>
<comment>/* Extension management access control. */</comment>
<abstract>true</abstract>
<category>grant_by_profile</category>
</properties>
<fields>
<field id="creation_date" xsi:type="AttributeDateTime">
<sql>creation_date</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="features_code" xsi:type="AttributeText">
<sql>features_code</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
<width/>
<height/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="classes" xsi:type="AttributeText">
<sql>classes</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
<width/>
<height/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
</fields>
<presentation/>
<methods/>
<presentation>
<list>
<items>
<item id="creation_date">
<rank>10</rank>
</item>
<item id="features_code">
<rank>20</rank>
</item>
<item id="classes">
<rank>30</rank>
</item>
</items>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="creation_date">
<rank>10</rank>
</item>
<item id="features_code">
<rank>20</rank>
</item>
<item id="classes">
<rank>30</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
</classes>
<dictionaries>
<dictionary id="EN US">
<entries>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation" _delta="define"><![CDATA[DataFeatureRemovalBackgroundOperation]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:creation_date" _delta="define"><![CDATA[Creation date]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:creation_date+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:features_code" _delta="define"><![CDATA[Features code]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:features_code+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:classes" _delta="define"><![CDATA[Classes]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:classes+" _delta="define"><![CDATA[]]></entry>
</entries>
</dictionary>
</dictionaries>
<files>
<file id="icons8-electricity_643cf7fd7a024968679dc0c35a710a03" xsi:type="File" _delta="define_if_not_exists">
<name>images/icons/icons8-electricity.svg</name>
<mime_type>image/svg+xml</mime_type>
<data>PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSIyNDBweCIgaGVpZ2h0PSIyNDBweCI+PHBhdGggZmlsbD0iIzM3YzZkMCIgZD0iTTI2LjQ1MTIsMzdsNy43NzY0Ni0xNS41NTI5MWExLDEsMCwwLDAtLjg5NDM1LTEuNDQ3MjJMMjIuOTUxMiwxOS45OTkyNWw2LjMzNi0xMy41NzYzNUExLDEsMCwwLDAsMjguMzgxLDVIMjIuNTk2MzlhMSwxLDAsMCwwLS45MTExMS41ODc4M2wtOC41OTUyNCwxOUExLDEsMCwwLDAsMTQuMDAxMTUsMjZoMTAuNDVsLTUuNSwxMVoiLz48cGF0aCBmaWxsPSIjMzdjNmQwIiBkPSJNMTYuMjIyODYsMzdIMjkuODY0NzFhLjU1NzM1LjU1NzM1LDAsMCwxLC4zNTgxNSwxTDE5Ljk2LDQ0LjM2N2ExLjAzMzY0LDEuMDMzNjQsMCwwLDEtMS40OTEyMS0uNDI1ODZMMTUuNzIyODYsMzcuNzVBLjUwNDUuNTA0NSwwLDAsMSwxNi4yMjI4NiwzN1oiLz48L3N2Zz4=</data>
</file>
</files>
<menus>
<menu id="DataFeatureRemovalMenu" xsi:type="WebPageMenuNode" _delta="define">
<rank>30</rank>
<parent>SystemTools</parent>
<url>$pages/exec.php?exec_module=combodo-data-feature-removal&amp;exec_page=index.php&amp;c[menu]=DataFeatureRemovalMenu</url>
<enable_admin_only>1</enable_admin_only>
<enable_class>ResourceDataFeatureRemoval</enable_class>
<enable_action>UR_ACTION_MODIFY</enable_action>
</menu>
</menus>
<module_parameters>

View File

@@ -10,40 +10,51 @@
*/
Dict::Add('EN US', 'English', 'English', [
'Menu:DataFeatureRemovalMenu' => 'Features Removal',
'combodo-data-feature-removal/Operation:Main/Title' => 'Features Removal',
'Menu:DataFeatureRemovalMenu' => 'Extension management',
'combodo-data-feature-removal/Operation:Main/Title' => 'Extension management',
'DataFeatureRemoval:Main:Title' => 'Features Removal',
'DataFeatureRemoval:Main:SubTitle' => 'Prepare features you want to enable/disable in a future setup',
'DataFeatureRemoval:Failure:Title' => 'Feature dry removal errors',
'DataFeatureRemoval:Helper:Title' => 'Enable or disable features that are installed in your iTop.',
'DataFeatureRemoval:Helper:Desc1' => 'It will prepare the setup step that proceeds to feature enabling or disabling.',
'DataFeatureRemoval:Helper:Desc2' => 'Analyze if there are any data or dependency preventing you from enabling/disabling a feature.',
'DataFeatureRemoval:Main:Title' => 'Extension management',
'DataFeatureRemoval:Main:SubTitle' => 'Toggle extensions installed on your iTop',
'DataFeatureRemoval:Failure:Title' => 'Extensions dry removal errors',
'DataFeatureRemoval:Helper:Title' => 'Analyze if there are any data or dependency preventing you from adding/removing an extension.',
'DataFeatureRemoval:Features:Title' => 'Features',
'DataFeatureRemoval:Features:Title' => 'Extensions',
'DataFeatureRemoval:Result:Title' => 'Modification requested',
'DataFeatureRemoval:NoResult:Title' => 'No modification requested',
'DataFeatureRemoval:Execution:Title' => 'Deletion Executions',
'DataFeatureRemoval:Analysis:Title' => 'Analysis result',
'DataFeatureRemoval:Analysis:Subtitle' => 'Review all elements requiring attention',
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s element(s) to clean before continuing',
'DataFeatureRemoval:Analysis:Ok' => "No data to cleanup",
'DataFeatureRemoval:DeletionPlan:Title' => 'Deletion plan',
'DataFeatureRemoval:DeletionPlan:Title' => 'Data deletion plan',
'DataFeatureRemoval:DeletionPlan:SubTitle' => '%1$s rows to clean before continuing',
'DataFeatureRemoval:DoDeletion:Title' => 'Do deletion',
'DataFeatureRemoval:DoDeletion:SubTitle' => 'Remove all the entries from the database',
'DataFeatureRemoval:DeletionPlan:ToManyOperations' => 'Too many entries to clean',
'DataFeatureRemoval:DeletionPlan:Error:Issues' => 'Some objects must be deleted manually prior to cleanup',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Element to remove',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Feature name',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Extension name',
'DataFeatureRemoval:Table:Analysis:Module' => 'Module name',
'DataFeatureRemoval:Table:Analysis:Occurrence' => 'Occurrence',
'DataFeatureRemoval:CleanupComplete:Title' => 'All clear.',
'DataFeatureRemoval:CompilComplete' => 'Compilation successful. No Cleanup needed. You can proceed to setup.',
'DataFeatureRemoval:Compile:InProgress' => 'Compilation in progress...',
'DataFeatureRemoval:Compile:Success' => 'Compilation successful',
'DataFeatureRemoval:Compile:Error' => 'Compilation error',
'DataFeatureRemoval:RunAudit:InProgress' => 'Analysis in progress...',
'DataFeatureRemoval:RunAudit:Success' => 'Analysis completed',
'DataFeatureRemoval:RunAudit:Error' => 'Error during analysis',
'UI:Button:Analyze' => 'Analyze',
'UI:Button:ModifyChoices' => 'Modify Choices',
'UI:Button:ModifyChoices' => 'Change my selection',
'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup',
'UI:Button:PlanDeletion' => 'Prepare deletion plan',
'UI:Button:DoDeletion' => 'Delete data',
'UI:Button:DoAsyncDeletion' => 'Do asynchronous deletion',
'UI:Button:BackToMain' => 'Back to Feature Removal',
'UI:Button:Setup' => 'Back to setup',
'UI:Button:PlanDeletion' => 'Proceed with deletion',
'UI:Button:DoDeletion' => 'Proceed with deletion',
'UI:Button:BackToMain' => 'Change my selection',
'UI:Button:Setup' => 'Run setup',
'UI:Action:ForceUninstall' => 'Force uninstall',
'UI:Action:MoreInfo' => 'More information',
@@ -53,7 +64,7 @@ Dict::Add('EN US', 'English', 'English', [
'DataFeatureRemoval:Column:Class' => 'Class',
'DataFeatureRemoval:Column:DeleteCount' => 'Entries to delete',
'DataFeatureRemoval:Column:UpdateCount' => 'Entries to update',
'DataFeatureRemoval:Column:Issue' => 'Issue',
'DataFeatureRemoval:Column:IssueCount' => 'Issues found preventing automatic cleanup',
'DataFeatureRemoval:Column:DeletedCount' => 'Deleted entries',
'DataFeatureRemoval:Column:UpdatedCount' => 'Updated entries',

View File

@@ -10,40 +10,51 @@
*/
Dict::Add('FR FR', 'French', 'Français', [
'Menu:DataFeatureRemovalMenu' => 'Suppression de fonctionnalités',
'combodo-data-feature-removal/Operation:Main/Title' => 'Suppression de fonctionnalités',
'Menu:DataFeatureRemovalMenu' => 'Gestion des extensions',
'combodo-data-feature-removal/Operation:Main/Title' => 'Gestion des extensions',
'DataFeatureRemoval:Main:Title' => 'Suppression de fonctionnalités',
'DataFeatureRemoval:Main:SubTitle' => 'Préparez les fonctionnalités que vous souhaitez activer ou désactiver lors dune prochaine configuration',
'DataFeatureRemoval:Failure:Title' => 'Erreurs lors de la simulation de suppression de fonctionnalités',
'DataFeatureRemoval:Helper:Title' => 'Activez ou désactivez les fonctionnalités installées dans votre iTop.',
'DataFeatureRemoval:Helper:Desc1' => 'Cette étape prépare lassistant de configuration à activer ou désactiver des fonctionnalités.',
'DataFeatureRemoval:Helper:Desc2' => 'Analyse si des données ou des dépendances empêchent lactivation ou la désactivation dune fonctionnalité.',
'DataFeatureRemoval:Main:Title' => 'Gestion des extensions',
'DataFeatureRemoval:Main:SubTitle' => 'Sélectionner les extensions à installer sur votre iTop',
'DataFeatureRemoval:Failure:Title' => 'Erreurs lors de la simulation de suppression d\'extensions',
'DataFeatureRemoval:Helper:Title' => 'Activez ou désactivez les extensions installées dans votre iTop.',
'DataFeatureRemoval:Features:Title' => 'Fonctionnalités',
'DataFeatureRemoval:Features:Title' => 'Extensions',
'DataFeatureRemoval:Result:Title' => 'Modification demandée',
'DataFeatureRemoval:NoResult:Title' => 'Aucune modification demandée',
'DataFeatureRemoval:Execution:Title' => 'Suppressions',
'DataFeatureRemoval:Analysis:Title' => 'Résultat de lanalyse',
'DataFeatureRemoval:Analysis:Subtitle' => 'Vérifier les éléments à nettoyer',
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s élément(s) à nettoyer avant de poursuivre',
'DataFeatureRemoval:Analysis:Ok' => "Aucune donnée à nettoyer",
'DataFeatureRemoval:DeletionPlan:Title' => 'Plan de suppression',
'DataFeatureRemoval:DeletionPlan:Title' => 'Plan de suppression des données',
'DataFeatureRemoval:DeletionPlan:SubTitle' => '%1$s ligne(s) à nettoyer avant de poursuivre',
'DataFeatureRemoval:DoDeletion:Title' => 'Exécuter la suppression',
'DataFeatureRemoval:DoDeletion:SubTitle' => 'Supprime toutes les entrées de la base de données',
'DataFeatureRemoval:DeletionPlan:ToManyOperations' => 'Trop dentrées à nettoyer',
'DataFeatureRemoval:DeletionPlan:Error:Issues' => 'Certains objets doivent être supprimés manuellement avant le nettoyage',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Élément à supprimer',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Fonctionnalité',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Extension',
'DataFeatureRemoval:Table:Analysis:Module' => 'Module',
'DataFeatureRemoval:Table:Analysis:Occurrence' => 'Occurrence',
'DataFeatureRemoval:CleanupComplete:Title' => 'All clear.',
'DataFeatureRemoval:CompilComplete' => 'Compilation successful. No Cleanup needed. You can proceed to setup.',
'DataFeatureRemoval:Compile:InProgress' => 'Compilation en cours...',
'DataFeatureRemoval:Compile:Success' => 'Compilation terminée',
'DataFeatureRemoval:Compile:Error' => 'Erreur lors de la compilation',
'DataFeatureRemoval:RunAudit:InProgress' => 'Analyse en cours...',
'DataFeatureRemoval:RunAudit:Success' => 'Analyse terminée',
'DataFeatureRemoval:RunAudit:Error' => 'Erreur lors de l\'analyse',
'UI:Button:Analyze' => 'Analyser',
'UI:Button:ModifyChoices' => 'Modifier les choix',
'UI:Button:ModifyChoices' => 'Modifier la sélection',
'UI:Button:AnalyzeAndSetup' => 'Analyser et ouvrir lassistant de configuration',
'UI:Button:PlanDeletion' => 'Préparer le plan de suppression',
'UI:Button:DoAsyncDeletion' => 'Lancer la suppression en tâche de fond',
'UI:Button:PlanDeletion' => 'Supprimer les données',
'UI:Button:DoDeletion' => 'Supprimer les données',
'UI:Button:BackToMain' => 'Retour à la suppression de fonctionnalités',
'UI:Button:Setup' => 'Retour à lassistant de configuration',
'UI:Button:BackToMain' => 'Modifier la sélection',
'UI:Button:Setup' => 'Lancer le setup',
'UI:Action:ForceUninstall' => 'Forcer la désinstallation',
'UI:Action:MoreInfo' => 'Plus dinformations',
@@ -53,7 +64,7 @@ Dict::Add('FR FR', 'French', 'Français', [
'DataFeatureRemoval:Column:Class' => 'Classe',
'DataFeatureRemoval:Column:DeleteCount' => 'Entrées à supprimer',
'DataFeatureRemoval:Column:UpdateCount' => 'Entrées à mettre à jour',
'DataFeatureRemoval:Column:Issue' => 'Problème',
'DataFeatureRemoval:Column:IssueCount' => 'Problèmes empêchant le nettoyage automatique',
'DataFeatureRemoval:Column:DeletedCount' => 'Entrées supprimées',
'DataFeatureRemoval:Column:UpdatedCount' => 'Entrées mises à jour',

View File

@@ -0,0 +1,75 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/**
* Localized data
*/
/**
* @author Vladimir Kunin <v.b.kunin@gmail.com>
*
*/
Dict::Add('RU RU', 'Russian', 'Русский', [
'Menu:DataFeatureRemovalMenu' => 'Управление расширениями',
'combodo-data-feature-removal/Operation:Main/Title' => 'Управление расширениями',
'DataFeatureRemoval:Main:Title' => 'Управление расширениями',
'DataFeatureRemoval:Main:SubTitle' => 'Включение и отключение расширений, установленных в вашем iTop',
'DataFeatureRemoval:Failure:Title' => 'Ошибки пробного удаления расширений',
'DataFeatureRemoval:Helper:Title' => 'Проверьте, есть ли данные или зависимости, мешающие добавить/удалить расширение.',
'DataFeatureRemoval:Features:Title' => 'Расширения',
'DataFeatureRemoval:Result:Title' => 'Запрошено изменение',
'DataFeatureRemoval:NoResult:Title' => 'Изменений не запрошено',
'DataFeatureRemoval:Execution:Title' => 'Выполнения удаления',
'DataFeatureRemoval:Analysis:Title' => 'Результат анализа',
'DataFeatureRemoval:Analysis:Subtitle' => 'Просмотрите все элементы, требующие внимания',
'DataFeatureRemoval:Analysis:SubTitle' => 'Элементов для очистки перед продолжением: %1$s',
'DataFeatureRemoval:DeletionPlan:Title' => 'План удаления данных',
'DataFeatureRemoval:DeletionPlan:SubTitle' => 'Строк для очистки перед продолжением: %1$s',
'DataFeatureRemoval:DoDeletion:Title' => 'Выполнить удаление',
'DataFeatureRemoval:DoDeletion:SubTitle' => 'Удалить все записи из базы данных',
'DataFeatureRemoval:DeletionPlan:Error:Issues' => 'Некоторые объекты нужно удалить вручную перед очисткой',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Элемент для удаления',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Название расширения',
'DataFeatureRemoval:Table:Analysis:Module' => 'Название модуля',
'DataFeatureRemoval:Table:Analysis:Occurrence' => 'Количество',
'DataFeatureRemoval:CleanupComplete:Title' => 'Всё чисто.',
'DataFeatureRemoval:CompilComplete' => 'Компиляция выполнена успешно. Очистка не требуется. Можно переходить к установке.',
'DataFeatureRemoval:Compile:InProgress' => 'Идёт компиляция...',
'DataFeatureRemoval:Compile:Success' => 'Компиляция выполнена успешно',
'DataFeatureRemoval:Compile:Error' => 'Ошибка компиляции',
'DataFeatureRemoval:RunAudit:InProgress' => 'Идёт анализ...',
'DataFeatureRemoval:RunAudit:Success' => 'Анализ завершён',
'DataFeatureRemoval:RunAudit:Error' => 'Ошибка при анализе',
'UI:Button:Analyze' => 'Анализировать',
'UI:Button:ModifyChoices' => 'Изменить выбор',
'UI:Button:AnalyzeAndSetup' => 'Анализировать и перейти к установке',
'UI:Button:PlanDeletion' => 'Продолжить удаление',
'UI:Button:DoDeletion' => 'Продолжить удаление',
'UI:Button:BackToMain' => 'Изменить выбор',
'UI:Button:Setup' => 'Запустить установку',
'UI:Action:ForceUninstall' => 'Принудительно удалить',
'UI:Action:MoreInfo' => 'Подробнее',
'DataFeatureRemoval:Table:Empty' => 'Нет данных для удаления',
'DataFeatureRemoval:Column:Class' => 'Класс',
'DataFeatureRemoval:Column:DeleteCount' => 'Записей к удалению',
'DataFeatureRemoval:Column:UpdateCount' => 'Записей к обновлению',
'DataFeatureRemoval:Column:IssueCount' => 'Найдено проблем, мешающих автоматической очистке',
'DataFeatureRemoval:Column:DeletedCount' => 'Удалено записей',
'DataFeatureRemoval:Column:UpdatedCount' => 'Обновлено записей',
]);

View File

@@ -0,0 +1,62 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/**
* Localized data
*/
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Menu:DataFeatureRemovalMenu' => '扩展管理',
'combodo-data-feature-removal/Operation:Main/Title' => '扩展管理',
'DataFeatureRemoval:Main:Title' => '扩展管理',
'DataFeatureRemoval:Main:SubTitle' => '切换安装在您的 iTop 上的扩展',
'DataFeatureRemoval:Failure:Title' => '扩展预删除错误',
'DataFeatureRemoval:Helper:Title' => '分析是否有任何数据或依赖关系阻止您添加/删除扩展。',
'DataFeatureRemoval:Features:Title' => '扩展',
'DataFeatureRemoval:Result:Title' => '请求的修改',
'DataFeatureRemoval:Execution:Title' => '删除执行',
'DataFeatureRemoval:Analysis:Title' => '分析结果',
'DataFeatureRemoval:Analysis:Subtitle' => '审查所有需要关注的元素',
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s 个元素需要在继续之前清理',
'DataFeatureRemoval:DeletionPlan:Title' => '数据删除计划',
'DataFeatureRemoval:DeletionPlan:SubTitle' => '%1$s 行需要在继续之前清理',
'DataFeatureRemoval:DoDeletion:Title' => '执行删除',
'DataFeatureRemoval:DoDeletion:SubTitle' => '从数据库中删除所有条目',
'DataFeatureRemoval:DeletionPlan:Error:Issues' => '某些对象必须在清理前手动删除',
'DataFeatureRemoval:Table:Analysis:ClassName' => '要删除的元素',
'DataFeatureRemoval:Table:Analysis:FeatureName' => '扩展名称',
'DataFeatureRemoval:Table:Analysis:Module' => '模块名称',
'DataFeatureRemoval:Table:Analysis:Occurrence' => '出现次数',
'DataFeatureRemoval:CleanupComplete:Title' => '全部清除.',
'DataFeatureRemoval:CompilComplete' => '编译成功. 无需清理. 您可以继续进行设置.',
'UI:Button:Analyze' => '分析',
'UI:Button:ModifyChoices' => '改变我的选择',
'UI:Button:AnalyzeAndSetup' => '分析并进入设置',
'UI:Button:PlanDeletion' => '继续删除',
'UI:Button:DoDeletion' => '继续删除',
'UI:Button:BackToMain' => '改变我的选择',
'UI:Button:Setup' => '运行安装向导',
'UI:Action:ForceUninstall' => '强制卸载',
'UI:Action:MoreInfo' => '更多信息',
'DataFeatureRemoval:Table:Empty' => '没有数据需要删除',
'DataFeatureRemoval:Column:Class' => '类',
'DataFeatureRemoval:Column:DeleteCount' => '待删除的条目',
'DataFeatureRemoval:Column:UpdateCount' => '待更新的条目',
'DataFeatureRemoval:Column:IssueCount' => '发现阻止自动清理的问题',
'DataFeatureRemoval:Column:DeletedCount' => '已删除的条目',
'DataFeatureRemoval:Column:UpdatedCount' => '已更新的条目',
]);

View File

@@ -31,7 +31,6 @@ SetupWebPage::AddModule(
'datamodel' => [
'vendor/autoload.php',
'model.combodo-data-feature-removal.php', // Contains the PHP code generated by the "compilation" of datamodel.combodo-data-feature-removal.xml
'src/Hook/DataFeatureRemovalBackgroundTask.php',
],
'webservice' => [],
'data.struct' => [

View File

@@ -8,51 +8,64 @@
namespace Combodo\iTop\DataFeatureRemoval\Controller;
require_once APPROOT.'setup/feature_removal/SetupAudit.php';
require_once APPROOT.'setup/feature_removal/DryRemovalRuntimeEnvironment.php';
use Combodo\iTop\Application\Helper\Session;
use Combodo\iTop\Application\TwigBase\Controller\Controller;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalConfig;
use Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\DataFeatureRemoval\Service\BackgroundOperationService;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
use Combodo\iTop\DataFeatureRemoval\Service\DataCleanupService;
use Combodo\iTop\DataFeatureRemoval\Service\DataFeatureRemoverExtensionService;
use Combodo\iTop\DataFeatureRemoval\Service\DeletionPlanService;
use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment;
use Combodo\iTop\DataFeatureRemoval\Service\StaticDeletionPlan;
use Combodo\iTop\Service\Session\SessionParameters;
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
use ContextTag;
use CoreException;
use Dict;
use Exception;
use IssueLog;
use MetaModel;
use MFCompiler;
use RunTimeEnvironment;
use SetupUtils;
use utils;
class DataFeatureRemovalController extends Controller
{
private array $aSelectedExtensionsForCheck = [];
private ?array $aExtensionsToCheck = null;
private bool $bForcedUninstallation = false;
private array $aCountClassesToCleanup = [];
private array $aAnalysisDataTable = [];
private array $aDeletionExecutionSummary = [];
private int $iCount = 0;
private int $iColumnCount = 2;
private RunTimeEnvironment $oRuntimeEnvironment;
public function OperationMain($sErrorMessage = null): void
{
$aParams = [];
$this->ReadRemovedExtensions();
SetupUtils::EraseSetupToken();
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
$oParameters->Erase();
Session::Unset('aDeletionExecutionSummary');
Session::Set('bForceCompilation', true);
$oParameters->SetParameter('return_application', 'DataFeatureRemoval');
$this->AddAnalyzeParams();
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['aExtensions'] = $this->GetExtensionsTable();
$aParams['aExtensionsCode'] = $this->aSelectedExtensionsForCheck;
$aParams['iColumnCount'] = $this->iColumnCount;
$aParams['aAvailableExtensions'] = $this->SplitArrayIntoColumns($this->GetAvailableExtensions(), $this->iColumnCount);
$aParams['aAnalysisDataTable'] = $this->aAnalysisDataTable;
$aParams['aClasses'] = array_keys($this->aCountClassesToCleanup);
$aParams['DataFeatureRemovalErrorMessage'] = $sErrorMessage;
$aParams['bHasData'] = $this->iCount > 0;
$aParams['sSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup';
$aParams['iCount'] = $this->iCount;
$aParams['DataFeatureRemovalErrorMessage'] = $sErrorMessage;
$aParams['bAnalysisOk'] = (count($this->aCountClassesToCleanup) > 0) && ($this->iCount === 0);
$this->AddLinkedStylesheet(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/css/DataFeatureRemoval.css');
$this->AddLinkedScript(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/js/DataFeatureRemoval.js');
$this->m_sOperation = "Main";
$this->DisplayPage($aParams);
}
@@ -73,143 +86,312 @@ class DataFeatureRemovalController extends Controller
$this->aAnalysisDataTable = $this->GetTableData('Analysis', $aColumns, $aData);
}
public function OperationAnalyze(): void
{
$this->ReadRemovedExtensions();
$this->m_sOperation = 'Main';
try {
if (count($this->aSelectedExtensionsForCheck) > 0) {
$this->Analyze();
}
$this->OperationMain();
} catch (Exception $e) {
IssueLog::Error(__METHOD__, null, ['stack' => $e->getTraceAsString(), 'exception' => $e->getMessage()]);
$this->OperationMain($e->getMessage());
}
}
private function Analyze(): void
{
$sSourceEnv = MetaModel::GetEnvironment();
$oDryRemovalRuntimeEnvironment = new DryRemovalRuntimeEnvironment($sSourceEnv, $this->aSelectedExtensionsForCheck);
$oDryRemovalRuntimeEnvironment->CompileFrom($sSourceEnv);
$oSetupAudit = new SetupAudit($sSourceEnv);
$aGetRemovedClasses = $oSetupAudit->RunDataAudit();
IssueLog::Debug(__METHOD__, null, ['aGetRemovedClasses' => $aGetRemovedClasses]);
$this->aCountClassesToCleanup = $aGetRemovedClasses;
}
public function OperationDeletionPlan(): void
public function OperationAnalysisResult(): void
{
$aParams = [];
$this->ValidateTransactionId();
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
//from setup wizard/mtp
if (!SetupUtils::IsSessionSetupTokenValid()) {
//from same module
$this->ValidateTransactionId();
}
$aDeletionPlanSummaryEntities = DeletionPlanService::GetInstance()->GetDeletionPlanSummary($aClasses);
$aColumns = ['Class', 'DeleteCount' , 'UpdateCount', 'Issue'];
// Display changed extensions
$aSetupParameterNames = [
'selected_extensions' => '[]',
'selected_modules' => '[]',
'display_choices' => '',
'added_extensions' => '[]',
'removed_extensions' => '[]',
'extensions_not_uninstallable' => '[]',
'copy_setup_files' => 1,
'force-uninstall' => '',
'use_symbolic_links' => MFCompiler::UseSymbolicLinks() ? 'on' : '',
'return_application' => '',
'target_env' => ITOP_DEFAULT_ENV,
];
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
foreach ($aSetupParameterNames as $sInputName => $defaultValue) {
$oParameters->SetParameter($sInputName, $oParameters->GetParameter($sInputName, $defaultValue));
}
$aAddedExtensions = json_decode($oParameters->GetParameter('added_extensions', '[]'), true);
$aRemovedExtensions = json_decode($oParameters->GetParameter('removed_extensions', '[]'), true);
if ('[]' === $oParameters->GetParameter('selected_modules', '[]')) {
//it does not come from setup
// we get extensions from 1st screen uiblocks
$this->ReadExtensionsDiff();
$oParameters->SetParameter('force-uninstall', $this->bForcedUninstallation ? 'on' : '');
$aAddedExtensions = $this->aExtensionsToCheck['to_be_installed'];
$oParameters->SetParameter('added_extensions', $this->ConvertIntoSetupFormat($aAddedExtensions));
$aRemovedExtensions = $this->aExtensionsToCheck['to_be_removed'];
$oParameters->SetParameter('removed_extensions', $this->ConvertIntoSetupFormat($aRemovedExtensions));
$aExtensionsNotUninstallable = $this->aExtensionsToCheck['extensions_not_uninstallable'];
$oParameters->SetParameter('extensions_not_uninstallable', $this->ConvertIntoSetupFormat($aExtensionsNotUninstallable));
}
DataFeatureRemovalLog::Debug(__METHOD__.' Extensions given in parameter', null, [
'added_extensions' => $aAddedExtensions,
'removed_extensions' => $aRemovedExtensions]);
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['iColumnCount'] = $this->iColumnCount;
$aAvailableExtensions = $this->GetExtensionsDiff($aAddedExtensions, $aRemovedExtensions);
$aParams['aAvailableExtensionsCount'] = count($aAvailableExtensions);
$aParams['aAvailableExtensions'] = $this->SplitArrayIntoColumns($aAvailableExtensions, $this->iColumnCount);
$aParams['sAjaxURL'] = utils::GetAbsoluteUrlModulePage(DataFeatureRemovalHelper::MODULE_NAME, 'index.php');
$this->DisplayPage($aParams, 'AnalysisResult');
}
private function ConvertIntoSetupFormat(array $aData): string
{
$aNewData = [];
foreach ($aData as $sCode => $sLabel) {
$aNewData[] = sprintf('"%s":"%s"', $sCode, $sLabel);
}
return "{".implode(',', $aNewData)."}";
}
/**
* @return void
* @throws \ConfigException
* @throws \CoreException
*/
public function OperationAjaxCompile(): void
{
$aParams = [];
//to make setup redirection work, we need to pass complex data structures to setup wizards (ie extension/module lists)
$sSourceEnv = MetaModel::GetEnvironment();
$oRuntimeEnvironment = new RunTimeEnvironment($sSourceEnv, false);
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
$aSelectedModules = json_decode($oParameters->GetParameter('selected_modules', '[]'), true);
$aSelectedExtensions = json_decode($oParameters->GetParameter('selected_extensions', '[]'), true);
$aAddedExtensions = json_decode($oParameters->GetParameter('added_extensions', '[]'), true);
$aRemovedExtensions = json_decode($oParameters->GetParameter('removed_extensions', '[]'), true);
try {
$this->ValidateTransactionId();
$oConfig = MetaModel::GetConfig();
if (count($aSelectedModules) === 0) {
$aSelectedExtensions = DataFeatureRemoverExtensionService::GetInstance()->GetExtensionMap()->GetSelectedExtensions($oConfig, array_keys($aAddedExtensions), array_keys($aRemovedExtensions));
$oParameters->SetParameter('selected_extensions', $this->ConvertIntoSetupFormat($aSelectedExtensions));
}
$sBuildDir = APPROOT."/env-$sSourceEnv-build";
if (! is_dir($sBuildDir)) {
SetupUtils::builddir($sBuildDir);
}
$bIsDirEmpty = count(scandir($sBuildDir)) === 2;
$bForceCompilation = Session::Get('bForceCompilation', false);
if ($bIsDirEmpty || $bForceCompilation) {
$oRuntimeEnvironment->CopySetupFiles();
if (count($aSelectedModules) === 0) {
$aSelectedModules = $oRuntimeEnvironment->GetModulesToLoadFromChoices($oConfig, $aSelectedExtensions);
}
DataFeatureRemovalLog::Debug(
__METHOD__,
null,
['sSourceEnv' => $sSourceEnv, 'sBuildDir' => $sBuildDir, 'bIsDirEmpty' => $bIsDirEmpty, glob("$sBuildDir/*")]
);
$oRuntimeEnvironment->DoCompile($aSelectedExtensions, $aRemovedExtensions, $aSelectedModules, MFCompiler::CanUseSymbolicLinks());
Session::Unset('bForceCompilation');
} else {
if (count($aSelectedModules) === 0) {
$aSelectedModules = $oRuntimeEnvironment->GetModulesToLoadFromChoices($oConfig, $aSelectedExtensions);
}
}
} catch (CoreException $e) {
$aParams['error_message'] = $e->getHtmlDesc();
} catch (Exception $e) {
$aParams['error_message'] = $e->getMessage();
}
$aParams['success_message'] = Dict::S('DataFeatureRemoval:Compile:Success');
$aParams['transaction_id'] = utils::GetNewTransactionId();
$oParameters->SetParameter('selected_modules', json_encode($aSelectedModules));
$this->DisplayJSONPage($aParams);
}
public function OperationAjaxRunAudit(): void
{
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
$aParams = [];
$aPageParams = [];
try {
$this->ValidateTransactionId();
$sSourceEnv = MetaModel::GetEnvironment();
$oSetupAudit = new SetupAudit($sSourceEnv);
$aRemovedClasses = array_keys($oSetupAudit->RunDataAudit());
$oParameters->SetParameter('classes', $aRemovedClasses);
new ContextTag(ContextTag::TAG_SETUP);
$aPageParams['sLaunchSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup/wizard.php';
$aPageParams['aSetupParams'] = [
"_class" => "WizStepLandingBeforeAudit",
"operation" => "next",
];
$aPageParams['sTransactionId'] = utils::GetNewTransactionId();
$aDeletionPlanSummaryEntities = $this->GetDeletionPlanSummaryEntities($aRemovedClasses);
[$aPageParams['aDeletionPlanSummary'], $aPageParams['iQueryCount'], $aPageParams['bDeletionPossible']] = $this->GetDeletionPlanSummaryTable($aDeletionPlanSummaryEntities);
[$aPageParams['aDeletionExecutionSummary'], $aPageParams['bHasDeletionExecution']] = $this->GetExecutionSummaryTable();
$aPageParams['bDeletionNeeded'] = ($aPageParams['iQueryCount'] > 0);
if (!$aPageParams['bDeletionNeeded']) {
// Erase session setup parameters
SetupUtils::CreateSetupToken();
}
$this->DisplayAjaxPage($aPageParams, 'AjaxRunAudit');
return;
} catch (CoreException $e) {
$aParams['error_message'] = $e->getHtmlDesc();
} catch (Exception $e) {
$aParams['error_message'] = $e->getMessage();
}
$this->DisplayJSONPage($aParams);
}
private function GetExecutionSummaryTable(): array
{
$sName = 'ExecutionSummary';
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary') ?? serialize([]));
$aTableData = [];
if (count($this->aDeletionExecutionSummary) === 0) {
return [$aTableData, false];
}
$aColumns = ['Class', 'Total Deleted Count' , 'Total Updated Count', 'Deleted Count' , 'Updated Count'];
$aRows = [];
/** @var DataCleanupSummaryEntity $oSummary */
foreach ($this->aDeletionExecutionSummary as $sClass => $oSummary) {
$aRows[] = [
$sClass,
$oSummary->iTotalDeleteCount,
$oSummary->iTotalUpdateCount,
$oSummary->iDeleteCount,
$oSummary->iUpdateCount,
];
}
$aTableData = $this->GetTableData($sName, $aColumns, $aRows);
return [$aTableData, true];
}
private function GetDeletionPlanSummaryEntities(array $aRemovedClasses): array
{
$oDataCleanupService = new StaticDeletionPlan();
return $oDataCleanupService->GetCleanupSummary($aRemovedClasses);
}
private function GetDeletionPlanSummaryTable(array $aDeletionPlanSummaryEntities): array
{
$sName = 'DeletionPlanSummary';
$aColumns = ['Class', 'Delete Count' , 'Update Count', 'Issue Count'];
$aRows = [];
$iQueryCount = 0;
$bHasIssues = false;
foreach ($aDeletionPlanSummaryEntities as $oDeletionPlanSummaryEntity) {
$aRows[] = [
$oDeletionPlanSummaryEntity->sClass,
$oDeletionPlanSummaryEntity->iDeleteCount,
$oDeletionPlanSummaryEntity->iUpdateCount,
$oDeletionPlanSummaryEntity->sIssue ?? '',
$oDeletionPlanSummaryEntity->iIssueCount,
];
$bHasIssues |= ($oDeletionPlanSummaryEntity->iIssueCount !== 0);
$iQueryCount += $oDeletionPlanSummaryEntity->iDeleteCount;
$iQueryCount += $oDeletionPlanSummaryEntity->iUpdateCount;
}
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['aDeletionPlanSummary'] = $this->GetTableData('Extensions', $aColumns, $aRows);
$aParams['aClasses'] = $aClasses;
$aParams['aExtensionsCode'] = utils::ReadPostedParam('aExtensionsCode', []);
$aParams['iQueryCount'] = $iQueryCount;
$aParams['bDeletionPossible'] = ($iQueryCount <= DataFeatureRemovalConfig::GetInstance()->Get('max_count_estimation_for_safe_cleanup', 100));
$this->DisplayPage($aParams);
return [$this->GetTableData($sName, $aColumns, $aRows), $iQueryCount, !$bHasIssues];
}
public function OperationDoDeletion(): void
{
$aParams = [];
$this->ValidateTransactionId();
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
$sDeletionButtonValue = utils::ReadPostedParam('btn_deletion', null);
$bAsynchronous = ('async_deletion' === $sDeletionButtonValue);
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary') ?? serialize([]));
Session::Unset('aDeletionExecutionSummary');
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
$aClasses = $oParameters->GetParameter('classes', []);
if ($bAsynchronous) {
BackgroundOperationService::GetInstance()->CreateOperation(utils::ReadPostedParam('aExtensionsCode', []), $aClasses);
$this->OperationMain();
return;
$oDataCleanupService = new DataCleanupService();
$aDeletionExecutionSummary = $oDataCleanupService->ExecuteCleanup($aClasses);
foreach ($aDeletionExecutionSummary as $sClass => $oExecutionSummary) {
if (!array_key_exists($sClass, $this->aDeletionExecutionSummary)) {
$this->aDeletionExecutionSummary[$sClass] = new DataCleanupSummaryEntity($sClass);
}
$oSummary = $this->aDeletionExecutionSummary[$sClass];
$oSummary->iDeleteCount = $oExecutionSummary->iDeleteCount;
$oSummary->iUpdateCount = $oExecutionSummary->iUpdateCount;
$oSummary->iTotalDeleteCount += $oExecutionSummary->iDeleteCount;
$oSummary->iTotalUpdateCount += $oExecutionSummary->iUpdateCount;
}
$aDeletionExecutionSummary = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses);
$aColumns = ['Class', 'DeletedCount' , 'UpdatedCount'];
$aRows = [];
foreach ($aDeletionExecutionSummary as $oDeletionExecutionSummaryEntity) {
$aRows[] = [
$oDeletionExecutionSummaryEntity->sClass,
$oDeletionExecutionSummaryEntity->iDeleteCount,
$oDeletionExecutionSummaryEntity->iUpdateCount,
];
}
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['aDeletionExecutionSummary'] = $this->GetTableData('Extensions', $aColumns, $aRows);
$this->DisplayPage($aParams);
Session::Set('aDeletionExecutionSummary', serialize($this->aDeletionExecutionSummary));
$this->OperationAnalysisResult();
}
/**
* Get installed extensions from disk
*
* @return array structure for twig datatable
*/
private function GetExtensionsTable(): array
private function GetAvailableExtensions(bool $bIncludePackageExtensions = false): array
{
$aExtensions = [];
$aColumns = ['', 'Version', 'Name', 'Code'];
$aExtensionsData = [];
if ($bIncludePackageExtensions) {
$aExtensionsRef = DataFeatureRemoverExtensionService::GetInstance()->GetExtensionMap()->GetAllExtensionsWithPreviouslyInstalled();
} else {
$aExtensionsRef = DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions();
}
foreach (DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions() as $sCode => $oExtension) {
foreach ($aExtensionsRef as $oExtension) {
/** @var \iTopExtension $oExtension */
$aExtensionsData[$oExtension->sCode] = [
'version' => $oExtension->sVersion,
'label' => $oExtension->sLabel,
'code' => $oExtension->sCode,
'description' => $oExtension->sDescription,
'source' => $oExtension->GetExtensionSourceLabel(),
'installed' => $oExtension->bInstalled,
'extra_flags' => [
'uninstallable' => $oExtension->CanBeUninstalled(),
'remote' => $oExtension->IsRemote(),
'missing' => $oExtension->bRemovedFromDisk,
'dependency_issue' => $oExtension->HasDependencyIssue(),
],
$bCleanupOngoing = BackgroundOperationService::GetInstance()->IsExtensionBeingCleaned($sCode);
$sChecked = '';
$sDisabledHtml = '';
$sLabel = $oExtension->sLabel;
if ($bCleanupOngoing) {
$sDisabledHtml = 'disabled=""';
$sLabel .= <<<HTML
&nbsp; <span class="ibo-spinner ibo-is-inline ibo-spinner ibo-block" data-role="ibo-spinner"><i class="ibo-spinner--icon fas fa-sync-alt fa-spin" aria-hidden="true"/></span>
HTML;
;
} elseif ($oExtension->bRemovedFromDisk) {
$sDisabledHtml = 'disabled=""';
$sChecked = 'checked';
} elseif (in_array($sCode, $this->aSelectedExtensionsForCheck)) {
$sChecked = 'checked';
}
$sVersion = $oExtension->sVersion;
$sIdEnable = "aExtensions[$sCode][enable]";
$aExtensions[] = [
<<<HTML
<input type="checkbox" $sDisabledHtml class="extension_check" $sChecked id="$sIdEnable" name="$sIdEnable"/>
HTML,
$sVersion,
$sLabel,
$sCode,
];
}
return $this->GetTableData('Extensions', $aColumns, $aExtensions);
return $aExtensionsData;
}
private function GetExtensionsDiff(array $aAddedExtensions, array $aRemovedExtensions): array
{
$aExtensions = [];
foreach ($this->GetAvailableExtensions(true) as $sCode => $aExtension) {
$aExtension['extra_flags']['disabled'] = true;
if (isset($aAddedExtensions[$sCode])) {
$aExtension['extra_flags']['selected'] = true;
$aExtensions[$sCode] = $aExtension;
} elseif (isset($aRemovedExtensions[$sCode])) {
$aExtension['extra_flags']['selected'] = false;
$aExtensions[$sCode] = $aExtension;
}
}
return $aExtensions;
}
private function GetTableData(string $sTableName, array $aColumns, array $aData): array
@@ -246,36 +428,67 @@ HTML,
}
$sTransactionId = utils::ReadPostedParam('transaction_id', null, utils::ENUM_SANITIZATION_FILTER_TRANSACTION_ID);
IssueLog::Debug(__FUNCTION__.": Transaction [$sTransactionId]");
DataFeatureRemovalLog::Debug(__FUNCTION__.": Transaction [$sTransactionId]");
if (empty($sTransactionId) || !utils::IsTransactionValid($sTransactionId, false)) {
throw new DataFeatureRemovalException(Dict::S("iTopUpdate:Error:InvalidToken"));
}
}
/**
* @return array
* Read extensions selected from posted parameters
*/
public function ReadRemovedExtensions(): array
public function ReadExtensionsDiff(): void
{
if (count($this->aSelectedExtensionsForCheck) > 0) {
return $this->aSelectedExtensionsForCheck;
if (!is_null($this->aExtensionsToCheck)) {
return;
}
$aSelectedExtensionsFromUI = utils::ReadPostedParam('aExtensions', []);
foreach ($aSelectedExtensionsFromUI as $sCode => $aData) {
$sValue = $aData['enable'] ?? 'off';
if (($sValue) === 'on') {
$this->aSelectedExtensionsForCheck[] = $sCode;
$aAvailableExtensions = $this->GetAvailableExtensions();
$aSelectedExtensionsFromUI = utils::ReadPostedParam('aSelectedExtensions', []);
$this->aExtensionsToCheck = [
'to_be_installed' => [],
'to_be_removed' => [],
'extensions_not_uninstallable' => [],
];
foreach ($aAvailableExtensions as $sCode => &$aExtensionData) {
if (!isset($aSelectedExtensionsFromUI[$sCode])) {
continue;
}
if ($aExtensionData['installed'] && $aSelectedExtensionsFromUI[$sCode] !== 'on') {
$aExtensionData['extra_flags']['selected'] = false;
$sLabel = $aAvailableExtensions[$sCode]['label'];
$this->aExtensionsToCheck['to_be_removed'][$sCode] = $sLabel;
if (! $this->bForcedUninstallation && $aExtensionData['extra_flags']['uninstallable']) {
$this->bForcedUninstallation = true;
}
if (false === $aExtensionData['extra_flags']['uninstallable'] || true === $aExtensionData['extra_flags']['remote']) {
$this->aExtensionsToCheck['extensions_not_uninstallable'][] = $sCode;
}
} elseif (!$aExtensionData['installed'] && $aSelectedExtensionsFromUI[$sCode] === 'on') {
$aExtensionData['extra_flags']['selected'] = true;
$sLabel = $aAvailableExtensions[$sCode]['label'];
$this->aExtensionsToCheck['to_be_installed'][$sCode] = $sLabel;
}
}
}
// Add source removed to check
foreach (DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions() as $sCode => $oExtension) {
if ($oExtension->bRemovedFromDisk) {
$this->aSelectedExtensionsForCheck[] = $sCode;
}
/**
* Divide an array into several sub arrays, distributing elements so that every sub array has an equal amount of elements
* @param mixed[] $aInput
* @param int $iColNumber
*
* @return array[]
*/
private function SplitArrayIntoColumns(array $aInput, int $iColNumber)
{
$aOutput = array_fill(0, $iColNumber, []);
$iIndex = 0;
foreach ($aInput as $mItem) {
//Split extensions in $iColNumber columns
$aOutput[$iIndex % $this->iColumnCount][] = $mItem;
$iIndex++;
}
return $this->aSelectedExtensionsForCheck;
return $aOutput;
}
}

View File

@@ -2,18 +2,14 @@
namespace Combodo\iTop\DataFeatureRemoval\Entity;
class DeletionPlanSummaryEntity
class DataCleanupSummaryEntity
{
public string $sClass;
/**
* @var int : DEL_MANUAL|DEL_AUTO|DEL_SILENT|DEL_MOVEUP|DEL_NONE
* @see \AttributeDefinition DEL_xxx
*/
public int $iMode = 0;
public ?string $sIssue = null;
public int $iIssueCount = 0;
public int $iUpdateCount = 0;
public int $iDeleteCount = 0;
public int $iTotalUpdateCount = 0;
public int $iTotalDeleteCount = 0;
/**
* @param string $sClass

View File

@@ -0,0 +1,37 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Entity;
class DeletionPlanEntity
{
public readonly DeletionPlanItem $oDelete;
public readonly DeletionPlanItem $oUpdate;
public readonly DeletionPlanItem $oIssue;
/**
* @param \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem $oDelete
* @param \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem $oUpdate
* @param \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem $oIssue
*/
public function __construct()
{
$this->oDelete = $oDelete ?? new DeletionPlanItem();
$this->oUpdate = $oUpdate ?? new DeletionPlanItem();
$this->oIssue = $oIssue ?? new DeletionPlanItem();
}
public function TotalCount(): int
{
return $this->oDelete->Count() + $this->oUpdate->Count() + $this->oIssue->Count();
}
public function FilterUpdatesByDeletes()
{
$this->oUpdate->FilterBy($this->oDelete);
}
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Entity;
class DeletionPlanItem
{
public array $aIds = [];
/**
* @param array $aIds
*/
public function __construct(array $aIds = [])
{
$this->aIds = $aIds;
}
public function Merge(DeletionPlanItem $oItem): void
{
$this->aIds = array_unique(array_merge($this->aIds, $oItem->aIds));
}
public function Count(): int
{
return count($this->aIds);
}
public function FilterBy(DeletionPlanItem $oItem): void
{
$this->aIds = array_diff($this->aIds, $oItem->aIds);
}
}

View File

@@ -7,7 +7,6 @@
namespace Combodo\iTop\DataFeatureRemoval\Helper;
use Config;
use MetaModel;
use utils;
@@ -21,11 +20,11 @@ class DataFeatureRemovalConfig
final public static function GetInstance(): DataFeatureRemovalConfig
{
if (!isset(static::$oInstance)) {
static::$oInstance = new DataFeatureRemovalConfig();
if (!isset(self::$oInstance)) {
self::$oInstance = new DataFeatureRemovalConfig();
}
return static::$oInstance;
return self::$oInstance;
}
public function Get(string $sParamName, $default = null)
@@ -50,22 +49,4 @@ class DataFeatureRemovalConfig
$oConfig = utils::GetConfig();
$oConfig->SetModuleSetting(DataFeatureRemovalHelper::MODULE_NAME, $sParamName, $value);
}
/**
* @param \Config|null $oConfig
*
* @return void
* @throws \ConfigException
* @throws \CoreException
*/
public function SaveItopConfiguration(Config $oConfig = null)
{
if (is_null($oConfig)) {
$oConfig = utils::GetConfig();
}
$sConfigFile = APPROOT.'conf/'.utils::GetCurrentEnvironment().'/config-itop.php';
@chmod($sConfigFile, 0770); // Allow overwriting the file
$oConfig->WriteToFile($sConfigFile);
@chmod($sConfigFile, 0444); // Read-only
}
}

View File

@@ -10,14 +10,4 @@ namespace Combodo\iTop\DataFeatureRemoval\Helper;
class DataFeatureRemovalHelper
{
public const MODULE_NAME = 'combodo-data-feature-removal';
public static function IsTimeLimitExceeded(int $iUnixTimeLimit): bool
{
if ($iUnixTimeLimit === 0) {
//no time limit
return false;
}
return (time() > $iUnixTimeLimit);
}
}

View File

@@ -1,48 +0,0 @@
<?php
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalConfig;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\DataFeatureRemoval\Service\BackgroundOperationService;
use Combodo\iTop\DataFeatureRemoval\Service\DeletionPlanService;
class DataFeatureRemovalBackgroundTask implements iBackgroundProcess
{
/**
* @inheritDoc
*/
public function GetPeriodicity()
{
return DataFeatureRemovalConfig::GetInstance()->Get('cron_periodicity_in_s', 10);
}
/**
* @inheritDoc
*/
public function Process($iUnixTimeLimit)
{
$iCount = 0;
while ($oBackgroundOperation = BackgroundOperationService::GetInstance()->GetNext()) {
$aClasses = BackgroundOperationService::GetInstance()->GetClasses($oBackgroundOperation);
$aRes = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses, $iUnixTimeLimit);
IssueLog::Info(__METHOD__, null, $aRes);
IssueLog::Debug(__METHOD__, null, [
'$iUnixTimeLimit' => $iUnixTimeLimit,
'time' => time(),
'timeout reached' => DataFeatureRemovalHelper::IsTimeLimitExceeded($iUnixTimeLimit),
]);
if (DataFeatureRemovalHelper::IsTimeLimitExceeded($iUnixTimeLimit)) {
//timeout reached
return "Handled $iCount operations.";
}
//execution finished before timeout: nothing left to remove
$oBackgroundOperation->DBDelete();
$iCount++;
}
return "Handled $iCount operations.";
}
}

View File

@@ -1,105 +0,0 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Service;
use DataFeatureRemovalBackgroundOperation;
use DBObjectSet;
use DBSearch;
use MetaModel;
class BackgroundOperationService
{
private static BackgroundOperationService $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): BackgroundOperationService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new BackgroundOperationService();
}
return self::$oInstance;
}
final public static function SetInstance(?BackgroundOperationService $oInstance): void
{
static::$oInstance = $oInstance;
}
/**
* @return \DataFeatureRemovalBackgroundOperation
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
* @throws \OQLException
*/
public function GetNext(): ?DataFeatureRemovalBackgroundOperation
{
$sOQL = <<<OQL
SELECT DataFeatureRemovalBackgroundOperation
OQL;
$oSet = new DBObjectSet(DBSearch::FromOQL($sOQL), ['creation_date' => true]);
/** @var DataFeatureRemovalBackgroundOperation $oDBObject */
$oDBObject = $oSet->Fetch();
return $oDBObject;
}
/**
* @param array $aExtensionsCodes
* @param array $aClasses
* @return void
* @throws \ArchivedObjectException
* @throws \CoreCannotSaveObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \CoreWarning
* @throws \MySQLException
* @throws \OQLException
*/
public function CreateOperation(array $aExtensionsCodes, array $aClasses): void
{
sort($aExtensionsCodes);
sort($aClasses);
$aValues = [
'creation_date' => time(),
'features_code' => '|'.implode('|', $aExtensionsCodes).'|',
'classes' => implode(',', $aClasses),
];
$oObj = MetaModel::NewObject('DataFeatureRemovalBackgroundOperation', $aValues);
$oObj->DBWrite();
}
/**
* @param string $sExtensionCode
* @return bool
* @throws \CoreException
* @throws \MissingQueryArgument
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
* @throws \OQLException
*/
public function IsExtensionBeingCleaned(string $sExtensionCode): bool
{
$sOQL = <<<OQL
SELECT DataFeatureRemovalBackgroundOperation WHERE features_code LIKE '%|$sExtensionCode|%'
OQL;
$oSet = new DBObjectSet(DBSearch::FromOQL($sOQL));
return $oSet->Count() > 0;
}
/**
* @param \DataFeatureRemovalBackgroundOperation $oBackgroundOperation
* @return array
*/
public function GetClasses(DataFeatureRemovalBackgroundOperation $oBackgroundOperation): array
{
return explode(',', $oBackgroundOperation->Get('classes'));
}
}

View File

@@ -0,0 +1,179 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Service;
use CMDBObjectSet;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
use Combodo\iTop\Service\Limits\ExecutionLimits;
use DBObject;
use DBObjectSearch;
use MetaModel;
class DataCleanupService
{
private array $aVisited = [];
private iObjectService $oObjectService;
private ExecutionLimits $oExecutionLimits;
public function __construct(int $iMaxExecutionTime = 30, int $iMaxMemoryPercent = 80)
{
DataFeatureRemovalLog::Enable();
$this->oExecutionLimits = new ExecutionLimits($iMaxExecutionTime, $iMaxMemoryPercent);
}
/**
* Get a summary of the deletion plan computed for the classes.
* The result is used for display
*
* @param array|null $aClasses
*
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity>
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
*/
public function GetCleanupSummary(?array $aClasses): array
{
return $this->ExecuteCleanup($aClasses ?? [], oObjectService: new ObjectServiceSummary());
}
private function GetNextObjectToDelete(array $aClasses): ?DBObject
{
foreach ($aClasses as $sClass) {
$oFilter = new DBObjectSearch($sClass);
$oFilter->AllowAllData();
$oSet = new \DBObjectSet($oFilter);
while ($oObject = $oSet->Fetch()) {
if (!$this->IsVisited($oObject)) {
return $oObject;
}
}
}
return null;
}
/**
* @param array $aClasses
* @param \Combodo\iTop\DataFeatureRemoval\Service\iObjectService|null $oObjectService
*
* @return array execution summary
* @throws \ArchivedObjectException
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function ExecuteCleanup(array $aClasses, ?iObjectService $oObjectService = null): array
{
$this->oObjectService = $oObjectService ?? new ObjectService();
$this->aVisited = [];
while ($oObject = $this->GetNextObjectToDelete($aClasses)) {
if ($this->RecursiveDeletion($oObject) === false) {
// Timeout, stop here
break;
}
}
return $this->oObjectService->GetSummary();
}
private function MarkObjectAsVisited(DBObject $oObject): void
{
$sClass = get_class($oObject);
$sId = $oObject->GetKey();
$sKey = "$sClass-$sId";
$this->aVisited[$sKey] = true;
}
private function IsVisited(DBObject $oObject): bool
{
$sClass = get_class($oObject);
$sId = $oObject->GetKey();
$sKey = "$sClass-$sId";
$bRes = $this->aVisited[$sKey] ?? false;
DataFeatureRemovalLog::Debug('Checking if object is visited', null, [$sKey, $bRes]);
return $bRes;
}
/**
*
* @param \DBObject $oObjectToClean
*
* @return bool true if deletion is complete, false in case of timeout or memory limit reached
*
* @throws \ArchivedObjectException
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
private function RecursiveDeletion(DBObject $oObjectToClean): bool
{
$this->MarkObjectAsVisited($oObjectToClean);
$sClass = get_class($oObjectToClean);
$aReferencingMe = MetaModel::EnumReferencingClasses($sClass);
foreach ($aReferencingMe as $sRemoteClass => $aExtKeys) {
/** @var \AttributeExternalKey $oExtKeyAttDef */
foreach ($aExtKeys as $sExtKeyAttCode => $oExtKeyAttDef) {
// skip if this external key is behind an external field
if (!$oExtKeyAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) {
continue;
}
$oSearch = new DBObjectSearch($sRemoteClass);
$oSearch->AddCondition($sExtKeyAttCode, $oObjectToClean->GetKey(), '=');
$oSearch->AllowAllData();
$oSet = new CMDBObjectSet($oSearch);
$oSet->OptimizeColumnLoad([$sRemoteClass => [$oExtKeyAttDef->GetCode()]]);
/** @var DBObject $oDependentObj */
while ($oDependentObj = $oSet->Fetch()) {
$iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption();
if ($oExtKeyAttDef->IsNullAllowed()) {
// Optional external key, list to reset
if (($iDeletePropagationOption == DEL_MOVEUP) && ($oExtKeyAttDef->IsHierarchicalKey())) {
// Move the child up one level i.e. set the same parent as the current object
$iParentId = $oObjectToClean->Get($oExtKeyAttDef->GetCode());
$this->oObjectService->Update($oDependentObj, $oExtKeyAttDef->GetCode(), $iParentId);
} else {
$this->oObjectService->Update($oDependentObj, $oExtKeyAttDef->GetCode(), 0);
}
if ($this->oExecutionLimits->ShouldStopExecution()) {
return false;
}
} else {
// Mandatory external key
if ($iDeletePropagationOption == DEL_MANUAL) {
// Cannot be deleted automatically, must be handled manually
$this->oObjectService->SetIssue(get_class($oDependentObj));
continue;
}
// Propagate deletion only if not visited
if ($this->IsVisited($oDependentObj)) {
continue;
}
if (!$this->RecursiveDeletion($oDependentObj)) {
// Timeout
return false;
}
}
}
}
}
$this->oObjectService->Delete($sClass, $oObjectToClean->GetKey());
if ($this->oExecutionLimits->ShouldStopExecution()) {
return false;
}
return true;
}
}

View File

@@ -14,7 +14,7 @@ use MetaModel;
class DataFeatureRemoverExtensionService
{
private static DataFeatureRemoverExtensionService $oInstance;
private ?iTopExtensionsMap $oMap = null;
private array $aItopExtensions = [];
private array $aIncludingExtensionsByModuleName = [];
@@ -60,15 +60,25 @@ class DataFeatureRemoverExtensionService
return $this->aIncludingExtensionsByModuleName[$sModuleName] ?? [];
}
/**
* @return \iTopExtensionsMap
*/
public function GetExtensionMap(): iTopExtensionsMap
{
if (is_null($this->oMap)) {
$this->oMap = iTopExtensionsMap::GetExtensionsMap();
$this->oMap->LoadInstalledExtensionsFromDatabase(MetaModel::GetConfig());
}
return $this->oMap;
}
/**
* @return iTopExtension[]
*/
public function ReadItopExtensions(): array
{
if (count($this->aItopExtensions) === 0) {
$oExtensionsMap = new iTopExtensionsMap();
$oExtensionsMap->LoadInstalledExtensionsFromDatabase(MetaModel::GetConfig());
$this->aItopExtensions = $oExtensionsMap->GetAllExtensionsToDisplayInSetup(true);
$this->aItopExtensions = $this->GetExtensionMap()->GetAllExtensionsToDisplayInSetup(true);
uasort($this->aItopExtensions, function (iTopExtension $oiTopExtension1, iTopExtension $oiTopExtension2) {
return strcmp($oiTopExtension1->sLabel, $oiTopExtension2->sLabel);

View File

@@ -1,212 +0,0 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Service;
use CMDBSource;
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use DBObjectSearch;
use DeletionPlan;
use MetaModel;
class DeletionPlanService
{
private static DeletionPlanService $oInstance;
public int $iExecutionCount = 0;
protected function __construct()
{
}
final public static function GetInstance(): DeletionPlanService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new DeletionPlanService();
}
return self::$oInstance;
}
final public static function SetInstance(?DeletionPlanService $oInstance): void
{
self::$oInstance = $oInstance;
}
/**
* Get a summary of the deletion plan computed for the classes.
* The result is used for display
*
* @param array|null $aClasses
*
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity>
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function GetDeletionPlanSummary(?array $aClasses): array
{
$aSummary = [];
if (is_null($aClasses)) {
return $aSummary;
}
$oDeletionPlan = $this->GetDeletionPlan($aClasses);
foreach ($oDeletionPlan->ListUpdates() as $sClass => $aUpdates) {
$oDeletionPlanSummaryEntity = new DeletionPlanSummaryEntity($sClass);
$oDeletionPlanSummaryEntity->iUpdateCount = count($aUpdates);
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
}
foreach ($oDeletionPlan->ListDeletes() as $sClass => $aDeletes) {
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
$oDeletionPlanSummaryEntity->iDeleteCount = count($aDeletes);
$aDelete = array_shift($aDeletes);
$oDeletionPlanSummaryEntity->iMode = $aDelete['mode'];
$oDeletionPlanSummaryEntity->sIssue = $aDelete['issue'] ?? null;
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
}
return $aSummary;
}
/**
* @param string $sClass
*
* @return \DBObject[]
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
* @throws \Exception
*/
private function GetAllObjects(string $sClass): array
{
$oFilter = new DBObjectSearch($sClass);
$oFilter->AllowAllData();
$oSet = new \DBObjectSet($oFilter);
return $oSet->ToArray();
}
/**
* @since 3.3.0
* @param array $aClasses
* @param int $iUnixTimeLimit : max execution time in seconds since Epoch before stopping deletion. by default: no limit (ie remove all without stop)
* @param int $iMaxExecutionCount : max execution count before stopping deletion. by default: no limit (ie remove all without stop)
*
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity>
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function ExecuteDeletionPlan(array $aClasses, int $iUnixTimeLimit = 0, int $iMaxExecutionCount = -1): array
{
$oDeletionPlan = $this->GetDeletionPlan($aClasses);
if (count($oDeletionPlan->GetIssues()) > 0) {
throw new DataFeatureRemovalException("Deletion Plan cannot be executed due to issues");
}
$this->iExecutionCount = 0;
$aSummary = [];
foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate) {
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
foreach ($aToUpdate as $aData) {
if ($this->IsTimeLimitExceeded($iUnixTimeLimit, $iMaxExecutionCount)) {
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
return $aSummary;
}
$this->iExecutionCount++;
$oToUpdate = $aData['to_reset'];
/** @var \DBObject $oToUpdate */
foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef) {
$oToUpdate->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]);
}
$oToUpdate->DBUpdate();
$oDeletionPlanSummaryEntity->iUpdateCount++;
}
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
}
foreach ($oDeletionPlan->ListDeletes() as $sClass => $aDeletes) {
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
foreach ($aDeletes as $sId => $aDelete) {
if ($this->IsTimeLimitExceeded($iUnixTimeLimit, $iMaxExecutionCount)) {
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
return $aSummary;
}
$this->iExecutionCount++;
try {
CMDBSource::Query('START TRANSACTION');
// Delete any existing change tracking about the current object
$oFilter = new DBObjectSearch('CMDBChangeOp');
$oFilter->AddCondition('objclass', $sClass, '=');
$oFilter->AddCondition('objkey', $sId, '=');
MetaModel::PurgeData($oFilter);
// Delete the entry
$aClassesToRemove = array_merge(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL), MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false));
foreach ($aClassesToRemove as $sParentClass) {
$oFilter = DBObjectSearch::FromOQL_AllData("SELECT $sParentClass WHERE id=:id");
$sQuery = $oFilter->MakeDeleteQuery(['id' => $sId]);
CMDBSource::DeleteFrom($sQuery);
}
CMDBSource::Query('COMMIT');
} catch (\Exception $e) {
\IssueLog::Exception(__METHOD__.": Cleanup failed", $e);
CMDBSource::Query('ROLLBACK');
throw $e;
}
$oDeletionPlanSummaryEntity->iDeleteCount++;
}
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
}
return $aSummary;
}
/**
* Get a deletion plan for all the objects of the classes
*
* @param array $aClasses array of class names to clean
*
* @return \DeletionPlan
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function GetDeletionPlan(array $aClasses): DeletionPlan
{
$oDeletionPlan = new DeletionPlan();
foreach ($aClasses as $sClass) {
$aObjects = $this->GetAllObjects($sClass);
foreach ($aObjects as $oObject) {
$oObject->CheckToDelete($oDeletionPlan);
}
}
return $oDeletionPlan;
}
public function IsTimeLimitExceeded(int $iUnixTimeLimit, int $iMaxExecutionCount = -1): bool
{
if (($iMaxExecutionCount !== -1) && ($iMaxExecutionCount <= $this->iExecutionCount)) {
return true;
}
return DataFeatureRemovalHelper::IsTimeLimitExceeded($iUnixTimeLimit);
}
}

View File

@@ -0,0 +1,60 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Service;
use CMDBSource;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
use DBObject;
use DBObjectSearch;
use MetaModel;
class ObjectService extends ObjectServiceSummary
{
public function Update(DBObject $oToUpdate, string $sAttCode, $value): void
{
$oToUpdate->Set($sAttCode, $value);
$oToUpdate->DBUpdate();
parent::Update($oToUpdate, $sAttCode, $value);
}
public function Delete(string $sClass, string $sId): void
{
try {
CMDBSource::Query('START TRANSACTION');
// Delete any existing change tracking about the current object
$oFilter = new DBObjectSearch('CMDBChangeOp');
$oFilter->AddCondition('objclass', $sClass, '=');
$oFilter->AddCondition('objkey', $sId, '=');
MetaModel::PurgeData($oFilter);
// Delete the entry
$aClassesToRemove = array_merge(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL), MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false));
foreach ($aClassesToRemove as $sParentClass) {
/** @var DBObjectSearch $oFilter */
$oFilter = DBObjectSearch::FromOQL_AllData("SELECT $sParentClass WHERE id=:id");
$sQuery = $oFilter->MakeDeleteQuery(['id' => $sId]);
CMDBSource::DeleteFrom($sQuery);
}
CMDBSource::Query('COMMIT');
parent::Delete($sClass, $sId);
} catch (\Exception $e) {
DataFeatureRemovalLog::Exception(__METHOD__.': Cleanup failed', $e);
CMDBSource::Query('ROLLBACK');
throw $e;
}
}
public function SetIssue(string $sClass): void
{
throw new DataFeatureRemovalException('Deletion Plan cannot be executed due to issues');
}
}

View File

@@ -0,0 +1,71 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Service;
use Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
use DBObject;
/**
* Manage operation summary instead of doing the actual update or delete
*
* The summary is an array [class => DeletionPlanSummaryEntity]
*/
class ObjectServiceSummary implements iObjectService
{
private array $aSummary = [];
public function Update(DBObject $oToUpdate, string $sAttCode, $value): void
{
$sClass = get_class($oToUpdate);
DataFeatureRemovalLog::Debug('Object to update', null, ['class' => $sClass, 'id' => $oToUpdate->GetKey(), 'code' => $sAttCode, 'value' => "$value"]);
if (! array_key_exists($sClass, $this->aSummary)) {
$this->aSummary[$sClass] = new DataCleanupSummaryEntity($sClass);
}
$oDeletionPlanSummaryEntity = $this->aSummary[$sClass];
$oDeletionPlanSummaryEntity->iUpdateCount++;
$oDeletionPlanSummaryEntity->iTotalUpdateCount++;
}
public function Delete(string $sClass, string $sId): void
{
DataFeatureRemovalLog::Debug('Object to delete', null, ['class' => $sClass, 'id' => $sId]);
if (!array_key_exists($sClass, $this->aSummary)) {
$this->aSummary[$sClass] = new DataCleanupSummaryEntity($sClass);
}
$oDeletionPlanSummaryEntity = $this->aSummary[$sClass];
$oDeletionPlanSummaryEntity->iDeleteCount++;
$oDeletionPlanSummaryEntity->iTotalDeleteCount++;
}
public function SetIssue(string $sClass): void
{
DataFeatureRemovalLog::Debug('Issue on object', null, ['class' => $sClass]);
if (!array_key_exists($sClass, $this->aSummary)) {
$this->aSummary[$sClass] = new DataCleanupSummaryEntity($sClass);
}
$oDeletionPlanSummaryEntity = $this->aSummary[$sClass];
$oDeletionPlanSummaryEntity->iIssueCount++;
}
public function GetSummary(): array
{
return $this->aSummary;
}
public function SetSummary(array $aSummary): void
{
foreach ($aSummary as $sClass => $oPreviousSummaryEntity) {
$oSummaryEntity = new DataCleanupSummaryEntity($sClass);
$oSummaryEntity->iTotalUpdateCount = $oPreviousSummaryEntity->iTotalUpdateCount;
$oSummaryEntity->iTotalDeleteCount = $oPreviousSummaryEntity->iTotalDeleteCount;
$this->aSummary[$sClass] = $oSummaryEntity;
}
}
}

View File

@@ -0,0 +1,221 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Service;
use CMDBSource;
use Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanEntity;
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem;
use MetaModel;
class StaticDeletionPlan
{
/** @var array<DeletionPlanEntity> */
private array $aDeletionPlan = [];
/**
* Get a summary of the deletion plan computed for the classes.
* The result is used for display
*
* @param array|null $aClasses
*
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity>
* @throws \CoreException
*/
public function GetCleanupSummary(?array $aClasses): array
{
$aSummary = [];
$aDeletionPlan = $this->GetStaticDeletionPlan($aClasses ?? []);
foreach ($aDeletionPlan as $sClass => $oDeletionPlanEntity) {
if ($oDeletionPlanEntity->TotalCount() === 0) {
continue;
}
$oDeletionPlanEntity->FilterUpdatesByDeletes();
$oDataCleanupSummary = new DataCleanupSummaryEntity($sClass);
$oDataCleanupSummary->iUpdateCount = $oDeletionPlanEntity->oUpdate->Count();
$oDataCleanupSummary->iDeleteCount = $oDeletionPlanEntity->oDelete->Count();
$oDataCleanupSummary->iIssueCount = $oDeletionPlanEntity->oIssue->Count();
$aSummary[$sClass] = $oDataCleanupSummary;
}
return $aSummary;
}
/**
* @param array $aClasses Classes to clean entirely
*
* @return array ['class' => DeletionPlanEntity];
*
* @throws \CoreException
*/
public function GetStaticDeletionPlan(array $aClasses): array
{
foreach ($aClasses as $sClass) {
$oDeletionPlanItem = $this->GetInitialClassDeletionPlan($sClass);
// N°9831 Do not overwrite existing entity as it may already exist for this class if a previously processed class references it (issues/updates already accumulated must be kept)
if (false === array_key_exists($sClass, $this->aDeletionPlan)) {
$this->aDeletionPlan[$sClass] = new DeletionPlanEntity();
}
$this->aDeletionPlan[$sClass]->oDelete->Merge($oDeletionPlanItem);
$this->DeletionPlanForReferencingClasses($sClass);
}
return $this->aDeletionPlan;
}
private function DeletionPlanForReferencingClasses(string $sClass): void
{
$sIdsToRemove = implode(', ', $this->aDeletionPlan[$sClass]->oDelete->aIds);
$aReferencingMe = MetaModel::EnumReferencingClasses($sClass);
foreach ($aReferencingMe as $sRemoteClass => $aExtKeys) {
if (!isset($this->aDeletionPlan[$sRemoteClass])) {
$this->aDeletionPlan[$sRemoteClass] = new DeletionPlanEntity();
}
$oDeletionPlanEntity = $this->aDeletionPlan[$sRemoteClass];
/** @var \AttributeExternalKey $oExtKeyAttDef */
foreach ($aExtKeys as $sExtKeyAttCode => $oExtKeyAttDef) {
// skip if this external key is behind an external field
if (!$oExtKeyAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) {
continue;
}
if ($oExtKeyAttDef->IsNullAllowed()) {
// update
$oUpdateItem = $this->UpdateExtKeyNullable($sRemoteClass, $sExtKeyAttCode, $sIdsToRemove);
$oDeletionPlanEntity->oUpdate->Merge($oUpdateItem);
} else {
// delete
$aRemoteIdsToRemove = $this->GetRemoteIdsForExtKey($sRemoteClass, $sExtKeyAttCode, $sIdsToRemove);
$iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption();
if ($iDeletePropagationOption == DEL_MANUAL) {
// Issue, do not recurse
$oDeletionPlanItem = new DeletionPlanItem(aIds: $aRemoteIdsToRemove);
$oDeletionPlanEntity->oIssue->Merge($oDeletionPlanItem);
continue;
}
if (($iDeletePropagationOption == DEL_MOVEUP) && ($oExtKeyAttDef->IsHierarchicalKey())) {
// update hierarchical keys due to row cleanup in the same table
$sTargetIdsToRemove = implode(',', $this->aDeletionPlan[$sRemoteClass]->oDelete->aIds);
$oUpdateItem = $this->UpdateHierarchicalExtKey($sRemoteClass, $sExtKeyAttCode, $sTargetIdsToRemove);
$oDeletionPlanEntity->oUpdate->Merge($oUpdateItem);
}
// Delete entries in Remote Class
if (count($aRemoteIdsToRemove) !== 0) {
$oDeletionPlanEntity->oDelete->Merge(new DeletionPlanItem($aRemoteIdsToRemove));
// Infinite loops do not occurs due to the datamodel structure
$this->DeletionPlanForReferencingClasses($sRemoteClass);
}
}
}
}
}
/**
* @param string $sRemoteClass
* @param string $sExtKeyAttCode
* @param string $sIdsToRemoveInTargetClass
*
* @return \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem
* @throws \CoreException
*/
public function UpdateExtKeyNullable(string $sRemoteClass, string $sExtKeyAttCode, string $sIdsToRemoveInTargetClass): DeletionPlanItem
{
$aIds = $this->GetRemoteIdsForExtKey($sRemoteClass, $sExtKeyAttCode, $sIdsToRemoveInTargetClass);
return new DeletionPlanItem($aIds);
}
/**
* @param string $sRemoteClass
* @param string $sExtKeyAttCode
* @param string $sIdsToRemoveInTargetClass
*
* @return \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem
* @throws \CoreException
* @throws \MySQLException
*/
public function UpdateHierarchicalExtKey(string $sRemoteClass, string $sExtKeyAttCode, string $sIdsToRemoveInTargetClass): DeletionPlanItem
{
[$sDBTable, $sDBField, $sDBKey] = $this->GetDBInfoForAttcode($sRemoteClass, $sExtKeyAttCode);
$sSQL = <<<SQL
SELECT `$sDBKey`
FROM `$sDBTable` AS `updated`
INNER JOIN `$sDBTable` AS `removed` ON `updated`.`$sDBField` = `removed`.`$sDBKey`
WHERE `removed`.`$sDBKey` IN ($sIdsToRemoveInTargetClass)
SQL;
$aIds = CMDBSource::QueryToCol($sSQL, $sDBKey);
return new DeletionPlanItem($aIds);
}
/**
* @param string $sRemoteClass
* @param string $sExtKeyAttCode
* @param string $sIdsToRemoveInTargetClass
*
* @return array
* @throws \CoreException
* @throws \MySQLException
*/
public function GetRemoteIdsForExtKey(string $sRemoteClass, string $sExtKeyAttCode, string $sIdsToRemoveInTargetClass): array
{
if (\utils::IsNullOrEmptyString($sIdsToRemoveInTargetClass)) {
return [];
}
[$sDBTable, $sDBField, $sDBKey] = $this->GetDBInfoForAttcode($sRemoteClass, $sExtKeyAttCode);
$sSQL = "SELECT `$sDBKey` FROM `$sDBTable` WHERE `$sDBField` IN ($sIdsToRemoveInTargetClass)";
return CMDBSource::QueryToCol($sSQL, $sDBKey);
}
/**
* @param string $sClass
*
* @return \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem
* @throws \CoreException
* @throws \MySQLException
*/
public function GetInitialClassDeletionPlan(string $sClass): DeletionPlanItem
{
$sTable = MetaModel::DBGetTable($sClass);
$sDBKey = MetaModel::DBGetKey($sClass);
$sSQL = "SELECT `$sDBKey` FROM `$sTable`";
$aIds = CMDBSource::QueryToCol($sSQL, $sDBKey);
return new DeletionPlanItem($aIds);
}
/**
* Get database table for an attcode
*
* @param string $sClass
* @param string $sExtKeyAttCode
*
* @return array
* @throws \CoreException
* @throws \Exception
*/
public function GetDBInfoForAttcode(string $sClass, string $sExtKeyAttCode): array
{
$sOriginClass = MetaModel::GetAttributeOrigin($sClass, $sExtKeyAttCode);
$sDBTable = MetaModel::DBGetTable($sOriginClass);
$oAttDef = MetaModel::GetAttributeDef($sOriginClass, $sExtKeyAttCode);
// External key is on a single DB column
$sDBField = array_keys($oAttDef->GetSQLColumns())[0];
$sDBKey = MetaModel::DBGetKey($sClass);
return [$sDBTable, $sDBField, $sDBKey];
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Service;
use DBObject;
interface iObjectService
{
public function Update(DBObject $oToUpdate, string $sAttCode, $value): void;
public function Delete(string $sClass, string $sId): void;
public function SetIssue(string $sClass): void;
public function GetSummary(): array;
}

View File

@@ -0,0 +1,34 @@
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% if bDeletionNeeded %}
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:DeletionPlan:Title'|dict_s} %}
{% UIDataTable ForForm { sRef:'aDeletionPlanSummary', aColumns:aDeletionPlanSummary.Columns, aData:aDeletionPlanSummary.Data} %}{% EndUIDataTable %}
{% EndUIFieldSet %}
{% if bDeletionPossible %}
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
{% UIInput ForHidden { sName:'operation', sValue:'DoDeletion'} %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}
{% else %}
{% UIAlert ForFailure { sTitle: '', sContent: 'DataFeatureRemoval:DeletionPlan:Error:Issues'|dict_s } %}{% EndUIAlert %}
{% endif %}
{% else %}
{% UIAlert ForSuccess { sTitle:'DataFeatureRemoval:CleanupComplete:Title'|dict_s, sContent:'DataFeatureRemoval:CompilComplete'|dict_s } %}{% EndUIAlert %}
{% UIForm Standard {sId:'launch-setup-form', Action:sLaunchSetupUrl, EncType: 'application/x-www-form-urlencoded'} %}
{% for sKey, sValue in aSetupParams %}
{% UIInput ForHidden { sName:sKey, sValue:sValue } %}
{% endfor %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Setup'|dict_s, sName:'btn_setup', sId:'btn_setup', bIsSubmit:true} %}
{% EndUIForm %}
{% endif %}
{% if bHasDeletionExecution %}
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:Execution:Title'|dict_s} %}
{% UIDataTable ForForm { sRef:'aDeletionExecutionSummary', aColumns:aDeletionExecutionSummary.Columns, aData:aDeletionExecutionSummary.Data} %}{% EndUIDataTable %}
{% EndUIFieldSet %}
{% endif %}

View File

@@ -0,0 +1,55 @@
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:Analysis:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:Analysis:Subtitle'|dict_s} %}
{% if null != DataFeatureRemovalErrorMessage %}
<div id="feature_removal_error_msg_div" style="display:block">
{% UIAlert ForFailure { sTitle:'DataFeatureRemoval:Failure:Title'|dict_s, sId: 'feature_removal_error_msg', sContent:DataFeatureRemovalErrorMessage } %}
{% EndUIAlert %}
</div>
{% endif %}
{% UIAlert ForInformation { sTitle: '', sId: 'ajax_compile_in_progress_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}
{{ 'DataFeatureRemoval:Compile:InProgress'|dict_s }}
{% UISpinner Standard { } %}
{% EndUIAlert %}
{% UIAlert ForSuccess { sTitle:'success', sId: 'ajax_compile_success_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
{% UIAlert ForFailure { sTitle:'error', sId: 'ajax_compile_error_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
{% UIAlert ForInformation { sTitle: '', sId: 'ajax_run_audit_in_progress_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}
{{ 'DataFeatureRemoval:RunAudit:InProgress'|dict_s }}
{% UISpinner Standard { } %}
{% EndUIAlert %}
{% UIAlert ForSuccess { sTitle:'DataFeatureRemoval:RunAudit:Success'|dict_s, sId: 'ajax_run_audit_success_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
{% UIAlert ForFailure { sTitle:'error', sId: 'ajax_run_audit_error_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
{% if aAvailableExtensionsCount == 0 %}
{% UITitle Neutral { sTitle:'DataFeatureRemoval:NoResult:Title'|dict_s, iLevel:2 } %}{% EndUITitle %}
{% else %}
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Result:Title'|dict_s, sSubTitle: '' } %}
{% UIMultiColumn Standard {} %}
{% for iColumnIndex in 0..iColumnCount-1 %}
{% UIColumn Standard {} %}
{% for aExtension in aAvailableExtensions[iColumnIndex] %}
{% if aExtension['installed'] %}
{% UIExtensionDetails Installed { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% else %}
{% UIExtensionDetails NotInstalled { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
{% endif %}
{% endfor %}
{% EndUIColumn %}
{% endfor %}
{% EndUIMultiColumn %}
{% EndUIPanel %}
{% endif %}
<div id="ajax_run_audit" class="ibo-block"></div>
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
{% UIInput ForHidden { sName:'operation', sValue:'Main'} %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:BackToMain'|dict_s, sName:'btn_back', sId:'btn_back', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}
{% EndUIPanel %}

View File

@@ -0,0 +1,56 @@
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
function ajax_run_audit(sTransactionId) {
$('#ajax_run_audit_in_progress_msg').removeClass('ibo-is-hidden');
$.post(
'{{ sAjaxURL|raw }}',
{ operation: 'ajax_run_audit', transaction_id: sTransactionId },
function (data) {
$('#ajax_run_audit_in_progress_msg').addClass('ibo-is-hidden');
if (data.error_message) {
$('#ajax_run_audit_error_msg .ibo-alert--title').html(data.error_message);
$('#ajax_run_audit_error_msg').removeClass('ibo-is-hidden');
} else {
$('#ajax_run_audit_success_msg').removeClass('ibo-is-hidden');
$('#ajax_run_audit').html(data);
}
}
)
.fail(function() {
$('#ajax_run_audit_in_progress_msg').addClass('ibo-is-hidden');
$('#ajax_run_audit_error_msg .ibo-alert--title').html('{{ 'DataFeatureRemoval:RunAudit:Error'|dict_s }}');
$('#ajax_run_audit_error_msg').removeClass('ibo-is-hidden');
});
}
function ajax_compile() {
$('#ajax_compile_in_progress_msg').removeClass('ibo-is-hidden');
$.post(
'{{ sAjaxURL|raw }}',
{ operation: 'ajax_compile', transaction_id: '{{ sTransactionId }}' },
function (data) {
$('#ajax_compile_in_progress_msg').addClass('ibo-is-hidden');
if (data.error_message) {
$('#ajax_compile_error_msg .ibo-alert--title').html(data.error_message);
$('#ajax_compile_error_msg').removeClass('ibo-is-hidden');
} else {
$('#ajax_compile_success_msg .ibo-alert--title').html(data.success_message);
$('#ajax_compile_success_msg').removeClass('ibo-is-hidden');
ajax_run_audit(data.transaction_id);
}
},
'json'
)
.fail(function() {
$('#ajax_compile_in_progress_msg').addClass('ibo-is-hidden');
$('#ajax_compile_error_msg .ibo-alert--title').html('{{ 'DataFeatureRemoval:Compile:Error'|dict_s }}');
$('#ajax_compile_error_msg').removeClass('ibo-is-hidden');
});
}
ajax_compile();

View File

@@ -1,35 +0,0 @@
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:DeletionPlan:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:DeletionPlan:SubTitle'|dict_format(iQueryCount) } %}
{% UIDataTable ForForm { sRef:'aDeletionPlanSummary', aColumns:aDeletionPlanSummary.Columns, aData:aDeletionPlanSummary.Data} %}{% EndUIDataTable %}
{% EndUIPanel %}
{% if bDeletionPossible %}
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
{% UIInput ForHidden { sName:'operation', sValue:'DoDeletion'} %}
{% for sKey, sClass in aClasses %}
{% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %}
{% endfor %}
{% for sKey, sCode in aExtensionsCode %}
{% UIInput ForHidden { sName:"aExtensionsCode[" ~ sKey ~ "]", sValue:sCode } %}
{% endfor %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoAsyncDeletion'|dict_s, sName:'btn_deletion', sId:'btn_async_deletion', sValue: 'async_deletion', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}
{% else %}
{{ 'DataFeatureRemoval:DeletionPlan:ToManyOperations'|dict_s }}
{% endif %}
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
{% UIInput ForHidden { sName:'operation', sValue:'Main'} %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:BackToMain'|dict_s, sName:'btn_back', sId:'btn_back', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}

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