Compare commits

..

206 Commits

Author SHA1 Message Date
odain
7f2b7afc5c N°9875 - code style 2026-08-18 17:16:19 +02:00
odain
c3412dd352 N°9875 - Error deprecated on component version object - use ZipArchive boilerplate methods 2026-08-18 17:04:03 +02:00
odain
ff21eb3bc9 N°9875 - Error deprecated on component version object - propose boilerplate methods to handle ZipArchive 2026-08-18 17:03:07 +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
817 changed files with 30382 additions and 11196 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
@@ -3986,6 +3813,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 +3983,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 +4344,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

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

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

@@ -29,6 +29,7 @@ use ScssPhp\ScssPhp\Compiler;
use ScssPhp\ScssPhp\OutputStyle;
use ScssPhp\ScssPhp\ValueConverter;
use Soundasleep\Html2Text;
use ZipArchive;
/**
* Static class utils
@@ -1886,7 +1887,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 +3023,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 +3037,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 +3250,47 @@ TXT
return (int)$sLimit;
}
}
/**
* 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 to open zip file due to inconsistent or empty content'));
}
throw new \Exception(sprintf('Cannot to open zip file due to error code %s', $code));
}
return $oZip;
}
/**
* @param string $sDirectory
* @param string $sPrefix
* @return array
* @throws \Exception
*/
public static function ZipArchiveOpenWithTempNam(string $sDirectory, string $sPrefix): array
{
$sTempnam = tempnam($sDirectory, $sPrefix);
unlink($sTempnam);
$sArchiveName = $sTempnam.'.zip';
;
return [ self::ZipArchiveOpen($sArchiveName, \ZipArchive::CREATE), $sArchiveName ];
}
}

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

