Compare commits

...

223 Commits

Author SHA1 Message Date
Eric Espie
9fe705c70d N°9950 - Error loading structural data on upgrade 2026-08-21 10:15:50 +02:00
Eric Espie
3987785690 N°9950 - Error loading structural data on upgrade 2026-08-21 10:12:31 +02:00
Eric Espié
b8f02f48bd Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-21 10:00:20 +02:00
Eric Espié
ba426a9602 Update application/loginbasic.class.inc.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-08-21 09:59:55 +02:00
Eric Espie
285f64f8db N°9954 - Login-basic incompatible with OAuth2 server 2026-08-20 15:48:40 +02:00
Eric Espie
be5a5b0da1 N°9954 - Login-basic incompatible with OAuth2 server 2026-08-20 15:07:06 +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
821 changed files with 31283 additions and 10623 deletions

View File

@@ -2,13 +2,17 @@
```mermaid
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'themeVariables': {
'git0': 'lawngreen',
'git3': 'dodgerblue',
'git4': 'grey',
'git5': 'grey',
'git6': 'grey',
'git7': 'grey',
'git8': '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
@@ -106,6 +110,11 @@ gitGraph
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).

View File

@@ -9,11 +9,6 @@ Any PRs not following the guidelines or with missing information will not be con
## Base information
| Question | Answer
|----------------------------------------------------------------|--------
| Related to a SourceForge thread / Another PR / A GitHub Issue / 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 --> |

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

@@ -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'] = $oAttDef->GetType();
$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", $oAttDef->GetType());
$oDivField->AddDataAttribute("attribute-type", $oAttDef->GetTypeShortClassName());
$oDivField->AddDataAttribute("attribute-label", $sAttMetaDataLabel);
$oDivField->AddDataAttribute("attribute-flag-hidden", false);
$oDivField->AddDataAttribute("attribute-flag-read-only", false);

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

@@ -5,7 +5,7 @@ use Combodo\iTop\Application\Helper\Session;
/**
* Class LoginBasic
*
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
@@ -24,11 +24,7 @@ class LoginBasic extends AbstractLoginFSMExtension
protected function OnModeDetection(&$iErrorCode)
{
if (!Session::IsSet('login_mode')) {
if (isset($_SERVER['HTTP_AUTHORIZATION']) && !empty($_SERVER['HTTP_AUTHORIZATION'])) {
Session::Set('login_mode', 'basic');
} elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && !empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
Session::Set('login_mode', 'basic');
} elseif (isset($_SERVER['PHP_AUTH_USER'])) {
if (!is_null($this->GetAuthUserAndPassword())) {
Session::Set('login_mode', 'basic');
}
}
@@ -86,38 +82,49 @@ class LoginBasic extends AbstractLoginFSMExtension
return LoginWebPage::LOGIN_FSM_CONTINUE;
}
private function GetAuthUserAndPassword()
/**
* @return array|null [$sAuthUser, $sAuthPwd]
*/
private function GetAuthUserAndPassword(): ?array
{
$sAuthUser = '';
$sAuthPwd = null;
$sAuthorization = '';
if (isset($_SERVER['HTTP_AUTHORIZATION']) && !empty($_SERVER['HTTP_AUTHORIZATION'])) {
$sAuthorization = $_SERVER['HTTP_AUTHORIZATION'];
} elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && !empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
$sAuthorization = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
} elseif (isset($_SERVER['PHP_AUTH_USER'])) {
$sAuthUser = $_SERVER['PHP_AUTH_USER'];
// Unfortunately, the RFC is not clear about the encoding...
// IE and FF supply the user and password encoded in ISO-8859-1 whereas Chrome provides them encoded in UTF-8
// So let's try to guess if it's an UTF-8 string or not... fortunately all encodings share the same ASCII base
if (!LoginWebPage::LooksLikeUTF8($sAuthUser)) {
// Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8
// Supposed to be harmless in case of a plain ASCII string...
$sAuthUser = iconv('iso-8859-1', 'utf-8', $sAuthUser);
}
$sAuthPwd = $_SERVER['PHP_AUTH_PW'];
if (!LoginWebPage::LooksLikeUTF8($sAuthPwd)) {
// Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8
// Supposed to be harmless in case of a plain ASCII string...
$sAuthPwd = iconv('iso-8859-1', 'utf-8', $sAuthPwd);
}
return [$sAuthUser, $sAuthPwd];
}
if (!empty($sAuthorization)) {
list($sAuthUser, $sAuthPwd) = explode(':', base64_decode(substr($sAuthorization, 6)));
} else {
if (isset($_SERVER['PHP_AUTH_USER'])) {
$sAuthUser = $_SERVER['PHP_AUTH_USER'];
// Unfortunately, the RFC is not clear about the encoding...
// IE and FF supply the user and password encoded in ISO-8859-1 whereas Chrome provides them encoded in UTF-8
// So let's try to guess if it's an UTF-8 string or not... fortunately all encodings share the same ASCII base
if (!LoginWebPage::LooksLikeUTF8($sAuthUser)) {
// Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8
// Supposed to be harmless in case of a plain ASCII string...
$sAuthUser = iconv('iso-8859-1', 'utf-8', $sAuthUser);
}
$sAuthPwd = $_SERVER['PHP_AUTH_PW'];
if (!LoginWebPage::LooksLikeUTF8($sAuthPwd)) {
// Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8
// Supposed to be harmless in case of a plain ASCII string...
$sAuthPwd = iconv('iso-8859-1', 'utf-8', $sAuthPwd);
}
}
if (empty($sAuthorization)) {
return null;
}
return [$sAuthUser, $sAuthPwd];
// Decode without Bearer
if (!utils::StartsWith($sAuthorization, 'Basic ')) {
return null;
}
$sDecodedAuthorisation = base64_decode(substr($sAuthorization, 6));
if (str_contains($sDecodedAuthorisation, ':')) {
return explode(':', $sDecodedAuthorisation);
}
return null;
}
}

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
@@ -3022,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
*
@@ -3037,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)) {
@@ -3218,4 +3249,48 @@ TXT
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);
}
}

14
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1cca58a0e26794fb9c546a001f7f7de8",
"content-hash": "24e7507794b971955358df5f2b143ac4",
"packages": [
{
"name": "apereo/phpcas",
@@ -5570,16 +5570,16 @@
},
{
"name": "twig/twig",
"version": "v3.24.0",
"version": "v3.28.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "a6769aefb305efef849dc25c9fd1653358c148f0"
"reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/a6769aefb305efef849dc25c9fd1653358c148f0",
"reference": "a6769aefb305efef849dc25c9fd1653358c148f0",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/597c12ed286fb9d1701a36684ce6e0cbe28ebc8b",
"reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b",
"shasum": ""
},
"require": {
@@ -5634,7 +5634,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.24.0"
"source": "https://github.com/twigphp/Twig/tree/v3.28.0"
},
"funding": [
{
@@ -5646,7 +5646,7 @@
"type": "tidelift"
}
],
"time": "2026-03-17T21:31:11+00:00"
"time": "2026-07-03T20:44:34+00:00"
}
],
"packages-dev": [

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;
}

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

@@ -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

@@ -408,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

@@ -1530,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

@@ -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

@@ -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

@@ -70,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;
}
@@ -84,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;

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

@@ -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

@@ -1,11 +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="ResourceDataFeatureRemoval" _delta="define">
<parent>AbstractResource</parent>
<properties>
<comment>/* Extension management access control. */</comment>
<abstract>true</abstract>
<category>grant_by_profile</category>
</properties>
<presentation/>
<methods/>
</class>
</classes>
<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

@@ -20,6 +20,7 @@ Dict::Add('EN US', 'English', 'English', [
'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',
@@ -39,6 +40,14 @@ Dict::Add('EN US', 'English', 'English', [
'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' => 'Change my selection',
'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup',

View File

@@ -20,6 +20,7 @@ Dict::Add('FR FR', 'French', 'Français', [
'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',
@@ -39,6 +40,14 @@ Dict::Add('FR FR', 'French', 'Français', [
'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 la sélection',
'UI:Button:AnalyzeAndSetup' => 'Analyser et ouvrir lassistant de configuration',

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

@@ -8,7 +8,6 @@
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;
@@ -18,15 +17,16 @@ use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
use Combodo\iTop\DataFeatureRemoval\Service\DataCleanupService;
use Combodo\iTop\DataFeatureRemoval\Service\DataFeatureRemoverExtensionService;
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 MetaModel;
use MFCompiler;
use RunTimeEnvironment;
use SecurityException;
use SetupUtils;
use utils;
@@ -37,15 +37,22 @@ class DataFeatureRemovalController extends Controller
private array $aCountClassesToCleanup = [];
private array $aAnalysisDataTable = [];
private array $aDeletionExecutionSummary = [];
private ?RuntimeEnvironment $oRuntimeEnvironment = null;
private int $iCount = 0;
private int $iColumnCount = 2;
private RunTimeEnvironment $oRuntimeEnvironment;
public function OperationMain($sErrorMessage = null): void
{
$aParams = [];
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['iColumnCount'] = $this->iColumnCount;
@@ -57,7 +64,6 @@ class DataFeatureRemovalController extends Controller
$aParams['sSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup';
$aParams['iCount'] = $this->iCount;
Session::Set('bForceCompilation', true);
$this->AddLinkedStylesheet(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/css/DataFeatureRemoval.css');
$this->AddLinkedScript(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/js/DataFeatureRemoval.js');
$this->DisplayPage($aParams);
@@ -84,164 +90,184 @@ class DataFeatureRemovalController extends Controller
{
$aParams = [];
try {
//from setup wizard/mtp
SetupUtils::CheckSetupToken();
SetupUtils::EraseSetupToken();
} catch (SecurityException $e) {
//from setup wizard/mtp
if (!SetupUtils::IsSessionSetupTokenValid()) {
//from same module
$this->ValidateTransactionId();
}
// Display changed extensions
$aHiddenInputNames = [
$aSetupParameterNames = [
'selected_extensions' => '[]',
'selected_modules' => '[]',
'display_choices' => '[]',
'display_choices' => '',
'added_extensions' => '[]',
'removed_extensions' => '[]',
'extensions_not_uninstallable' => '[]',
'copy_setup_files' => 1,
'return_button_label' => '',
'force-uninstall' => '',
'use_symbolic_links' => MFCompiler::UseSymbolicLinks() ? 'on' : '',
'return_application' => '',
'target_env' => ITOP_DEFAULT_ENV,
];
$aHiddenInputs = [];
foreach ($aHiddenInputNames as $sInputName => $defaultValue) {
$aHiddenInputs[$sInputName] = utils::ReadPostedParam($sInputName, $defaultValue, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
}
$aParams['aHiddenInputs'] = $aHiddenInputs;
if ($aHiddenInputs['return_button_label'] !== '') {
$aParams['sReturnButtonURL'] = utils::GetAbsoluteUrlModulePage('itsm-designer-connector', 'launch.php');
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
foreach ($aSetupParameterNames as $sInputName => $defaultValue) {
$oParameters->SetParameter($sInputName, $oParameters->GetParameter($sInputName, $defaultValue));
}
$aAddedExtensions = json_decode($aHiddenInputs['added_extensions'], true);
$aRemovedExtensions = json_decode($aHiddenInputs['removed_extensions'], true);
if ("[]" === $aHiddenInputs['selected_modules']) {
$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'];
$aHiddenInputs['added_extensions'] = $this->ConvertIntoSetupFormat($aAddedExtensions);
$oParameters->SetParameter('added_extensions', $this->ConvertIntoSetupFormat($aAddedExtensions));
$aRemovedExtensions = $this->aExtensionsToCheck['to_be_removed'];
$aHiddenInputs['removed_extensions'] = $this->ConvertIntoSetupFormat($aRemovedExtensions);
$oParameters->SetParameter('removed_extensions', $this->ConvertIntoSetupFormat($aRemovedExtensions));
$aExtensionsNotUninstallable = $this->aExtensionsToCheck['extensions_not_uninstallable'];
$oParameters->SetParameter('extensions_not_uninstallable', $this->ConvertIntoSetupFormat($aExtensionsNotUninstallable));
}
$aRemoveExtensionCodes = array_keys($aRemovedExtensions);
$aParams['aAddedExtensions'] = $aAddedExtensions;
$aParams['aRemovedExtensions'] = $aRemovedExtensions;
DataFeatureRemovalLog::Debug(__METHOD__.' Extensions given in parameter', null, [
'added_extensions' => $aAddedExtensions,
'removed_extensions' => $aRemovedExtensions]);
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['iColumnCount'] = $this->iColumnCount;
$aParams['aAvailableExtensions'] = $this->SplitArrayIntoColumns($this->GetExtensionsDiff($aAddedExtensions, $aRemovedExtensions), $this->iColumnCount);
$bForceCompilation = Session::Get('bForceCompilation', false);
try {
$this->Compile($aRemoveExtensionCodes, $bForceCompilation);
} catch (CoreException $e) {
$aParams['DataFeatureRemovalErrorMessage'] = $e->getHtmlDesc();
$this->DisplayPage($aParams, 'AnalysisResult');
return;
} catch (Exception $e) {
$aParams['DataFeatureRemovalErrorMessage'] = $e->getMessage();
$this->DisplayPage($aParams, 'AnalysisResult');
return;
}
if ("[]" === $aHiddenInputs['selected_modules']) {
//to make setup redirection work, we need to pass complex data structures to setup wizards (ie extension/module lists)
$oConfig = MetaModel::GetConfig();
$aSelectedExtensions = DataFeatureRemoverExtensionService::GetInstance()->GetExtensionMap()->GetSelectedExtensions($oConfig, $aAddedExtensions, $aRemovedExtensions);
$aHiddenInputs['selected_extensions'] = $this->ConvertIntoSetupFormat($aSelectedExtensions);
$oRunTimeEnvironment = $this->GetRuntimeEnvironment($aRemovedExtensions);
$aSearchDirs = [$oRunTimeEnvironment->GetBuildDir()];
$aSelectedModules = $oRunTimeEnvironment->GetModulesToLoadFromChoices($oConfig, $aSelectedExtensions, $aSearchDirs);
$aHiddenInputs['selected_modules'] = $this->ConvertIntoSetupFormat($aSelectedModules);
}
$sSourceEnv = MetaModel::GetEnvironment();
$oSetupAudit = new SetupAudit($sSourceEnv);
$aGetRemovedClasses = array_keys($oSetupAudit->RunDataAudit());
DataFeatureRemovalLog::Debug(__METHOD__, null, ['aGetRemovedClasses' => $aGetRemovedClasses]);
$aParams['aClasses'] = $aGetRemovedClasses;
new ContextTag(ContextTag::TAG_SETUP);
$aParams['sLaunchSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup/wizard.php';
$aParams['aSetupParams'] = [
"_class" => "WizStepLandingBeforeAudit",
"operation" => "next",
"_params[authent]" => SetupUtils::CreateSetupToken(),
];
foreach ($aHiddenInputs as $sInputName => $sInputValue) {
$aParams['aSetupParams']["_params[$sInputName]"] = $sInputValue;
}
if ($aHiddenInputs['return_button_label'] !== '') {
$aParams['sReturnButtonURL'] = utils::GetAbsoluteUrlModulePage('itsm-designer-connector', 'launch.php');
}
[$aParams['aDeletionPlanSummary'], $aParams['iQueryCount'], $aParams['bDeletionPossible']] = $this->GetDeletionPlanSummaryTable($aGetRemovedClasses);
[$aParams['aDeletionExecutionSummary'], $aParams['bHasDeletionExecution']] = $this->GetExecutionSummaryTable();
$aParams['bDeletionNeeded'] = ($aParams['iQueryCount'] > 0);
Session::Set('aDeletionExecutionSummary', serialize($this->aDeletionExecutionSummary));
$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
{
return json_encode($aData);
$aNewData = [];
foreach ($aData as $sCode => $sLabel) {
$aNewData[] = sprintf('"%s":"%s"', $sCode, $sLabel);
}
return "{".implode(',', $aNewData)."}";
}
/**
* @param array $aRemovedExtensions
* @param bool $bForceCompilation
* @return void
* @throws \ConfigException
* @throws \CoreException
* @return void
* @throws \ConfigException
* @throws \CoreException
*/
private function Compile(array $aRemovedExtensions, bool $bForceCompilation = true): void
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();
$sBuildDir = APPROOT."/env-$sSourceEnv-build";
if (! is_dir($sBuildDir)) {
SetupUtils::builddir($sBuildDir);
}
$bIsDirEmpty = count(scandir($sBuildDir)) === 2;
$oRuntimeEnvironment = new RunTimeEnvironment($sSourceEnv, false);
if ($bIsDirEmpty || $bForceCompilation) {
DataFeatureRemovalLog::Debug(
__METHOD__,
null,
['sSourceEnv' => $sSourceEnv, 'sBuildDir' => $sBuildDir, 'bIsDirEmpty' => $bIsDirEmpty, glob("$sBuildDir/*")]
);
$this->GetRuntimeEnvironment($aRemovedExtensions)->CompileFrom($sSourceEnv);
$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);
}
private function GetRuntimeEnvironment(array $aRemovedExtensions): RunTimeEnvironment
public function OperationAjaxRunAudit(): void
{
if (is_null($this->oRuntimeEnvironment)) {
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
$aParams = [];
$aPageParams = [];
try {
$this->ValidateTransactionId();
$sSourceEnv = MetaModel::GetEnvironment();
$this->oRuntimeEnvironment = new DryRemovalRuntimeEnvironment($sSourceEnv, $aRemovedExtensions);
$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();
}
return $this->oRuntimeEnvironment;
$this->DisplayJSONPage($aParams);
}
private function GetExecutionSummaryTable(): array
{
$sName = 'ExcutionSummary';
$sName = 'ExecutionSummary';
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary') ?? serialize([]));
$aTableData = [];
if (count($this->aDeletionExecutionSummary) === 0) {
@@ -267,11 +293,15 @@ class DataFeatureRemovalController extends Controller
}
private function GetDeletionPlanSummaryTable(array $aRemovedClasses): array
private function GetDeletionPlanSummaryEntities(array $aRemovedClasses): array
{
$oDataCleanupService = new StaticDeletionPlan();
return $oDataCleanupService->GetCleanupSummary($aRemovedClasses);
}
private function GetDeletionPlanSummaryTable(array $aDeletionPlanSummaryEntities): array
{
$sName = 'DeletionPlanSummary';
$oDataCleanupService = new DataCleanupService();
$aDeletionPlanSummaryEntities = $oDataCleanupService->GetCleanupSummary($aRemovedClasses);
$aColumns = ['Class', 'Delete Count' , 'Update Count', 'Issue Count'];
$aRows = [];
$iQueryCount = 0;
@@ -294,9 +324,10 @@ class DataFeatureRemovalController extends Controller
{
$this->ValidateTransactionId();
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary'));
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary') ?? serialize([]));
Session::Unset('aDeletionExecutionSummary');
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
$aClasses = $oParameters->GetParameter('classes', []);
$oDataCleanupService = new DataCleanupService();
$aDeletionExecutionSummary = $oDataCleanupService->ExecuteCleanup($aClasses);
@@ -311,6 +342,7 @@ class DataFeatureRemovalController extends Controller
$oSummary->iTotalUpdateCount += $oExecutionSummary->iUpdateCount;
}
Session::Set('aDeletionExecutionSummary', serialize($this->aDeletionExecutionSummary));
$this->OperationAnalysisResult();
}
@@ -336,6 +368,7 @@ class DataFeatureRemovalController extends Controller
'uninstallable' => $oExtension->CanBeUninstalled(),
'remote' => $oExtension->IsRemote(),
'missing' => $oExtension->bRemovedFromDisk,
'dependency_issue' => $oExtension->HasDependencyIssue(),
],
];
@@ -403,12 +436,11 @@ class DataFeatureRemovalController extends Controller
/**
* Read extensions selected from posted parameters
* @return int Number of extensions to be added or removed
*/
public function ReadExtensionsDiff(): int
public function ReadExtensionsDiff(): void
{
if (!is_null($this->aExtensionsToCheck)) {
return count($this->aExtensionsToCheck['to_be_installed']) + count($this->aExtensionsToCheck['to_be_removed']);
return;
}
$aAvailableExtensions = $this->GetAvailableExtensions();
@@ -416,6 +448,7 @@ class DataFeatureRemovalController extends Controller
$this->aExtensionsToCheck = [
'to_be_installed' => [],
'to_be_removed' => [],
'extensions_not_uninstallable' => [],
];
foreach ($aAvailableExtensions as $sCode => &$aExtensionData) {
if (!isset($aSelectedExtensionsFromUI[$sCode])) {
@@ -426,16 +459,18 @@ class DataFeatureRemovalController extends Controller
$aExtensionData['extra_flags']['selected'] = false;
$sLabel = $aAvailableExtensions[$sCode]['label'];
$this->aExtensionsToCheck['to_be_removed'][$sCode] = $sLabel;
if (!$aExtensionData['extra_flags']['uninstallable'] || $aExtensionData['extra_flags']['remote']) {
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;
}
}
return count($this->aExtensionsToCheck['to_be_installed']) + count($this->aExtensionsToCheck['to_be_removed']);
}
/**

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

@@ -134,10 +134,6 @@ class DataCleanupService
/** @var DBObject $oDependentObj */
while ($oDependentObj = $oSet->Fetch()) {
$iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption();
if ($iDeletePropagationOption == DEL_MANUAL) {
$this->oObjectService->SetIssue(get_class($oDependentObj));
continue;
}
if ($oExtKeyAttDef->IsNullAllowed()) {
// Optional external key, list to reset
@@ -152,6 +148,12 @@ class DataCleanupService
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;

View File

@@ -66,7 +66,7 @@ class DataFeatureRemoverExtensionService
public function GetExtensionMap(): iTopExtensionsMap
{
if (is_null($this->oMap)) {
$this->oMap = new iTopExtensionsMap();
$this->oMap = iTopExtensionsMap::GetExtensionsMap();
$this->oMap->LoadInstalledExtensionsFromDatabase(MetaModel::GetConfig());
}
return $this->oMap;

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,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

@@ -1,15 +1,31 @@
{# @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 %}
@@ -25,77 +41,15 @@
{% endfor %}
{% EndUIMultiColumn %}
{% EndUIPanel %}
{% 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 %}
{% 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'} %}
{% for sKey, sClass in aClasses %}
{% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %}
{% endfor %}
{% for sCode, sLabel in aAddedExtensions %}
{% UIInput ForHidden { sName:"aAddedExtensions[" ~ sCode ~ "]", sValue:sLabel } %}
{% endfor %}
{% for sCode, sLabel in aRemovedExtensions %}
{% UIInput ForHidden { sName:"aRemovedExtensions[" ~ sCode ~ "]", sValue:sLabel } %}
{% endfor %}
{% for sInputName, sValue in aHiddenInputs %}
{% UIInput ForHidden { sName:sInputName, sValue:sValue } %}
{% endfor %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}
{% else %}
{% UIAlert ForFailure { sContent: 'DataFeatureRemoval:DeletionPlan:Error:Issues'|dict_s } %}{% EndUIAlert %}
{% endif %}
{% else %}
{% UIAlert ForSuccess { sTitle:'DataFeatureRemoval:CleanupComplete:Title'|dict_s, sContent:'DataFeatureRemoval:CompilComplete'|dict_s, sId:value } %}{% 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 %}
{% endif %}
{% if aHiddenInputs.return_button_label != '' %}
{% UIButton ForAlternativeSecondaryAction { sLabel:aHiddenInputs.return_button_label, OnClickJsCode: 'window.location.href="' ~ sReturnButtonURL ~ '"'} %}
{% else %}
{% 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 %}
{% 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

@@ -8,6 +8,8 @@ $baseDir = dirname($vendorDir);
return array(
'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => $baseDir . '/src/Controller/DataFeatureRemovalController.php',
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DataCleanupSummaryEntity' => $baseDir . '/src/Entity/DataCleanupSummaryEntity.php',
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanEntity' => $baseDir . '/src/Entity/DeletionPlanEntity.php',
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanItem' => $baseDir . '/src/Entity/DeletionPlanItem.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalConfig' => $baseDir . '/src/Helper/DataFeatureRemovalConfig.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => $baseDir . '/src/Helper/DataFeatureRemovalException.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => $baseDir . '/src/Helper/DataFeatureRemovalHelper.php',
@@ -16,6 +18,7 @@ return array(
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => $baseDir . '/src/Service/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectService' => $baseDir . '/src/Service/ObjectService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectServiceSummary' => $baseDir . '/src/Service/ObjectServiceSummary.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\StaticDeletionPlan' => $baseDir . '/src/Service/StaticDeletionPlan.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\iObjectService' => $baseDir . '/src/Service/iObjectService.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

View File

@@ -23,6 +23,8 @@ class ComposerStaticInit4f96a7199e2c0d90e547333758b26464
public static $classMap = array (
'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => __DIR__ . '/../..' . '/src/Controller/DataFeatureRemovalController.php',
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DataCleanupSummaryEntity' => __DIR__ . '/../..' . '/src/Entity/DataCleanupSummaryEntity.php',
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanEntity' => __DIR__ . '/../..' . '/src/Entity/DeletionPlanEntity.php',
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanItem' => __DIR__ . '/../..' . '/src/Entity/DeletionPlanItem.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalConfig' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalConfig.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalException.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalHelper.php',
@@ -31,6 +33,7 @@ class ComposerStaticInit4f96a7199e2c0d90e547333758b26464
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => __DIR__ . '/../..' . '/src/Service/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectService' => __DIR__ . '/../..' . '/src/Service/ObjectService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectServiceSummary' => __DIR__ . '/../..' . '/src/Service/ObjectServiceSummary.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\StaticDeletionPlan' => __DIR__ . '/../..' . '/src/Service/StaticDeletionPlan.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\iObjectService' => __DIR__ . '/../..' . '/src/Service/iObjectService.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);

View File

@@ -16,8 +16,8 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'DBTools:Class' => 'Класс',
'DBTools:Title' => 'Инструменты обслуживания базы данных',
'DBTools:ErrorsFound' => 'Найденные ошибки',
'DBTools:Indication' => 'Important: after fixing errors in the database you\'ll have to run the analysis again as new inconsistencies will be generated~~',
'DBTools:Disclaimer' => 'DISCLAIMER: BACKUP YOUR DATABASE BEFORE RUNNING THE FIXES~~',
'DBTools:Indication' => 'Важно: после исправления ошибок в базе данных нужно будет запустить анализ заново, так как появятся новые несоответствия',
'DBTools:Disclaimer' => 'ВНИМАНИЕ: СДЕЛАЙТЕ РЕЗЕРВНУЮ КОПИЮ БАЗЫ ДАННЫХ ПЕРЕД ЗАПУСКОМ ИСПРАВЛЕНИЙ',
'DBTools:Error' => 'Ошибка',
'DBTools:Count' => 'Количество',
'DBTools:SQLquery' => 'SQL-запрос',
@@ -28,23 +28,23 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'DBTools:ShowIds' => 'Подробный вид',
'DBTools:ShowReport' => 'Отчёт',
'DBTools:IntegrityCheck' => 'Проверка целостности',
'DBTools:FetchCheck' => 'Fetch Check (long)~~',
'DBTools:SelectAnalysisType' => 'Select analysis type~~',
'DBTools:FetchCheck' => 'Проверка выборки (долго)',
'DBTools:SelectAnalysisType' => 'Выберите тип анализа',
'DBTools:Analyze' => 'Анализировать',
'DBTools:Details' => 'Показать подробности',
'DBTools:ShowAll' => 'Показать все ошибки',
'DBTools:Inconsistencies' => 'Несоответствия базы данных',
'DBTools:DetailedErrorTitle' => '%2$s error(s) in class %1$s: %3$s~~',
'DBTools:DetailedErrorLimit' => 'List limited to %1$s errors~~',
'DBTools:DetailedErrorTitle' => 'Ошибок (%2$s) в классе %1$s: %3$s',
'DBTools:DetailedErrorLimit' => 'Список ограничен %1$s ошибками',
'DBAnalyzer-Integrity-OrphanRecord' => 'Сиротская запись в `%1$s`, она должна иметь свой аналог в таблице `%2$s`',
'DBAnalyzer-Integrity-InvalidExtKey' => 'Недопустимый внешний ключ %1$s (столбец: `%2$s.%3$s`)',
'DBAnalyzer-Integrity-MissingExtKey' => 'Отсутствует внешний ключ %1$s (столбец: `%2$s.%3$s`)',
'DBAnalyzer-Integrity-InvalidValue' => 'Недопустимое значение для %1$s (столбец: `%2$s.%3$s`)',
'DBAnalyzer-Integrity-UsersWithoutProfile' => 'Некоторые учетные записи пользователей не имеют профилей',
'DBAnalyzer-Integrity-HKInvalid' => 'Broken hierarchical key `%1$s`~~',
'DBAnalyzer-Fetch-Count-Error' => 'Fetch count error in `%1$s`, %2$d entries fetched / %3$d counted~~',
'DBAnalyzer-Integrity-FinalClass' => 'Field `%2$s`.`%1$s` must have the same value as `%3$s`.`%1$s`~~',
'DBAnalyzer-Integrity-RootFinalClass' => 'Field `%2$s`.`%1$s` must contain a valid class~~',
'DBAnalyzer-Integrity-HKInvalid' => 'Повреждён иерархический ключ `%1$s`',
'DBAnalyzer-Fetch-Count-Error' => 'Ошибка количества выборки в `%1$s`: получено записей %2$d / посчитано %3$d',
'DBAnalyzer-Integrity-FinalClass' => 'Поле `%2$s`.`%1$s` должно иметь то же значение, что и `%3$s`.`%1$s`',
'DBAnalyzer-Integrity-RootFinalClass' => 'Поле `%2$s`.`%1$s` должно содержать допустимый класс',
]);
// Database Info

View File

@@ -4,7 +4,7 @@
* Localized data
*
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
* @license http://opensource.org/licenses/AGPL-3.0
*
* This file is part of iTop.
*
@@ -21,38 +21,42 @@
* You should have received a copy of the GNU Affero General Public License
* along with iTop. If not, see <http://www.gnu.org/licenses/>
*/
// Database inconsistencies
Dict::Add('ZH CN', 'Chinese', '简体中文', [
// Dictionary entries go here
'Menu:DBToolsMenu' => '数据库工具',
'Menu:DBToolsMenu' => '数据库完整性',
'DBTools:Class' => '类型',
'DBTools:Title' => '数据库维护工具',
'DBTools:Title' => '数据库完整性检查',
'DBTools:ErrorsFound' => '发现错误',
'DBTools:Indication' => '重要: 修复数据库错误后, 可能会出现新的不一致, 您必须重新运行一次分析.',
'DBTools:Indication' => '重要提示: 修复数据库错误后, 可能会出现新的不一致, 您必须重新运行一次分析.',
'DBTools:Disclaimer' => '免责申明: 在应用修复之前, 应先备份数据库',
'DBTools:Error' => '错误',
'DBTools:Count' => '个数',
'DBTools:SQLquery' => 'SQL查询',
'DBTools:FixitSQLquery' => '修复问题的SQL查询 (指示)',
'DBTools:SQLresult' => 'SQL结果',
'DBTools:NoError' => '数据库正确',
'DBTools:SQLquery' => 'SQL 查询',
'DBTools:FixitSQLquery' => '用于修复问题的 SQL 查询(说明)',
'DBTools:SQLresult' => 'SQL 结果',
'DBTools:NoError' => '数据库 OK',
'DBTools:HideIds' => '错误列表',
'DBTools:ShowIds' => '详细视图',
'DBTools:ShowReport' => '报告',
'DBTools:IntegrityCheck' => '完整性检查',
'DBTools:FetchCheck' => '提取检查 (耗时长)',
'DBTools:SelectAnalysisType' => '请选择分析类型',
'DBTools:Analyze' => '分析',
'DBTools:Details' => '显示详情',
'DBTools:ShowAll' => '显示所有错误',
'DBTools:Inconsistencies' => '数据库不一致',
'DBTools:DetailedErrorTitle' => '%2$s个错误在类型%1$s: %3$s',
'DBTools:DetailedErrorLimit' => '列表限制为%1$s错误',
'DBAnalyzer-Integrity-OrphanRecord' => '孤立记录位于 `%1$s`, 其应该有副本位于表 `%2$s`',
'DBAnalyzer-Integrity-InvalidExtKey' => '无效的外键%1$s (列: `%2$s.%3$s`)',
'DBAnalyzer-Integrity-MissingExtKey' => '外键丢失%1$s (列: `%2$s.%3$s`)',
'DBAnalyzer-Integrity-InvalidValue' => '无效的值%1$s (列: `%2$s.%3$s`)',
'DBAnalyzer-Integrity-UsersWithoutProfile' => '一些用户账号没有角色',
'DBAnalyzer-Integrity-UsersWithoutProfile' => '有的账号没有角色',
'DBAnalyzer-Integrity-HKInvalid' => '损坏的层级链 `%1$s`',
'DBAnalyzer-Fetch-Count-Error' => '读取计数出错于 `%1$s`, %2$d个记录已读取 / %3$d已',
'DBAnalyzer-Integrity-FinalClass' => '字段 `%2$s`.`%1$s` 必须是相同的值, 而不是 `%3$s`.`%1$s`',
@@ -69,20 +73,25 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
// Lost attachments
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'DBTools:LostAttachments' => '附件缺失',
'DBTools:LostAttachments:Disclaimer' => '可以在此搜索数据库中丢失或错放的附件. 这不是数据恢复工具, 无法恢复已删除的数据.',
'DBTools:LostAttachments:Disclaimer' => '可以在此搜索数据库中丢失或错放的附件. 请注意, 这不是数据恢复工具, 无法恢复已删除的数据.',
'DBTools:LostAttachments:Button:Analyze' => '分析',
'DBTools:LostAttachments:Button:Restore' => '还原',
'DBTools:LostAttachments:Button:Restore:Confirm' => '此操作无法回退, 请确认是否继续还原.',
'DBTools:LostAttachments:Button:Busy' => '请稍候...',
'DBTools:LostAttachments:Step:Analyze' => '首先, 通过分析数据库来搜索丢失或误挪动的附件.',
'DBTools:LostAttachments:Step:AnalyzeResults' => '分析结果:',
'DBTools:LostAttachments:Step:AnalyzeResults:None' => '非常好! 所有附件都是正常的.',
'DBTools:LostAttachments:Step:AnalyzeResults:Some' => '某些附件 (%1$d) 看起来放错了位置. 请检查下面的列表并选择要挪动的文件.',
'DBTools:LostAttachments:Step:AnalyzeResults:Item:Filename' => '文件名',
'DBTools:LostAttachments:Step:AnalyzeResults:Item:CurrentLocation' => '当前位置',
'DBTools:LostAttachments:Step:AnalyzeResults:Item:TargetLocation' => '移动到...',
'DBTools:LostAttachments:Step:RestoreResults' => '还原结果:',
'DBTools:LostAttachments:Step:RestoreResults:Results' => '%1$d/%2$d的附件被还原.',
'DBTools:LostAttachments:StoredAsInlineImage' => '存储为内嵌图像',
'DBTools:LostAttachments:History' => '附件 "%1$s" 已使用数据库工具还原',
]);

View File

@@ -90,6 +90,6 @@ Dict::Add('FR FR', 'French', 'Français', [
'Class:TriggerOnAttachmentDelete' => 'Déclencheur sur la suppression d\'une pièce jointe',
'Class:TriggerOnAttachmentDelete+' => 'Déclencheur sur la suppression d\'une pièce jointe d\'un objet',
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Ajoute le fichier supprimé dans l\'email',
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'If checked, the deleted file will be automatically attached to the email when an email action is triggered~~',
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'Si coché, le fichier supprimé sera automatiquement joint à l\'email quand une action email sera lancée',
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Les Triggers sur les objets ne sont pas autorisés sur la classe Attachement. Veuillez utiliser les triggers spécifiques pour cette classe',
]);

View File

@@ -26,12 +26,12 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'Attachments:NoAttachment' => 'Нет вложений.',
'Attachments:PreviewNotAvailable' => 'Предварительный просмотр не доступен для этого типа вложений.',
'Attachments:Error:FileTooLarge' => 'Файл слишком велик для загрузки. %1$s',
'Attachments:Error:UploadedFileEmpty' => 'The received file is empty and cannot be attached.
Either you have pushed an empty file,
or ask your '.ITOP_APPLICATION_SHORT.' administrator if the '.ITOP_APPLICATION_SHORT.' server disk is full.~~',
'Attachments:Render:Icons' => 'Display as icons~~',
'Attachments:Render:Table' => 'Display as list~~',
'UI:Attachments:DropYourFileHint' => 'Drop files anywhere in this area~~',
'Attachments:Error:UploadedFileEmpty' => 'Полученный файл пуст и не может быть прикреплён.
Либо вы загрузили пустой файл,
либо обратитесь к администратору '.ITOP_APPLICATION_SHORT.' — возможно, диск сервера '.ITOP_APPLICATION_SHORT.' переполнен.',
'Attachments:Render:Icons' => 'Отображать как иконки',
'Attachments:Render:Table' => 'Отображать как список',
'UI:Attachments:DropYourFileHint' => 'Перетащите файлы в любое место этой области',
]);
//
@@ -62,7 +62,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'Attachments:File:Uploader' => 'Пользователь',
'Attachments:File:Size' => 'Размер',
'Attachments:File:MimeType' => 'Тип',
'Attachments:File:DownloadsCount' => 'Downloads~~',
'Attachments:File:DownloadsCount' => 'Скачиваний',
]);
//
// Class: Attachment
@@ -82,15 +82,15 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
//
Dict::Add('RU RU', 'Russian', 'Русский', [
'Class:TriggerOnAttachmentDownload' => 'Trigger (on object\'s attachment download)~~',
'Class:TriggerOnAttachmentDownload+' => 'Trigger on object\'s attachment download of [a child class of] the given class~~',
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment creation)~~',
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment creation~~',
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => 'Add file in email~~',
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'If checked, the file will be automatically attached to the email when an email action is triggered~~',
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment deletion)~~',
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment deletion~~',
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Add deleted file in email~~',
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'If checked, the deleted file will be automatically attached to the email when an email action is triggered~~',
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Trigger on object is not allowed on class Attachment. Please use specific trigger~~',
'Class:TriggerOnAttachmentDownload' => 'Триггер (на скачивание вложения объекта)',
'Class:TriggerOnAttachmentDownload+' => 'Триггер на скачивание вложения объекта заданного класса (или его дочернего класса)',
'Class:TriggerOnAttachmentCreate' => 'Триггер (на создание вложения объекта)',
'Class:TriggerOnAttachmentCreate+' => 'Триггер на создание вложения объекта',
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => 'Добавлять файл в email',
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'Если отмечено, файл будет автоматически прикреплён к письму при срабатывании действия email',
'Class:TriggerOnAttachmentDelete' => 'Триггер (на удаление вложения объекта)',
'Class:TriggerOnAttachmentDelete+' => 'Триггер на удаление вложения объекта',
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Добавлять удалённый файл в email',
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'Если отмечено, удалённый файл будет автоматически прикреплён к письму при срабатывании действия email',
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Триггер на объект не допускается для класса Attachment. Используйте специальный триггер',
]);

View File

@@ -1,15 +1,23 @@
<?php
/**
* Localized data
* Copyright (C) 2013-2024 Combodo SAS
*
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
*
*/
/**
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
*/
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Attachments:TabTitle_Count' => '附件 (%1$d)',
'Attachments:EmptyTabTitle' => '附件',
@@ -39,10 +47,10 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:Attachment' => '附件',
'Class:Attachment+' => '',
'Class:Attachment+' => '文件: 链接到单一对象的文本或图片. 它无法被修改,只能被删除. 附件无法在编辑界面之外创建.',
'Class:Attachment/Attribute:expire' => '过期',
'Class:Attachment/Attribute:expire+' => '',
'Class:Attachment/Attribute:temp_id' => '临时编号',
'Class:Attachment/Attribute:temp_id' => '临时id',
'Class:Attachment/Attribute:temp_id+' => '',
'Class:Attachment/Attribute:item_class' => '项目类型',
'Class:Attachment/Attribute:item_class+' => '',
@@ -69,11 +77,11 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:Attachment/Attribute:creation_date' => '创建日期',
'Class:Attachment/Attribute:creation_date+' => '~~',
'Class:Attachment/Attribute:user_id' => '用户编号',
'Class:Attachment/Attribute:user_id+' => '~~',
'Class:Attachment/Attribute:contact_id' => '联系人编号',
'Class:Attachment/Attribute:contact_id+' => '~~',
'Class:Attachment/Attribute:creation_date+' => '',
'Class:Attachment/Attribute:user_id' => '用户id',
'Class:Attachment/Attribute:user_id+' => '',
'Class:Attachment/Attribute:contact_id' => '联系人id',
'Class:Attachment/Attribute:contact_id+' => '',
]);
//
@@ -81,15 +89,15 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
//
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:TriggerOnAttachmentDownload' => '触发器 (对象附件下载时)',
'Class:TriggerOnAttachmentDownload+' => '触发器于指定类型 [子类型] 对象附件下载时',
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment creation)~~',
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment creation~~',
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => 'Add file in email~~',
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'If checked, the file will be automatically attached to the email when an email action is triggered~~',
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment deletion)~~',
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment deletion~~',
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Add deleted file in email~~',
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'If checked, the deleted file will be automatically attached to the email when an email action is triggered~~',
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Trigger on object is not allowed on class Attachment. Please use specific trigger~~',
'Class:TriggerOnAttachmentDownload' => '触发器 (对象附件下载时)',
'Class:TriggerOnAttachmentDownload+' => '触发器于指定类型 [子类型] 对象附件下载时',
'Class:TriggerOnAttachmentCreate' => '触发器 (对象附件被创建时)',
'Class:TriggerOnAttachmentCreate+' => '触发器 (对象附件被创建时)',
'Class:TriggerOnAttachmentDelete' => '触发器 (对象附件被删除时)',
'Class:TriggerOnAttachmentDelete+' => '触发器 (对象附件被删除时)',
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => '在邮件里添加文件',
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => '如果勾选,文件将在触发邮件操作时自动附加到邮件中',
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => '在邮件里添加已删除的文件',
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => '如果勾选,已删除的文件将在触发邮件操作时自动附加到邮件中',
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => '此触发器不允许用于附件. 请使用特定的触发器',
]);

View File

@@ -20,7 +20,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'bkp-mysqldump-ok' => 'Утилита mysqldump найдена: %1$s',
'bkp-mysqldump-notfound' => 'Утилиту mysqldump найти не удалось: %1$s - пожалуйста, убедитесь в том, что она установлена, и путь до директории с бинарными файлами добавлен в PATH, либо измените параметр mysql_bindir в файле конфигурации.',
'bkp-mysqldump-issue' => 'Утилита mysqldump на может быть запущена (retcode=%1$d) Пожалуйста, убедитесь в том, что она установлена, и путь до директории с бинарными файлами добавлен в PATH, либо измените параметр mysql_bindir в файле конфигурации.',
'bkp-missing-dir' => 'The target directory <code>%1$s</code> could not be found~~',
'bkp-missing-dir' => 'Целевой каталог <code>%1$s</code> не найден',
'bkp-free-disk-space' => '<b>%1$s свободно</b> в <code>%2$s</code>',
'bkp-dir-not-writeable' => '%1$s недоступен для записи',
'bkp-wrong-format-spec' => 'Неправильный формат шаблона названия файлов резервных копий (%1$s). Будет использован шаблон по умолчанию: %2$s',
@@ -38,7 +38,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'bkp-status-backups-manual' => 'Резервное копирование вручную',
'bkp-status-backups-none' => 'Резервных копий ещё нет',
'bkp-next-backup' => 'Следующее резервное копирование будет выполняться в <b>%1$s</b> (%2$s) в %3$s',
'bkp-next-backup-unknown' => 'The next backup is <b>not scheduled</b> yet.~~',
'bkp-next-backup-unknown' => 'Следующее резервное копирование пока <b>не запланировано</b>.',
'bkp-button-backup-now' => 'Запустить сейчас!',
'bkp-button-restore-now' => 'Восстановить!',
'bkp-confirm-backup' => 'Пожалуйста, подтвердите, что вы хотите выполнить резервное копирование прямо сейчас.',

View File

@@ -21,25 +21,28 @@
* 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', '简体中文', [
'bkp-backup-running' => '备份正在进行, 请稍候...',
'bkp-restore-running' => '还原正在进行, 请稍等...',
'Menu:BackupStatus' => '定时备份',
'bkp-status-title' => '定时备份',
'Menu:BackupStatus' => '备份',
'bkp-status-title' => '备份',
'bkp-status-checks' => '设置与检查',
'bkp-mysqldump-ok' => '已找到 mysqldump : %1$s',
'bkp-mysqldump-notfound' => 'mysqldump找不到: %1$s - 请确认它安装在正确的路径, 或者调整'.ITOP_APPLICATION_SHORT.'配置文件的参数mysql_bindir.',
'bkp-mysqldump-issue' => 'mysqldump无法运行 (retcode=%1$d): 请确认它安装在正确的路径, 或者调整'.ITOP_APPLICATION_SHORT.'配置文件的参数mysql_bindir',
'bkp-mysqldump-notfound' => 'mysqldump 找不到: %1$s - 请确认它安装在正确的路径, 或者调整'.ITOP_APPLICATION_SHORT.'配置文件的参数 mysql_bindir.',
'bkp-mysqldump-issue' => 'mysqldump 无法运行 (retcode=%1$d): 请确认它安装在正确的路径, 或者调整'.ITOP_APPLICATION_SHORT.'配置文件的参数 mysql_bindir',
'bkp-missing-dir' => '目标目录<code>%1$s</code>找不到',
'bkp-free-disk-space' => '<b>%1$s可用空间</b>位于<code>%2$s</code>',
'bkp-dir-not-writeable' => '%1$s没有写入权限',
'bkp-wrong-format-spec' => '当前文件名格式错误 (%1$s). 默认格式应该是: %2$s',
'bkp-name-sample' => '备份文件将以数据库名, 日期和时间进行命名. 例如: %1$s',
'bkp-week-days' => '在每个<b>%1$s的%2$s</b>进行备份',
'bkp-retention' => '最多<b>%1$d份备份文件</b>在目标目录.',
'bkp-retention' => '在目标目录中最多保留<b>%1$d份备份文件</b>.',
'bkp-next-to-delete' => '当下一次备份时将被删除 (参阅设置 "retention_count")',
'bkp-table-file' => '文件',
'bkp-table-file+' => '只有扩展名是.zip的文件才被认为是备份文件',
'bkp-table-file+' => '只有扩展名是.zip的文件才被认为是有效的备份文件',
'bkp-table-size' => '大小',
'bkp-table-size+' => '',
'bkp-table-actions' => '操作',
@@ -47,7 +50,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
'bkp-status-backups-auto' => '定时备份',
'bkp-status-backups-manual' => '手动备份',
'bkp-status-backups-none' => '尚未开始备份',
'bkp-next-backup' => '下一次备份将发生在<b>%1$s</b> (%2$s) 的%3$s',
'bkp-next-backup' => '下一次备份将发生在<b>%1$s</b> (%2$s) 的%3$s',
'bkp-next-backup-unknown' => '下一次备份<b>尚未被计划</b>.',
'bkp-button-backup-now' => '立即备份!',
'bkp-button-restore-now' => '还原!',

View File

@@ -21,7 +21,6 @@
use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\FieldSet\FieldSet;
use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Spinner\SpinnerUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory;
@@ -410,8 +409,11 @@ JS
$sEnvironment = addslashes(utils::GetCurrentEnvironment());
$oModalSpinner = SpinnerUIBlockFactory::MakeMedium(null, $sPleaseWaitBackup);
$sModalSpinnerHtml = BlockRenderer::RenderBlockTemplates($oModalSpinner);
$oBackupModalSpinner = SpinnerUIBlockFactory::MakeMedium(null, $sPleaseWaitBackup);
$sBackupModalSpinnerHtml = BlockRenderer::RenderBlockTemplates($oBackupModalSpinner);
$oRestoreModalSpinner = SpinnerUIBlockFactory::MakeMedium(null, $sPleaseWaitRestore);
$sRestoreModalSpinnerHtml = BlockRenderer::RenderBlockTemplates($oRestoreModalSpinner);
$oP->add_script(
<<<JS
@@ -424,7 +426,7 @@ function LaunchBackupNow()
{
const oModal = CombodoModal.OpenModal({
title: '$sBackUpNow',
content: `$sModalSpinnerHtml`
content: `$sBackupModalSpinnerHtml`
});
var oParams = {};
@@ -450,10 +452,10 @@ function LaunchRestoreNow(sBackupFile, sConfirmationMessage)
{
return;
}
const oModal = CombodoModal.OpenModal({
title: '$sRestore',
content: '<i class="ajax-spin fas fa-sync-alt fa-spin"></i> $sPleaseWaitRestore'
content: `$sRestoreModalSpinnerHtml`
});
$('#backup_success').addClass('ibo-is-hidden');

View File

@@ -14,7 +14,7 @@
Dict::Add('RU RU', 'Russian', 'Русский', [
'Class:lnkFunctionalCIToProviderContract' => 'Связь Функциональная КЕ/Договор с поставщиком',
'Class:lnkFunctionalCIToProviderContract+' => '',
'Class:lnkFunctionalCIToProviderContract/Name' => '%1$s / %2$s~~',
'Class:lnkFunctionalCIToProviderContract/Name' => '%1$s / %2$s',
'Class:lnkFunctionalCIToProviderContract/Attribute:providercontract_id' => 'Договор с поставщиком',
'Class:lnkFunctionalCIToProviderContract/Attribute:providercontract_id+' => '',
'Class:lnkFunctionalCIToProviderContract/Attribute:providercontract_name' => 'Договор с поставщиком',
@@ -32,7 +32,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
Dict::Add('RU RU', 'Russian', 'Русский', [
'Class:lnkFunctionalCIToService' => 'Связь Функциональная КЕ/Услуга',
'Class:lnkFunctionalCIToService+' => '',
'Class:lnkFunctionalCIToService/Name' => '%1$s / %2$s~~',
'Class:lnkFunctionalCIToService/Name' => '%1$s / %2$s',
'Class:lnkFunctionalCIToService/Attribute:service_id' => 'Услуга',
'Class:lnkFunctionalCIToService/Attribute:service_id+' => '',
'Class:lnkFunctionalCIToService/Attribute:service_name' => 'Услуга',

View File

@@ -17,10 +17,11 @@
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
* @author Benjamin Planque <benjamin.planque@combodo.com>
* @author Benjamin Planque <benjamin.planque@combodo.com>
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
//////////////////////////////////////////////////////////////////////
// Note: The classes have been grouped by categories: bizmodel
//////////////////////////////////////////////////////////////////////
@@ -31,8 +32,9 @@
//
// Class: lnkFunctionalCIToProviderContract
//
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:lnkFunctionalCIToProviderContract' => '关联功能配置项/供应商合同',
'Class:lnkFunctionalCIToProviderContract' => '链接 功能配置项/供应商合同',
'Class:lnkFunctionalCIToProviderContract+' => '',
'Class:lnkFunctionalCIToProviderContract/Name' => '%1$s / %2$s',
'Class:lnkFunctionalCIToProviderContract/Attribute:providercontract_id' => '供应商合同',
@@ -50,9 +52,9 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
//
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:lnkFunctionalCIToService' => '关联 功能配置项/服务',
'Class:lnkFunctionalCIToService+' => '',
'Class:lnkFunctionalCIToService/Name' => '%1$s / %2$s~~',
'Class:lnkFunctionalCIToService' => '链接 功能配置项/服务',
'Class:lnkFunctionalCIToService+' => 'This link models the functional CIs that are required to deliver a Service and whose malfunction would affect the quality of the Service.~~',
'Class:lnkFunctionalCIToService/Name' => '%1$s / %2$s',
'Class:lnkFunctionalCIToService/Attribute:service_id' => '服务',
'Class:lnkFunctionalCIToService/Attribute:service_id+' => '',
'Class:lnkFunctionalCIToService/Attribute:service_name' => '服务名称',
@@ -80,7 +82,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:Document/Attribute:contracts_list' => '合同',
'Class:Document/Attribute:contracts_list+' => '此文档关的所有合同',
'Class:Document/Attribute:contracts_list+' => '此文档关的所有合同',
'Class:Document/Attribute:services_list' => '服务',
'Class:Document/Attribute:services_list+' => '此文档关的所有服务',
'Class:Document/Attribute:services_list+' => '此文档关的所有服务',
]);

View File

@@ -14,7 +14,7 @@
Dict::Add('RU RU', 'Russian', 'Русский', [
'Class:lnkFunctionalCIToTicket' => 'Связь Функциональная КЕ/Тикет',
'Class:lnkFunctionalCIToTicket+' => '',
'Class:lnkFunctionalCIToTicket/Name' => '%1$s / %2$s~~',
'Class:lnkFunctionalCIToTicket/Name' => '%1$s / %2$s',
'Class:lnkFunctionalCIToTicket/Attribute:ticket_id' => 'Тикет',
'Class:lnkFunctionalCIToTicket/Attribute:ticket_id+' => '',
'Class:lnkFunctionalCIToTicket/Attribute:ticket_ref' => 'Тикет',

View File

@@ -17,10 +17,11 @@
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license https://opensource.org/licenses/AGPL-3.0
* @author Benjamin Planque <benjamin.planque@combodo.com>
*/
* @author Benjamin Planque <benjamin.planque@combodo.com>
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
//////////////////////////////////////////////////////////////////////
// Note: The classes have been grouped by categories: bizmodel
//////////////////////////////////////////////////////////////////////
@@ -28,19 +29,21 @@
// Classes in 'bizmodel'
//////////////////////////////////////////////////////////////////////
//
//
// Class: lnkFunctionalCIToTicket
//
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:lnkFunctionalCIToTicket' => '关联 功能配置项/工单',
'Class:lnkFunctionalCIToTicket+' => '',
'Class:lnkFunctionalCIToTicket' => '链接 功能配置项/工单',
'Class:lnkFunctionalCIToTicket+' => 'This link models the functional CIs that are concerned by a Ticket (such as a User Request, an Incident, a Change, ...)~~',
'Class:lnkFunctionalCIToTicket/Name' => '%1$s / %2$s',
'Class:lnkFunctionalCIToTicket/Attribute:ticket_id' => '工单',
'Class:lnkFunctionalCIToTicket/Attribute:ticket_id+' => '',
'Class:lnkFunctionalCIToTicket/Attribute:ticket_ref' => '工单编号',
'Class:lnkFunctionalCIToTicket/Attribute:ticket_ref+' => '',
'Class:lnkFunctionalCIToTicket/Attribute:ticket_title' => '工单标题',
'Class:lnkFunctionalCIToTicket/Attribute:ticket_title+' => '~~',
'Class:lnkFunctionalCIToTicket/Attribute:ticket_title+' => '',
'Class:lnkFunctionalCIToTicket/Attribute:functionalci_id' => '配置项',
'Class:lnkFunctionalCIToTicket/Attribute:functionalci_id+' => '',
'Class:lnkFunctionalCIToTicket/Attribute:functionalci_name' => '配置项名称',
@@ -50,7 +53,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:lnkFunctionalCIToTicket/Attribute:impact_code' => '影响',
'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:manual' => '手动添加',
'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:computed' => '自动添加',
'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:not_impacted' => '不通知',
'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:not_impacted' => '未被影响',
]);
//

View File

@@ -21,11 +21,11 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', [
'Menu:SearchChanges' => 'Hledat změny',
'Menu:SearchChanges+' => 'Hledat změnové tikety',
'Menu:Change:Shortcuts' => 'Odkazy',
'Menu:Change:Shortcuts+' => '',
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
'Menu:WaitingAcceptance' => 'Změny čekající na přijetí',
'Menu:WaitingAcceptance+' => '',
'Menu:WaitingApproval' => 'Změny čekající na schválení',
'Menu:WaitingApproval+' => '',
'Menu:WaitingApproval+' => 'Changes in planned status~~',
'Menu:Changes' => 'Otevřené změny',
'Menu:Changes+' => 'Všechny otevřené změny',
'Menu:MyChanges' => 'Změny přidělené mně',

View File

@@ -20,11 +20,11 @@ Dict::Add('DA DA', 'Danish', 'Dansk', [
'Menu:SearchChanges' => 'Søg efter Changes',
'Menu:SearchChanges+' => 'Søg efter Change Tickets',
'Menu:Change:Shortcuts' => 'Genveje',
'Menu:Change:Shortcuts+' => '',
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
'Menu:WaitingAcceptance' => 'Changes, som afventer accept',
'Menu:WaitingAcceptance+' => '',
'Menu:WaitingApproval' => 'Changes, som afventer godkendelse',
'Menu:WaitingApproval+' => '',
'Menu:WaitingApproval+' => 'Changes in planned status~~',
'Menu:Changes' => 'Offene Changes',
'Menu:Changes+' => 'Alle åbne Changes',
'Menu:MyChanges' => 'Mine Changes',

View File

@@ -19,8 +19,8 @@ Dict::Add('DE DE', 'German', 'Deutsch', [
'Menu:NewChange+' => 'Einen neuen Change erstellen',
'Menu:SearchChanges' => 'Suche nach Changes',
'Menu:SearchChanges+' => 'Unter den bestehenden Changes suchen',
'Menu:Change:Shortcuts' => 'Shortcuts',
'Menu:Change:Shortcuts+' => '',
'Menu:Change:Shortcuts' => 'Changes~~',
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
'Menu:WaitingAcceptance' => 'Changes, die auf Annahme warten',
'Menu:WaitingAcceptance+' => 'Changes, die auf Annahme warten',
'Menu:WaitingApproval' => 'Changes, die auf Genehmigung warten',

View File

@@ -30,16 +30,16 @@ Dict::Add('EN US', 'English', 'English', [
'Menu:NewChange+' => 'Create a new change ticket',
'Menu:SearchChanges' => 'Search for changes',
'Menu:SearchChanges+' => 'Search for change tickets',
'Menu:Change:Shortcuts' => 'Shortcuts',
'Menu:Change:Shortcuts+' => '',
'Menu:Change:Shortcuts' => 'Changes',
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes',
'Menu:WaitingAcceptance' => 'Changes awaiting acceptance',
'Menu:WaitingAcceptance+' => '',
'Menu:WaitingApproval' => 'Changes awaiting approval',
'Menu:WaitingApproval+' => '',
'Menu:WaitingApproval+' => 'Changes in planned status',
'Menu:Changes' => 'Open changes',
'Menu:Changes+' => 'All open changes',
'Menu:Changes+' => 'All changes which are not in closed status',
'Menu:MyChanges' => 'Changes assigned to me',
'Menu:MyChanges+' => 'Changes assigned to me (as Agent)',
'Menu:MyChanges+' => 'Non-closed changes assigned to me (as an Agent)',
'UI-ChangeManagementOverview-ChangeByCategory-last-7-days' => 'Changes by category for the last 7 days',
'UI-ChangeManagementOverview-Last-7-days' => 'Number of changes for the last 7 days',
'UI-ChangeManagementOverview-ChangeByDomain-last-7-days' => 'Changes by domain for the last 7 days',

View File

@@ -30,16 +30,16 @@ Dict::Add('EN GB', 'British English', 'British English', [
'Menu:NewChange+' => 'Create a new change ticket',
'Menu:SearchChanges' => 'Search for changes',
'Menu:SearchChanges+' => 'Search for change tickets',
'Menu:Change:Shortcuts' => 'Shortcuts',
'Menu:Change:Shortcuts+' => '',
'Menu:Change:Shortcuts' => 'Changes~~',
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
'Menu:WaitingAcceptance' => 'Changes awaiting acceptance',
'Menu:WaitingAcceptance+' => '',
'Menu:WaitingApproval' => 'Changes awaiting approval',
'Menu:WaitingApproval+' => '',
'Menu:WaitingApproval+' => 'Changes in planned status~~',
'Menu:Changes' => 'Open changes',
'Menu:Changes+' => 'All open changes',
'Menu:Changes+' => 'All changes which are not in closed status',
'Menu:MyChanges' => 'Changes assigned to me',
'Menu:MyChanges+' => 'Changes assigned to me (as Agent)',
'Menu:MyChanges+' => 'Non-closed changes assigned to me (as an Agent)',
'UI-ChangeManagementOverview-ChangeByCategory-last-7-days' => 'Changes by category for the last 7 days',
'UI-ChangeManagementOverview-Last-7-days' => 'Number of changes for the last 7 days',
'UI-ChangeManagementOverview-ChangeByDomain-last-7-days' => 'Changes by domain for the last 7 days',

View File

@@ -18,16 +18,16 @@ Dict::Add('FR FR', 'French', 'Français', [
'Menu:NewChange+' => 'Créer un nouveau ticket de changement',
'Menu:SearchChanges' => 'Rechercher des changements',
'Menu:SearchChanges+' => 'Rechercher parmi les tickets de changement',
'Menu:Change:Shortcuts' => 'Raccourcis',
'Menu:Change:Shortcuts+' => '',
'Menu:WaitingAcceptance' => 'Changements en attente d\'acceptance',
'Menu:WaitingAcceptance+' => 'Changements en attente d\'acceptance',
'Menu:WaitingApproval' => 'Changements en attente d\'approbation',
'Menu:WaitingApproval+' => 'Changements en attente d\'approbation',
'Menu:Changes' => 'Changements ouverts',
'Menu:Changes+' => 'Tickets de changement ouverts',
'Menu:MyChanges' => 'Mes changements',
'Menu:MyChanges+' => 'Tickets de changement qui me sont assignés',
'Menu:Change:Shortcuts' => 'Changements',
'Menu:Change:Shortcuts+' => 'Raccourcis vers différents groupes de changements',
'Menu:WaitingAcceptance' => 'En attente de validation',
'Menu:WaitingAcceptance+' => 'Changements à l\'état nouveau, en attente d\'acceptation',
'Menu:WaitingApproval' => 'En attente d\'approbation',
'Menu:WaitingApproval+' => 'Changements dans l\'état programmé, en attente d\'approbation',
'Menu:Changes' => 'En cours',
'Menu:Changes+' => 'Tickets de changement qui ne sont pas dans l\'état fermé',
'Menu:MyChanges' => 'Qui me sont assignés',
'Menu:MyChanges+' => 'Changements ni fermés, ni résolus, dont je suis l\'agent assigné',
'UI-ChangeManagementOverview-ChangeByCategory-last-7-days' => 'Changements par catégorie',
'UI-ChangeManagementOverview-Last-7-days' => 'Changements par jour',
'UI-ChangeManagementOverview-ChangeByDomain-last-7-days' => 'Changements par domaine',

View File

@@ -19,11 +19,11 @@ Dict::Add('IT IT', 'Italian', 'Italiano', [
'Menu:SearchChanges' => 'Cerca per cambi',
'Menu:SearchChanges+' => 'Cerca i cambi per tickets',
'Menu:Change:Shortcuts' => 'Scorciatoie',
'Menu:Change:Shortcuts+' => '',
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
'Menu:WaitingAcceptance' => 'Modifiche in attesa di accettazione',
'Menu:WaitingAcceptance+' => '',
'Menu:WaitingApproval' => 'Modifiche in attesa di approvazione',
'Menu:WaitingApproval+' => '',
'Menu:WaitingApproval+' => 'Changes in planned status~~',
'Menu:Changes' => 'Modifiche aperte',
'Menu:Changes+' => 'Tutte le Modifiche aperte',
'Menu:MyChanges' => 'Modifiche assegnate a me',

View File

@@ -19,13 +19,13 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
'Menu:SearchChanges' => '変更検索',
'Menu:SearchChanges+' => '変更チケット検索',
'Menu:Change:Shortcuts' => 'ショートカット',
'Menu:Change:Shortcuts+' => '',
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
'Menu:WaitingAcceptance' => '受理待ちの変更',
'Menu:WaitingAcceptance+' => '',
'Menu:WaitingApproval' => '承認待ちの変更',
'Menu:WaitingApproval+' => '',
'Menu:WaitingApproval+' => 'Changes in planned status~~',
'Menu:Changes' => 'オープン状態の変更',
'Menu:Changes+' => '',
'Menu:Changes+' => 'All changes which are not in closed status~~',
'Menu:MyChanges' => '担当している変更',
'Menu:MyChanges+' => '担当している変更(エージェントとして)',
'UI-ChangeManagementOverview-ChangeByCategory-last-7-days' => '最近7日間のカテゴリ別の変更',

View File

@@ -21,11 +21,11 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', [
'Menu:SearchChanges' => 'Zoek naar changes',
'Menu:SearchChanges+' => 'Zoek naar changes',
'Menu:Change:Shortcuts' => 'Snelkoppelingen',
'Menu:Change:Shortcuts+' => '',
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
'Menu:WaitingAcceptance' => 'Changes die acceptatie vereisen',
'Menu:WaitingAcceptance+' => '',
'Menu:WaitingApproval' => 'Changes die goedkeuring vereisen',
'Menu:WaitingApproval+' => '',
'Menu:WaitingApproval+' => 'Changes in planned status~~',
'Menu:Changes' => 'Open changes',
'Menu:Changes+' => 'Alle open changes',
'Menu:MyChanges' => 'Changes toegewezen aan mij',

View File

@@ -19,11 +19,11 @@ Dict::Add('PL PL', 'Polish', 'Polski', [
'Menu:SearchChanges' => 'Szukaj zmian',
'Menu:SearchChanges+' => 'Szukaj zgłoszeń zmian',
'Menu:Change:Shortcuts' => 'Skróty',
'Menu:Change:Shortcuts+' => '',
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
'Menu:WaitingAcceptance' => 'Zmiany do akceptacji',
'Menu:WaitingAcceptance+' => '',
'Menu:WaitingApproval' => 'Zmiany do zatwierdzenia',
'Menu:WaitingApproval+' => '',
'Menu:WaitingApproval+' => 'Changes in planned status~~',
'Menu:Changes' => 'Otwarte zmiany',
'Menu:Changes+' => 'Wszystkie otwarte zmiany',
'Menu:MyChanges' => 'Zmiany przypisane do mnie',

View File

@@ -19,13 +19,13 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', [
'Menu:SearchChanges' => 'Pesquisar por mudanças',
'Menu:SearchChanges+' => 'Pesquisar por solicitações de mudança',
'Menu:Change:Shortcuts' => 'Atalhos',
'Menu:Change:Shortcuts+' => '',
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
'Menu:WaitingAcceptance' => 'Mudanças aguardando aceitação',
'Menu:WaitingAcceptance+' => '',
'Menu:WaitingApproval' => 'Mudanças aguardando aprovação',
'Menu:WaitingApproval+' => '',
'Menu:WaitingApproval+' => 'Changes in planned status~~',
'Menu:Changes' => 'Mudanças abertas',
'Menu:Changes+' => '',
'Menu:Changes+' => 'All changes which are not in closed status~~',
'Menu:MyChanges' => 'Mudanças atribuídas a mim',
'Menu:MyChanges+' => 'Mudanças atribuídas a mim (como Agente)',
'UI-ChangeManagementOverview-ChangeByCategory-last-7-days' => 'Mudanças por categoria nos últimos 7 dias',

View File

@@ -23,11 +23,11 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', [
'Menu:WaitingAcceptance' => 'Zmeny očakávajúce prijatie',
'Menu:WaitingAcceptance+' => '~~',
'Menu:WaitingApproval' => 'Zmeny očakávajúce schválenie',
'Menu:WaitingApproval+' => '~~',
'Menu:WaitingApproval+' => 'Changes in planned status~~',
'Menu:Changes' => 'Otvorené zmeny',
'Menu:Changes+' => 'All open changes~~',
'Menu:Changes+' => 'All changes which are not in closed status~~',
'Menu:MyChanges' => 'Zmeny pridelené mne',
'Menu:MyChanges+' => 'Changes assigned to me (as Agent)~~',
'Menu:MyChanges+' => 'Non-closed changes assigned to me (as an Agent)~~',
'UI-ChangeManagementOverview-ChangeByCategory-last-7-days' => 'Zmeny podľa kategórie za posledných 7 dní',
'UI-ChangeManagementOverview-Last-7-days' => 'Počet zmien za posledných 7 dní',
'UI-ChangeManagementOverview-ChangeByDomain-last-7-days' => 'Zmeny podľa domény za posledných 7 dní',

View File

@@ -20,13 +20,13 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', [
'Menu:SearchChanges' => 'Değişiklik ara',
'Menu:SearchChanges+' => 'Değişiklik isteği ara',
'Menu:Change:Shortcuts' => 'Kısayollar',
'Menu:Change:Shortcuts+' => '',
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
'Menu:WaitingAcceptance' => 'Kabul bekleyen değişiklik talepleri',
'Menu:WaitingAcceptance+' => '',
'Menu:WaitingApproval' => 'Onay bekleyen değişiklik talepleri',
'Menu:WaitingApproval+' => '',
'Menu:WaitingApproval+' => 'Changes in planned status~~',
'Menu:Changes' => 'Açık değişiklikler',
'Menu:Changes+' => '',
'Menu:Changes+' => 'All changes which are not in closed status~~',
'Menu:MyChanges' => 'Bana atanan değişiklik istekleri',
'Menu:MyChanges+' => 'Bana atanan değişiklik istekleri',
'UI-ChangeManagementOverview-ChangeByCategory-last-7-days' => 'Son 7 gün için kategoriye göre değişiklikler',

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