@@ -20,16 +20,6 @@
*
*/
use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
use PhpParser\Comment;
use PhpParser\Error;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\Variable;
use PhpParser\ParserFactory;
define('ITOP_APPLICATION', 'iTop');
define('ITOP_APPLICATION_SHORT', 'iTop');
@@ -94,7 +84,7 @@ define('DEFAULT_HASH_ALGO', PASSWORD_DEFAULT);
* @see utils::GetConfig() to load config from the current env, if metamodel is not loaded
* @package iTopORM
*/
#[AllowDynamicProperties] class Config
class Config
{
//protected $m_bIsLoaded = false;
protected $m_sFile = '';
@@ -2020,24 +2010,17 @@ define('DEFAULT_HASH_ALGO', PASSWORD_DEFAULT);
*/
protected $m_sAppSecret;
private bool $bPreserveComments;
private bool $bLegacyEvaluation;
/**
* Config constructor.
*
* @param string|null $sConfigFile
* @param bool $bLoadConfig
* @param bool $bPreserveComments
* @param bool $bLegacyEvaluation
*
* @throws \ConfigException
* @throws \CoreException
*/
public function __construct($sConfigFile = null, $bLoadConfig = true, bool $bPreserveComments = false, bool $bLegacyEvaluation = false)
public function __construct($sConfigFile = null, $bLoadConfig = true)
{
$this->bPreserveComments = $bPreserveComments;
$this->bLegacyEvaluation = $bLegacyEvaluation;
$this->oConfigPlaceholdersResolver = new ConfigPlaceholdersResolver();
$this->m_sFile = $sConfigFile;
@@ -2092,7 +2075,6 @@ define('DEFAULT_HASH_ALGO', PASSWORD_DEFAULT);
}
$this->Set('app_root_url', $sAppRootUrl);
*/
}
/**
@@ -2139,39 +2121,9 @@ define('DEFAULT_HASH_ALGO', PASSWORD_DEFAULT);
// So, I've implemented a solution suggested in the PHP doc (search for phpWrapper)
try {
ob_start();
/*file_put_contents(APPROOT . '/bugged-conf.php', '?'.'>'.trim($sConfigCode));
$e = new \Exception('');
var_dump($e->getTraceAsString());*/
eval('?'.'>'.trim($sConfigCode));
$sNoise = trim(ob_get_contents());
ob_end_clean();
if ($this->bPreserveComments || ! $this->bLegacyEvaluation) {
try {
$oParser = (new ParserFactory())->createForNewestSupportedVersion();
foreach ($oParser->parse($sConfigCode) as $oNode) {
if ($oNode instanceof \PhpParser\Node\Stmt\Expression) {
/** @var \PhpParser\Node\Stmt\Expression $oNode */
$oExpr = $oNode->expr;
if ($oExpr instanceof Assign) {
/** @var Assign $oExpr */
$oVar = $oExpr->var;
if ($oVar instanceof Variable && $oVar->name === "MyModuleSettings") {
if ($oExpr->expr instanceof Array_) {
$oPhpExpressionEvaluator = new PhpExpressionEvaluator();
if (! $this->bLegacyEvaluation) {
$aArrayWithComments = $oPhpExpressionEvaluator->GetArray($oExpr->expr, $this->bPreserveComments);
$MyModuleSettings = array_replace_recursive($aArrayWithComments, $MyModuleSettings);
}
}
}
}
}
}
} catch (Error $e) {
var_dump($e);
}
}
} catch (Error $e) {
// PHP 7
throw new ConfigException(
@@ -2762,15 +2714,6 @@ define('DEFAULT_HASH_ALGO', PASSWORD_DEFAULT);
foreach ($this->m_aModuleSettings as $sModule => $aProperties) {
fwrite($hFile, "\t'$sModule' => array (\n");
foreach ($aProperties as $sProperty => $value) {
if (is_string($value) && false !== strpos($value, 'PhpParserComment')) {
$value = preg_replace(
["/.*StartPhpParserComment/", "/EndPhpParserComment/"],
['', ''],
$value
);
fwrite($hFile, "\t\t$value\n");
continue;
}
$sNiceExport = self::PrettyVarExport($this->oItopConfigParser->GetVarValue('MyModuleSettings', $sProperty), $value, "\t\t");
fwrite($hFile, "\t\t'$sProperty' => $sNiceExport,\n");
}
@@ -2940,7 +2883,7 @@ define('DEFAULT_HASH_ALGO', PASSWORD_DEFAULT);
}
/**
* Pretty format a var_export'ed value so that (if possible) the indentation is preserved on every line
* Pretty format a var_export'ed value so that (if possible) the identation is preserved on every line
*
* @param array $aParserValue
* @param mixed $value The value to export
@@ -2957,19 +2900,12 @@ define('DEFAULT_HASH_ALGO', PASSWORD_DEFAULT);
}
$sExport = var_export($value, true);
if (strpos($sExport, 'PhpParserComment')) {
$sExport = preg_replace(
["/.*StartPhpParserComment/", "/EndPhpParserComment',/"],
['', ''],
$sExport
);
}
$sNiceExport = str_replace(["\r\n", "\n", "\r"], "\n".$sIndentation, trim($sExport));
if (!$bForceIndentation) {
/** @var array $aImported */
$aImported = null;
eval('$aImported='.$sNiceExport.';');
// Check if adding the indentations at the beginning of each line
// Check if adding the identations at the beginning of each line
// did not modify the values (in case of a string containing a line break)
if ($aImported != $value) {
$sNiceExport = $sExport;
@@ -2978,6 +2914,7 @@ define('DEFAULT_HASH_ALGO', PASSWORD_DEFAULT);
return $sNiceExport;
}
}
class ConfigPlaceholdersResolver

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';
@@ -4660,12 +4659,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 +5761,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

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

View File

@@ -21,29 +21,30 @@
* 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', '简体中文', [
'Menu:ChangeManagement' => '变更管理',
'Menu:Change:Overview' => '概况',
'Menu:Change:Overview+' => '',
'Menu:NewChange' => '新建变更',
'Menu:NewChange+' => '新建变更',
'Menu:NewChange+' => '新建变更工单',
'Menu:SearchChanges' => '搜索变更',
'Menu:SearchChanges+' => '搜索变更',
'Menu:Change:Shortcuts' => '快捷方式',
'Menu:Change:Shortcuts+' => '',
'Menu:SearchChanges+' => '搜索变更工单',
'Menu:Change:Shortcuts' => '变更',
'Menu:Change:Shortcuts+' => '快速访问预定义的变更数据',
'Menu:WaitingAcceptance' => '等待审核的变更',
'Menu:WaitingAcceptance+' => '',
'Menu:WaitingApproval' => '等待批准的变更',
'Menu:WaitingApproval+' => '',
'Menu:Changes' => '所有打开的变更',
'Menu:Changes+' => '所有打开的变更',
'Menu:WaitingApproval+' => '处于计划状态的变更',
'Menu:Changes' => '所有待处理的变更',
'Menu:Changes+' => '所有待处理的变更',
'Menu:MyChanges' => '分配给我的变更',
'Menu:MyChanges+' => '分配给我的变更 (作为办理人)',
'UI-ChangeManagementOverview-ChangeByCategory-last-7-days' => '最近一周的变更 (按类型)',
'UI-ChangeManagementOverview-Last-7-days' => '最近一周的变更 (按数量)',
'UI-ChangeManagementOverview-ChangeByDomain-last-7-days' => '最近一周的变更 (按范围)',
'UI-ChangeManagementOverview-ChangeByStatus-last-7-days' => '最近一周的变更 (按状态)',
'Tickets:Related:OpenChanges' => '打开的变更',
'Tickets:Related:OpenChanges' => '待处理的变更',
'Tickets:Related:RecentChanges' => '最近的变更 (72小时)',
]);
@@ -125,13 +126,13 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:Change/Attribute:parent_name' => '变更编号',
'Class:Change/Attribute:parent_name+' => '',
'Class:Change/Attribute:related_request_list' => '相关需求',
'Class:Change/Attribute:related_request_list+' => '此变更相关的所有用户需求',
'Class:Change/Attribute:related_request_list+' => '此变更相关的所有需求',
'Class:Change/Attribute:related_problems_list' => '相关问题',
'Class:Change/Attribute:related_problems_list+' => '此变更相关的所有问题',
'Class:Change/Attribute:related_incident_list' => '相关事件',
'Class:Change/Attribute:related_incident_list+' => '此变更相关的所有事件',
'Class:Change/Attribute:child_changes_list' => '子变更',
'Class:Change/Attribute:child_changes_list+' => '此变更相关的变更',
'Class:Change/Attribute:child_changes_list+' => '此变更相关的变更',
'Class:Change/Attribute:parent_id_friendlyname' => '父级变更昵称',
'Class:Change/Attribute:parent_id_friendlyname+' => '',
'Class:Change/Attribute:parent_id_finalclass_recall' => '变更类型',

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

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