mirror of
https://github.com/Combodo/iTop.git
synced 2026-08-21 13:48:22 +02:00
Compare commits
343 Commits
feature/91
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b919adf4d8 | ||
|
|
4ebd03eebb | ||
|
|
4f891a9170 | ||
|
|
7583ede3b3 | ||
|
|
95cb21cff7 | ||
|
|
cdd7bbedcb | ||
|
|
ea1ed03d0c | ||
|
|
2073e8c10c | ||
|
|
1a41349d7b | ||
|
|
fdc1ef40e6 | ||
|
|
4722551a16 | ||
|
|
a2934ef05b | ||
|
|
1cdc2c5648 | ||
|
|
31eddad029 | ||
|
|
b43d71f324 | ||
|
|
26033774b8 | ||
|
|
27c0fe191b | ||
|
|
fac132f707 | ||
|
|
6f9a4cb55f | ||
|
|
c07317dc71 | ||
|
|
a3fcc6fdf0 | ||
|
|
b13065f23f | ||
|
|
2eaca40632 | ||
|
|
7f3a1811d7 | ||
|
|
87b48c638c | ||
|
|
54a1221e4a | ||
|
|
97c589c440 | ||
|
|
68e799c0fd | ||
|
|
658825b308 | ||
|
|
af08824866 | ||
|
|
8693951aa9 | ||
|
|
a28e481ee4 | ||
|
|
6032628a70 | ||
|
|
2892949e33 | ||
|
|
ba643ba2ab | ||
|
|
3711e8a5a1 | ||
|
|
a38fef5bbf | ||
|
|
23e4ce7263 | ||
|
|
1bba486ee8 | ||
|
|
801090fddb | ||
|
|
1d77b9faa2 | ||
|
|
5282913791 | ||
|
|
36fae27355 | ||
|
|
812f0b573d | ||
|
|
c7c16f091b | ||
|
|
3739067473 | ||
|
|
fd553b86dd | ||
|
|
d7625d9650 | ||
|
|
893a1d7f71 | ||
|
|
fda1d3dd16 | ||
|
|
61464fc819 | ||
|
|
c281677768 | ||
|
|
e280251623 | ||
|
|
f657308137 | ||
|
|
4596404666 | ||
|
|
64394099f7 | ||
|
|
67e2899a60 | ||
|
|
8d75531d95 | ||
|
|
1e023e5431 | ||
|
|
e2a1c3e79e | ||
|
|
c7570d62c1 | ||
|
|
a30ed63a62 | ||
|
|
dd77124b07 | ||
|
|
9bfe0430f5 | ||
|
|
3fe91ed0b4 | ||
|
|
ccb9633226 | ||
|
|
82b50dc6cd | ||
|
|
d3451f8c30 | ||
|
|
b5c8a98695 | ||
|
|
1403914572 | ||
|
|
b976471a0d | ||
|
|
fa66a09104 | ||
|
|
47981f78e3 | ||
|
|
476ec75a2e | ||
|
|
10c000ddfe | ||
|
|
7e55b9ccfc | ||
|
|
5bb288ffca | ||
|
|
56d7e40f4c | ||
|
|
e0dc1fa571 | ||
|
|
d6466c0bd6 | ||
|
|
eb16e6cbc8 | ||
|
|
42007ffd95 | ||
|
|
6329c157e8 | ||
|
|
9ee0548767 | ||
|
|
620a6b9a51 | ||
|
|
968c56745a | ||
|
|
04a5e5463a | ||
|
|
006dd3d6c2 | ||
|
|
a808e89c30 | ||
|
|
c6e50a0ed7 | ||
|
|
d88687527e | ||
|
|
2cc071b07a | ||
|
|
76a410e00d | ||
|
|
c68887c2bc | ||
|
|
837b433622 | ||
|
|
52fca750b0 | ||
|
|
a5c14e4870 | ||
|
|
0e72b090ee | ||
|
|
d1aedee245 | ||
|
|
1b70b4bf70 | ||
|
|
41f51a2c0c | ||
|
|
73ede13443 | ||
|
|
40dd72ef82 | ||
|
|
c8aea56171 | ||
|
|
041437cbeb | ||
|
|
85d50f31bd | ||
|
|
5715e0484f | ||
|
|
109fdfa06b | ||
|
|
3c301affb3 | ||
|
|
147fae7f8c | ||
|
|
a5aa8c279f | ||
|
|
13fad9efc1 | ||
|
|
9fbc53cf00 | ||
|
|
04f91ab4a2 | ||
|
|
918f110dc9 | ||
|
|
8e0246601a | ||
|
|
bd9cc9c15e | ||
|
|
60ce21509f | ||
|
|
e6cdcd0a5a | ||
|
|
2ab3081926 | ||
|
|
5cabac63e7 | ||
|
|
3918dcf830 | ||
|
|
489300552a | ||
|
|
8507c606ba | ||
|
|
128989450b | ||
|
|
1bd404af6f | ||
|
|
6c770d3e16 | ||
|
|
a74dbb3a73 | ||
|
|
b365aa0971 | ||
|
|
56aa50c9b1 | ||
|
|
eacb7f3401 | ||
|
|
b14045af29 | ||
|
|
7321eeb23f | ||
|
|
05b8c529fe | ||
|
|
a68c017225 | ||
|
|
82b52b95a1 | ||
|
|
556f612f7e | ||
|
|
edc69bebba | ||
|
|
dce1994b85 | ||
|
|
fde6527057 | ||
|
|
0de0592c18 | ||
|
|
c9d512cc31 | ||
|
|
a3ef049063 | ||
|
|
87ec702c5e | ||
|
|
127be1933d | ||
|
|
2c14030fc5 | ||
|
|
7825c08069 | ||
|
|
4ddf17233f | ||
|
|
9493bb03d5 | ||
|
|
8f87c759b5 | ||
|
|
348c90145d | ||
|
|
c37143176a | ||
|
|
0c8229a68e | ||
|
|
fda5fe5ded | ||
|
|
6b396adeea | ||
|
|
c448f8411c | ||
|
|
4b4fe55060 | ||
|
|
dedcd4af19 | ||
|
|
0556a03839 | ||
|
|
64df429715 | ||
|
|
9ae813db2b | ||
|
|
768423a6aa | ||
|
|
921e7c88fe | ||
|
|
d6ef4fb7bb | ||
|
|
a335e1004b | ||
|
|
e882df96d9 | ||
|
|
3a7439e970 | ||
|
|
7fca6424ab | ||
|
|
f09a73c45a | ||
|
|
5a174dd8fa | ||
|
|
01f32086ec | ||
|
|
5c59fdebaf | ||
|
|
363d0767d5 | ||
|
|
5202adb902 | ||
|
|
f77dd7b668 | ||
|
|
f1af3d5c5f | ||
|
|
614d3586c8 | ||
|
|
f9dc3ee73a | ||
|
|
1d9e7f9b55 | ||
|
|
99a9c8ef40 | ||
|
|
ff2357473b | ||
|
|
9a93bcb06f | ||
|
|
9b4f3290e8 | ||
|
|
67792155dd | ||
|
|
a23b9aa596 | ||
|
|
017481af21 | ||
|
|
4f70441618 | ||
|
|
fb6a332bd0 | ||
|
|
a9af4fc060 | ||
|
|
6a8dc159c1 | ||
|
|
2930a122c6 | ||
|
|
75433d3390 | ||
|
|
d4d6fa149d | ||
|
|
207ac373d0 | ||
|
|
0240d12007 | ||
|
|
1123a83490 | ||
|
|
8ccf832bdd | ||
|
|
caa1b8652d | ||
|
|
ed1cfc3b08 | ||
|
|
dd6b03cccc | ||
|
|
3e38e349e5 | ||
|
|
65b58ec4e2 | ||
|
|
6a20e36434 | ||
|
|
3805a322c1 | ||
|
|
0bf773a23c | ||
|
|
28a09068a0 | ||
|
|
db7fd6b3c0 | ||
|
|
cf76c23ac6 | ||
|
|
b36b0de93c | ||
|
|
a95125e8bf | ||
|
|
6e536e7dbe | ||
|
|
733eea8a78 | ||
|
|
f48efbbd03 | ||
|
|
1a57a427c0 | ||
|
|
a419914236 | ||
|
|
3426b92768 | ||
|
|
7c44c94de1 | ||
|
|
4371ddfbbd | ||
|
|
66bf1dc6fa | ||
|
|
6999000d69 | ||
|
|
eedcdf32fe | ||
|
|
2321d89981 | ||
|
|
3bf47d1e8c | ||
|
|
feb89650e4 | ||
|
|
b672dfc9f2 | ||
|
|
210e2cefca | ||
|
|
07afcdcec6 | ||
|
|
24fa5d3bbd | ||
|
|
65de991853 | ||
|
|
a1a82a02c8 | ||
|
|
6266d4ce15 | ||
|
|
367ae456eb | ||
|
|
c80dbf9bb4 | ||
|
|
683b075bf5 | ||
|
|
cbbcc489e0 | ||
|
|
406f688394 | ||
|
|
06bff3f9e4 | ||
|
|
7c49909dd9 | ||
|
|
51180c9529 | ||
|
|
643ec3bc43 | ||
|
|
b3f984b0e1 | ||
|
|
cbc02dce52 | ||
|
|
f236dd6c4d | ||
|
|
955ce5fc2c | ||
|
|
e7ce3fcf8a | ||
|
|
c156c09be0 | ||
|
|
3df12bba46 | ||
|
|
0912cc21cf | ||
|
|
3aa59ae0b7 | ||
|
|
a5b5d22e2f | ||
|
|
08a3044cd1 | ||
|
|
ba2af7ed63 | ||
|
|
00f1c7498d | ||
|
|
a4345d0311 | ||
|
|
77a820105f | ||
|
|
03e1806f9a | ||
|
|
eef06ee032 | ||
|
|
d6ce202fa8 | ||
|
|
b6c48f4bba | ||
|
|
e9dd2dc3e6 | ||
|
|
ba6cc9dcba | ||
|
|
1c38d989e4 | ||
|
|
fb7a38c83f | ||
|
|
b29a3b5df8 | ||
|
|
48e6203869 | ||
|
|
e65542f978 | ||
|
|
ab6c50d52d | ||
|
|
51e7ef32dc | ||
|
|
829857ec85 | ||
|
|
29502a4297 | ||
|
|
c5b678b4cf | ||
|
|
5ac675a587 | ||
|
|
769afb2715 | ||
|
|
cb46889e92 | ||
|
|
b529a61bc5 | ||
|
|
2fe487df3c | ||
|
|
ae1f9391f9 | ||
|
|
c501280f53 | ||
|
|
f4a1aca3f9 | ||
|
|
e662370c32 | ||
|
|
3c39c6d8d1 | ||
|
|
a7d0262b21 | ||
|
|
c56617abf5 | ||
|
|
4d2722654a | ||
|
|
73ebf663d6 | ||
|
|
99a5b47899 | ||
|
|
ff94639a61 | ||
|
|
7676115725 | ||
|
|
43ceaeb5a3 | ||
|
|
7cac280b83 | ||
|
|
109aa03901 | ||
|
|
80813dba22 | ||
|
|
da282e6c24 | ||
|
|
c4eaeeeb5f | ||
|
|
e605b147d3 | ||
|
|
bfcff9b9c8 | ||
|
|
cd9a755162 | ||
|
|
841ea9bb11 | ||
|
|
4582256f01 | ||
|
|
8de5c2525b | ||
|
|
de4ec9d9ce | ||
|
|
ea35ad48d2 | ||
|
|
90b7c3ac6a | ||
|
|
8f2c990065 | ||
|
|
03437c3453 | ||
|
|
1113788d84 | ||
|
|
0fc7434794 | ||
|
|
eee57b25fa | ||
|
|
7be19c713b | ||
|
|
42fdffe25b | ||
|
|
309f8f8727 | ||
|
|
16108a80fc | ||
|
|
befe294986 | ||
|
|
d271dda54f | ||
|
|
04bb07fc59 | ||
|
|
33a51b47bf | ||
|
|
7e0e357713 | ||
|
|
1772a9f73b | ||
|
|
ed45c3bbb0 | ||
|
|
a49ebd531e | ||
|
|
c5ca5f9176 | ||
|
|
e768cc1c0c | ||
|
|
12b1339542 | ||
|
|
278254c955 | ||
|
|
f6741a6306 | ||
|
|
19386191aa | ||
|
|
d76752a988 | ||
|
|
8c2c4e5ff0 | ||
|
|
4bc28ea38a | ||
|
|
805087a01b | ||
|
|
1ec8aca12d | ||
|
|
f439490bfc | ||
|
|
39011faedd | ||
|
|
46f8f5faeb | ||
|
|
170d24d0ad | ||
|
|
9683b8d96a | ||
|
|
a6171c896b | ||
|
|
89231976f8 | ||
|
|
4e24ac508d | ||
|
|
e1c804139f | ||
|
|
f0a95cbb3d | ||
|
|
265acb4a1e | ||
|
|
30f720b9ad |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
BIN
.doc/contributing-guide/contributing-stickers-side-by-side.png
Normal file
BIN
.doc/contributing-guide/contributing-stickers-side-by-side.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
@@ -2,12 +2,17 @@
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'themeVariables': {
|
||||
'git0': 'lawngreen',
|
||||
'git3': 'dodgerblue',
|
||||
'git4': 'grey',
|
||||
'git5': 'grey',
|
||||
'git6': 'grey',
|
||||
'git7': 'grey'
|
||||
'git0': '#7ccf00',
|
||||
'git1': '#7ccf00',
|
||||
'git2': '#99a1af',
|
||||
'git3': '#99a1af',
|
||||
'git4': '#99a1af',
|
||||
'git5': '#99a1af',
|
||||
'git6': '#99a1af',
|
||||
'git7': '#99a1af',
|
||||
'git8': '#99a1af',
|
||||
'git9': '#99a1af',
|
||||
'git10': '#99a1af'
|
||||
}, 'gitGraph': {'showBranches': true,'mainBranchName': 'develop','rotateCommitLabel': true}} }%%
|
||||
gitGraph
|
||||
commit id: "2016-07-06" tag: "2.3.0" type: HIGHLIGHT
|
||||
@@ -86,24 +91,30 @@ gitGraph
|
||||
commit id: "2024-01-17a" tag: "2.7.10"
|
||||
checkout support/3.0
|
||||
commit id: "2024-01-17b" tag: "3.0.4"
|
||||
checkout support/2.7
|
||||
commit id: "2024-09-28" tag: "2.7.11"
|
||||
checkout support/3.1
|
||||
commit id: "2024-09-27" tag: "3.1.2"
|
||||
checkout support/3.2
|
||||
commit id: "2024-06-25" tag: "3.2.0-beta1" type: REVERSE
|
||||
commit id: "2024-08-07" tag: "3.2.0"
|
||||
commit id: "2024-09-13" tag: "3.2.0-2"
|
||||
checkout support/3.1
|
||||
commit id: "2024-09-27" tag: "3.1.2"
|
||||
checkout support/2.7
|
||||
commit id: "2024-09-28" tag: "2.7.11"
|
||||
checkout support/2.7
|
||||
commit id: "2025-02-25" tag: "2.7.12"
|
||||
commit id: "2025-02-07a" tag: "2.7.12"
|
||||
checkout support/3.1
|
||||
commit id: "2025-02-25 " tag: "3.1.3"
|
||||
commit id: "2025-02-07b" tag: "3.1.3"
|
||||
checkout support/3.2
|
||||
commit id: "2025-02-25 " tag: "3.2.1"
|
||||
commit id: "2025-04-08" tag: "3.2.1-1"
|
||||
commit id: "2025-08-19" tag: "3.2.2-1"
|
||||
commit id: "2025-02-07c " tag: "3.2.1"
|
||||
commit id: "2025-03-31 " tag: "3.2.1-1"
|
||||
commit id: "2025-07-28 " tag: "3.2.2"
|
||||
checkout support/2.7
|
||||
commit id: "2025-10-07" tag: "2.7.13"
|
||||
commit id: "2025-09-25" tag: "2.7.13"
|
||||
checkout support/3.2
|
||||
commit id: "2026-04-27 " tag: "3.2.3"
|
||||
checkout support/3.2.3
|
||||
commit id: "2026-05-25 " tag: "3.2.3-1"
|
||||
commit id: "2026-07-17 " tag: "3.2.3-2"
|
||||
checkout develop
|
||||
commit id: "2026-07-23" tag: "3.3.0-beta1"
|
||||
```
|
||||
|
||||
To learn more, check the [iTop community versions history on the official wiki](https://www.itophub.io/wiki/page?id=latest:release:start).
|
||||
|
||||
60
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
60
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: "Bug report"
|
||||
description: "Report a bug that you identified in iTop"
|
||||
type: bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please explain why you're creating this issue :
|
||||
- Are you willing to create a PR for the bug fix ? If so, we'll indicate in the issue if we're interested in it.
|
||||
- Then, please describe how to reproduce the issue.
|
||||
|
||||
- type: dropdown
|
||||
id: willing_to_pr
|
||||
attributes:
|
||||
label: Are you willing to create (at a later stage) a PR for that?
|
||||
options:
|
||||
- 'Yes'
|
||||
- 'No'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: itop_version
|
||||
attributes:
|
||||
label: iTop version
|
||||
description: "Complete iTop version (e.g., 3.2.3)"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: php_version
|
||||
attributes:
|
||||
label: PHP version
|
||||
description: "Complete PHP version (e.g., 8.4.20)"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: reproduction_steps
|
||||
attributes:
|
||||
label: Reproduction procedure
|
||||
description: |
|
||||
Please explain step by step how to reproduce the issue on a standard iTop Community.
|
||||
If it requires a custom datamodel, provide the minimal XML delta to reproduce it.
|
||||
placeholder: |
|
||||
1. First go there
|
||||
2. Then do that
|
||||
3. ...
|
||||
4. Finally, see that... (what is expected and what is actually happening)
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: upload
|
||||
id: additional_info
|
||||
attributes:
|
||||
label: Additional information (if needed)
|
||||
description: "Add/drag and drop screenshots, logs or any files that can be relevant for your issue."
|
||||
validations:
|
||||
required: false
|
||||
accept: ".png, .jpg, .jpeg, .gif, .webp, .log, .txt, .json, .csv, .xml, .zip"
|
||||
47
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
Normal file
47
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: "Enhancement suggestion"
|
||||
description: "Suggest an improvement to iTop"
|
||||
type: feature
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please explain why you're creating this issue :
|
||||
- Please describe what's your improvement proposition.
|
||||
- Then tell us if you're willing to create a PR for this enhancement ? If so, we'll indicate in the issue if we're interested in it.
|
||||
|
||||
- type: textarea
|
||||
id: enhancement_details
|
||||
attributes:
|
||||
label: Enhancement details
|
||||
description: |
|
||||
Please explain what you want to improve, and your proposition to make it better.
|
||||
If it requires a custom datamodel, provide the minimal XML delta to reproduce it.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: itop_version
|
||||
attributes:
|
||||
label: iTop version (if appropriate)
|
||||
description: "Complete iTop version (e.g., 3.2.3)"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: willing_to_pr
|
||||
attributes:
|
||||
label: Are you willing to create (at a later stage) a PR for that?
|
||||
options:
|
||||
- 'Yes'
|
||||
- 'No'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: upload
|
||||
id: additional_info
|
||||
attributes:
|
||||
label: Additional information (if needed)
|
||||
description: "Add/drag and drop screenshots, logs or any files that can be relevant for your issue."
|
||||
validations:
|
||||
required: false
|
||||
accept: ".png, .jpg, .jpeg, .gif, .webp, .log, .txt, .json, .csv, .xml, .zip"
|
||||
61
.github/pull_request_template.md
vendored
61
.github/pull_request_template.md
vendored
@@ -1,83 +1,76 @@
|
||||
<!--
|
||||
IMPORTANT: Before creating your PR, please create an issue first to know if Combodo is interested in your contribution (not needed for translations PR).
|
||||
Since we may refuse a PR, it's preferable to create an issue first, to avoid spending time coding something that won't be accepted.
|
||||
|
||||
IMPORTANT: Please follow the guidelines within this PR template before submitting it, it will greatly help us process your PR. 🙏
|
||||
Once you've done it, and we confirmed we're interested in it, please follow the guidelines within this PR template before submitting it, it will greatly help us process your PR. 🙏
|
||||
|
||||
Any PRs not following the guidelines or with missing information will not be considered.
|
||||
|
||||
-->
|
||||
|
||||
## Base information
|
||||
| Question | Answer
|
||||
|---------------------------------------------------------------|--------
|
||||
| Related to a SourceForge thread / Another PR / Combodo ticket? | <!-- Put the URL -->
|
||||
| Type of change? | Bug fix / Enhancement / Translations
|
||||
|
||||
| Question | Answer |
|
||||
|---------------------------------------------------------------------------------|--------------------------------------|
|
||||
| Related to a SourceForge thread / Another PR / A GitHub Issue / Combodo ticket? | <!-- Put the URL --> |
|
||||
| Type of change? | Bug fix / Enhancement / Translations |
|
||||
|
||||
## Symptom (bug) / Objective (enhancement)
|
||||
|
||||
<!--
|
||||
If it's a bug
|
||||
- Explain the symptom in details
|
||||
- If possible put error messages, logs or screenshots (you can paste image directly in this editor).
|
||||
|
||||
If it's an enhancement
|
||||
- Describe what is blocking you, what is the objective with as much details as possible.
|
||||
- Describe what is blocking you, what is the objective with as many details as possible.
|
||||
- Add screenshots if it's related to UI.
|
||||
-->
|
||||
|
||||
|
||||
## Reproduction procedure (bug)
|
||||
|
||||
<!--
|
||||
Remove this section only if it's NOT a bug.
|
||||
|
||||
Otherwise, explain step by step how to reproduce the issue on a standard iTop Community.
|
||||
|
||||
Please explain step by step how to reproduce the issue on a standard iTop Community.
|
||||
If it requires a custom datamodel, provide the minimal XML delta to reproduce it on a standard iTop Community.
|
||||
-->
|
||||
|
||||
1. On iTop x.y.z <!-- Put complete iTop version (eg. 3.1.0-2) -->
|
||||
2. With PHP x.y.z <!-- Put complete PHP version (eg. 8.1.24) -->
|
||||
2. First go there
|
||||
2. Then do that
|
||||
3. ...
|
||||
4. Finally, see that...
|
||||
3. First go there
|
||||
4. Then do that
|
||||
5. ...
|
||||
6. Finally, see that... (what is expected and what is actually happening)
|
||||
|
||||
## Reproduction procedure (enhancement - if needed)
|
||||
|
||||
<!--
|
||||
Please explain how we can reproduce the feature/behavior you want to improve, and what's your proposition to make it better.
|
||||
Add screenshots if it's related to UI.
|
||||
If it requires a custom datamodel, provide the minimal XML delta to reproduce it on a standard iTop Community.
|
||||
-->
|
||||
|
||||
## Cause (bug)
|
||||
|
||||
<!--
|
||||
Remove this section only if it's NOT a bug.
|
||||
|
||||
Otherwise, explain what is the cause of the issue (where in the code and why)
|
||||
-->
|
||||
|
||||
|
||||
## Proposed solution (bug and enhancement)
|
||||
|
||||
<!--
|
||||
Explain in details how you are proposing to solve this:
|
||||
- What did you do in the code and why
|
||||
- If you changed something in the UI, put before / after screenshots (you can paste image directly in this editor)
|
||||
-->
|
||||
|
||||
|
||||
## Checklist before requesting a review
|
||||
|
||||
<!--
|
||||
Don't remove these lines, check them once done.
|
||||
-->
|
||||
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have tested all changes I made on an iTop instance
|
||||
- [ ] I have added a unit test, otherwise I have explained why I couldn't
|
||||
- [ ] Is the PR clear and detailed enough so anyone can understand digging in the code?
|
||||
|
||||
## Checklist of things to do before PR is ready to merge
|
||||
<!--
|
||||
Things that needs to be done in the PR before it can be considered as ready to be merged
|
||||
|
||||
Examples:
|
||||
- Changes requested in the review
|
||||
- Unit test to add
|
||||
- Dictionary entries to translate
|
||||
- ...
|
||||
-->
|
||||
|
||||
- [ ] ...
|
||||
- [ ] ...
|
||||
- [ ] ...
|
||||
- [ ] Is the PR clear and detailed enough so anyone can understand without digging in the code?
|
||||
15
.github/workflows/action.yml
vendored
15
.github/workflows/action.yml
vendored
@@ -1,9 +1,13 @@
|
||||
name: Add PRs to Combodo PRs Dashboard
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
@@ -31,23 +35,22 @@ jobs:
|
||||
run: |
|
||||
curl -X POST -H "Authorization: token ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
https://api.github.com/repos/Combodo/iTop/issues/${{ github.event.pull_request.number }}/labels \
|
||||
https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels \
|
||||
-d '{"labels":["internal"]}'
|
||||
|
||||
- name: Set PR author as assignee if member of the organization
|
||||
if: env.is_member == 'true'
|
||||
if: env.is_member == 'true' && github.event_name == 'pull_request'
|
||||
run: |
|
||||
curl -L \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}" \
|
||||
https://api.github.com/repos/Combodo/iTop/issues/${{ github.event.pull_request.number }}/assignees \
|
||||
https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/assignees \
|
||||
-d '{"assignees":["${{ github.event.pull_request.user.login }}"]}'
|
||||
env:
|
||||
is_member: ${{ env.is_member }}
|
||||
|
||||
- name: Add PR to the appropriate project
|
||||
uses: actions/add-to-project@v1.0.2
|
||||
uses: actions/add-to-project@v2
|
||||
with:
|
||||
project-url: ${{ env.project_url }}
|
||||
github-token: ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}
|
||||
|
||||
0
.make/license/gen-community-license.sh
Normal file → Executable file
0
.make/license/gen-community-license.sh
Normal file → Executable file
0
.make/license/sortLicenceXml.php
Normal file → Executable file
0
.make/license/sortLicenceXml.php
Normal file → Executable file
0
.make/license/updateLicenses.php
Normal file → Executable file
0
.make/license/updateLicenses.php
Normal file → Executable file
0
.make/release/changelog.php
Normal file → Executable file
0
.make/release/changelog.php
Normal file → Executable file
0
.make/release/update-versions.php
Normal file → Executable file
0
.make/release/update-versions.php
Normal file → Executable file
0
.make/release/update-xml.php
Normal file → Executable file
0
.make/release/update-xml.php
Normal file → Executable file
0
.make/release/update.classes.inc.php
Normal file → Executable file
0
.make/release/update.classes.inc.php
Normal file → Executable file
@@ -4,30 +4,33 @@ You want to contribute to iTop? Many thanks to you! 🎉 👍
|
||||
|
||||
Here are some guidelines that will help us integrate your work!
|
||||
|
||||
|
||||
|
||||
## Contributions
|
||||
|
||||
### Subjects
|
||||
|
||||
You are welcome to create pull requests on any of those subjects:
|
||||
|
||||
* 🐛 bug fix
|
||||
* 🌐 translation / i18n / l10n
|
||||
* 🚸 enhancement
|
||||
|
||||
If you want to implement a **new feature**, please [create a corresponding ticket](https://sourceforge.net/p/itop/tickets/new/) for review.
|
||||
If you ever want to begin implementation, do so in a fork, and add a link to the corresponding commits in the ticket.
|
||||
But before creating a PR, please [create a corresponding issue][itop-issues] for review.
|
||||
We should review within two weeks, and get back to you to indicate if we're interested in your proposal or not.
|
||||
If you don't create an issue, you won't know if we're interested in your contribution, and you may spend time coding something that won't be accepted.
|
||||
If you ever want to begin implementation, do so in a fork, and add a link to the corresponding commits in the issue.
|
||||
|
||||
For all **security related subjects**, please see our [security policy](SECURITY.md).
|
||||
|
||||
All **datamodel modification** should be done in an extension. Beware that such change would
|
||||
impact all existing customers, and could prevent them from
|
||||
upgrading!
|
||||
Combodo has a long experience of datamodel changes: they are very disruptive!
|
||||
All **datamodel modification** should be done in an extension. Beware that such change would
|
||||
impact all existing customers, and could prevent them from upgrading!
|
||||
Combodo has a long experience of datamodel changes: they are very disruptive!
|
||||
This is why we avoid them in iTop core, especially the changes on existing objects/fields.
|
||||
If you have an idea you're sure would benefit to all of iTop users, you may
|
||||
[create a corresponding ticket](https://sourceforge.net/p/itop/tickets/new/) to submit it, but be warned that there are lots of good
|
||||
If you have an idea you're sure would benefit to all of iTop users, you may
|
||||
[create a corresponding issue][itop-issues] to submit it, but be warned that there are lots of good
|
||||
reasons to refuse such changes.
|
||||
|
||||
### 📄 License and copyright
|
||||
|
||||
iTop is distributed under the AGPL-3.0 license (see the [license.txt] file).
|
||||
|
||||
The iTop repository is divided in three parts: iTop (mainly PHP/JS/XML sources and dictionaries), images, and third-party libraries.
|
||||
@@ -37,48 +40,33 @@ Anyhow, you are encouraged to signal your contribution by the mean of `@author`
|
||||
If you want to use another license or keep the code ownership (copyright), you may [create an extension][wiki new ext].
|
||||
|
||||
[license.txt]: https://github.com/Combodo/iTop/blob/develop/license.txt
|
||||
[wiki new ext]: https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Astart#by_writing_your_own_extension
|
||||
|
||||
[itop-issues]: https://github.com/Combodo/iTop/issues
|
||||
|
||||
[wiki new ext]: https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Astart#by_writing_your_own_extension
|
||||
|
||||
## 🔀 iTop branch model
|
||||
|
||||
When we first start with Git, we were using the [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/) branch model. As
|
||||
there was some confusions about branches to use for current developed release and previous maintained release, and also because we were
|
||||
using just a very few of the GitFlow commands, we decided to add just a little modification to this branch model : since april 2020
|
||||
we don't have a `master` branch anymore.
|
||||
there was some confusions about branches to use for current developed release and previous maintained release, and also because we were
|
||||
using just a very few of the GitFlow commands, we decided to add just a little modification to this branch model : since April 2020
|
||||
we don't have a `master` branch anymore.
|
||||
|
||||
Here are the branches we use and their meaning :
|
||||
Here are the branches we use and their meaning :
|
||||
|
||||
- `develop`: ongoing development version
|
||||
- `release/*`: if present, that means we are working on a alpha/beta/rc version for shipping
|
||||
- `support/*`: maintenance branches for older versions
|
||||
|
||||
For example, if no version is currently prepared for shipping we could have:
|
||||
|
||||
- `develop` containing future 3.1.0 version
|
||||
- `support/3.0`: 3.0.x maintenance version
|
||||
- `support/2.7`: 2.7.x maintenance version
|
||||
- `support/2.6`: 2.6.x maintenance version
|
||||
- `develop` containing future 3.3.0 version
|
||||
- `support/3.2`: 3.2.x maintenance version
|
||||
|
||||
In this example, when 3.1.0-beta is shipped that will become:
|
||||
|
||||
- `develop`: future 3.2.0 version
|
||||
- `release/3.1.0`: 3.1.0-beta
|
||||
- `support/3.0`: 3.0.x maintenance version
|
||||
- `support/2.7`: 2.7.x maintenance version
|
||||
- `support/2.6`: 2.6.x maintenance version
|
||||
|
||||
And when 3.1.0 final will be out:
|
||||
|
||||
- `develop`: future 3.2.0 version
|
||||
- `support/3.1`: 3.1.x maintenance version (will host developments for 3.1.1)
|
||||
- `support/3.0`: 3.0.x maintenance version
|
||||
- `support/2.7`: 2.7.x maintenance version
|
||||
- `support/2.6`: 2.6.x maintenance version
|
||||
|
||||
Also note that we have a "micro-version" concept : each of those versions have a very small amount of modifications. They are made from
|
||||
`support/*` branches as well. For example 2.6.2-1 and 2.6.2-2 were made from the `support/2.6.2` branch.
|
||||
And when 3.3.0 will be out:
|
||||
|
||||
- `develop`: future 3.4.0 version
|
||||
- `support/3.3`: 3.3.x maintenance version (will host developments for 3.3.1)
|
||||
- `support/3.2`: 3.2.x maintenance version
|
||||
|
||||
## Coding
|
||||
|
||||
@@ -92,12 +80,11 @@ A [dedicated page](https://www.itophub.io/wiki/page?id=latest%3Acustomization%3A
|
||||
2. Create a branch in this fork, based on the develop branch
|
||||
3. Code !
|
||||
|
||||
Do create a dedicated branch for each modification you want to propose : if you don't it will be very hard to merge back your work !
|
||||
Do create a dedicated branch for each modification you want to propose : if you don't, it will be very hard to merge back your work !
|
||||
|
||||
Most of the time you should based your developments on the develop branch.
|
||||
Most of the time you should base your developments on the develop branch.
|
||||
That may be different if you want to fix a bug, please use develop anyway and ask in your PR if rebase is possible.
|
||||
|
||||
|
||||
### 🎨 PHP styleguide
|
||||
|
||||
Please follow [our guidelines](https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Acoding_standards).
|
||||
@@ -106,7 +93,7 @@ Please follow [our guidelines](https://www.itophub.io/wiki/page?id=latest%3Acust
|
||||
|
||||
Please create tests that covers as much as possible the code you're submitting.
|
||||
|
||||
Our tests are located in the `test/` directory, containing a PHPUnit config file : `phpunit.xml.dist`.
|
||||
Our tests are located in the `tests/` directory, containing a PHPUnit config file : `phpunit.xml.dist`.
|
||||
|
||||
### Git Commit Messages
|
||||
|
||||
@@ -138,14 +125,14 @@ When your code is working, please:
|
||||
* Rebase your branch on our repo last commit,
|
||||
* Create a pull request. _Detailed procedure to work on fork and create PR is available [in GitHub help pages](https://help.github.com/articles/creating-a-pull-request-from-a-fork/)_.
|
||||
* Pull request description: mind to add all the information useful to understand why you're suggesting this modification and anything necessary to dive into your work. Especially:
|
||||
- Bugfixes: exact steps to reproduce the bug (given/when/then), description of the bug cause and what solution is implemented
|
||||
- Enhancements: use cases, implementation details if needed
|
||||
* Mind to check the "[Allow edits from maintainers](https://docs.github.com/en/github-ae@latest/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork)" option ! (note that if you are working with an org fork, this option [won't be available](https://github.com/orgs/community/discussions/5634))
|
||||
|
||||
- Bugfixes: exact steps to reproduce the bug (given/when/then), description of the bug cause and what solution is implemented
|
||||
- Enhancements: use cases, implementation details if needed
|
||||
* Mind to check the "[Allow edits from maintainers](https://docs.github.com/en/github-ae@latest/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork)" option ! (note that if you are working with an org fork, this
|
||||
option [won't be available](https://github.com/orgs/community/discussions/5634))
|
||||
|
||||
## 🙏 We are thankful
|
||||
|
||||
We are thankful for all your contributions to the iTop universe! As a thank you gift, we will send stickers to every iTop (& extensions) contributors!
|
||||
We are thankful for all your contributions to the iTop universe! As a thank-you gift, we will send stickers to every iTop (& extensions) contributors!
|
||||
|
||||
We have one sticker per contribution type. You might get multiple stickers with one contribution though :)
|
||||
|
||||
@@ -159,6 +146,6 @@ We have one sticker per contribution type. You might get multiple stickers with
|
||||
* Beta tester: Test and give feedback on beta releases
|
||||
* Extension developer: Develop and publish an extension
|
||||
|
||||
Here is the design of each stickers for year 2024:
|
||||
Here is the design of each sticker:
|
||||
|
||||

|
||||

|
||||
@@ -53,7 +53,7 @@ iTop also offers mass import tools to help you become even more efficient.
|
||||
[4]: https://www.itophub.io/wiki/page?id=latest:install:requirements
|
||||
[5]: https://www.itophub.io/wiki
|
||||
[6]: https://store.itophub.io/en_US/
|
||||
[7]: itop-version-history.md
|
||||
[7]: .doc/itop-version-history.md
|
||||
|
||||
[10]: https://www.itophub.io/wiki/page?id=latest%3Adatamodel%3Astart#configuration_management_cmdb
|
||||
[11]: https://www.itophub.io/wiki/page?id=latest%3Adatamodel%3Astart#ticketing
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -175,6 +175,11 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
|
||||
*/
|
||||
protected $sDisplayMode;
|
||||
protected $aFieldsMap;
|
||||
/**
|
||||
* @var array Store posted values in order to be used in GetAttributeFlags
|
||||
* @since 3.3.0
|
||||
*/
|
||||
protected $aPostedValues = [];
|
||||
|
||||
/**
|
||||
* If true, bypass IsActionAllowedOnAttribute when writing this object
|
||||
@@ -524,6 +529,12 @@ JS
|
||||
$sLabel = Dict::S('Tag:Synchronized');
|
||||
$sSynchroTagId = 'synchro_icon-'.$this->GetKey();
|
||||
$aTags[$sSynchroTagId] = ['title' => $sTip, 'css_classes' => 'ibo-object-details--tag--synchronized', 'decoration_classes' => 'fas fa-lock', 'label' => $sLabel];
|
||||
if (UserRights::IsActionAllowed(SynchroReplica::class, UR_ACTION_READ)) {
|
||||
$oDBSearch = DBObjectSearch::FromOQL('SELECT SynchroReplica WHERE dest_class=:sClass AND dest_id=:id');
|
||||
$sFilter = rawurlencode($oDBSearch->serialize(false, ['sClass' => get_class($this),'id' => $this->GetKey()]));
|
||||
$sUrlSearchReplica = 'UI.php?operation=search&filter='.$sFilter;
|
||||
$oPage->add_ready_script("$('#$sSynchroTagId').on('click',function() {window.location = '$sUrlSearchReplica' });");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1035,7 +1046,7 @@ HTML
|
||||
// Add extra data for markup generation
|
||||
// - Attribute code and AttributeDef. class
|
||||
$val['attcode'] = $sAttCode;
|
||||
$val['atttype'] = $sAttDefClass;
|
||||
$val['atttype'] = $oAttDef->GetTypeShortClassName();
|
||||
$val['attlabel'] = $sAttLabel;
|
||||
$val['attflags'] = ($bEditMode) ? $this->GetFormAttributeFlags($sAttCode) : OPT_ATT_READONLY;
|
||||
|
||||
@@ -1524,190 +1535,6 @@ HTML
|
||||
return $sHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oPage
|
||||
* @param \CMDBObjectSet $oSet
|
||||
* @param array $aParams
|
||||
*
|
||||
* @throws \Exception
|
||||
* only used in old and deprecated export.php
|
||||
*
|
||||
* @internal Only to be used by `/webservices/export.php` : this is a legacy method that produces wrong HTML (no TR on table body rows)
|
||||
*/
|
||||
public static function DisplaySetAsHTMLSpreadsheet(WebPage $oPage, CMDBObjectSet $oSet, $aParams = [])
|
||||
{
|
||||
$oPage->add(self::GetSetAsHTMLSpreadsheet($oSet, $aParams));
|
||||
}
|
||||
|
||||
/**
|
||||
* Spreadsheet output: designed for end users doing some reporting
|
||||
* Then the ids are excluded and replaced by the corresponding friendlyname
|
||||
*
|
||||
* @param \DBObjectSet $oSet
|
||||
* @param array $aParams
|
||||
*
|
||||
* @return string
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MissingQueryArgument
|
||||
* @throws \MySQLException
|
||||
* @throws \MySQLHasGoneAwayException
|
||||
* @throws \Exception
|
||||
*
|
||||
* @internal Only to be used by `/webservices/export.php` : this is a legacy method that produces wrong HTML (no TR on table body rows)
|
||||
*/
|
||||
public static function GetSetAsHTMLSpreadsheet(DBObjectSet $oSet, $aParams = [])
|
||||
{
|
||||
$aFields = null;
|
||||
if (isset($aParams['fields']) && (strlen($aParams['fields']) > 0)) {
|
||||
$aFields = explode(',', $aParams['fields']);
|
||||
}
|
||||
|
||||
$bFieldsAdvanced = false;
|
||||
if (isset($aParams['fields_advanced'])) {
|
||||
$bFieldsAdvanced = (bool)$aParams['fields_advanced'];
|
||||
}
|
||||
|
||||
$bLocalize = true;
|
||||
if (isset($aParams['localize_values'])) {
|
||||
$bLocalize = (bool)$aParams['localize_values'];
|
||||
}
|
||||
|
||||
$aList = [];
|
||||
|
||||
$aClasses = $oSet->GetFilter()->GetSelectedClasses();
|
||||
$aAuthorizedClasses = [];
|
||||
foreach ($aClasses as $sAlias => $sClassName) {
|
||||
if (UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $oSet) != UR_ALLOWED_NO) {
|
||||
$aAuthorizedClasses[$sAlias] = $sClassName;
|
||||
}
|
||||
}
|
||||
$aHeader = [];
|
||||
foreach ($aAuthorizedClasses as $sAlias => $sClassName) {
|
||||
$aList[$sAlias] = [];
|
||||
|
||||
foreach (MetaModel::ListAttributeDefs($sClassName) as $sAttCode => $oAttDef) {
|
||||
if (is_null($aFields) || (count($aFields) == 0)) {
|
||||
// Standard list of attributes (no link sets)
|
||||
if ($oAttDef->IsScalar() && ($oAttDef->IsWritable() || $oAttDef->IsExternalField())) {
|
||||
$sAttCodeEx = $oAttDef->IsExternalField() ? $oAttDef->GetKeyAttCode().'->'.$oAttDef->GetExtAttCode() : $sAttCode;
|
||||
|
||||
$aList[$sAlias][$sAttCodeEx] = $oAttDef;
|
||||
|
||||
if ($bFieldsAdvanced && $oAttDef->IsExternalKey(EXTKEY_RELATIVE)) {
|
||||
$sRemoteClass = $oAttDef->GetTargetClass();
|
||||
foreach (MetaModel::GetReconcKeys($sRemoteClass) as $sRemoteAttCode) {
|
||||
$aList[$sAlias][$sAttCode.'->'.$sRemoteAttCode] = MetaModel::GetAttributeDef(
|
||||
$sRemoteClass,
|
||||
$sRemoteAttCode
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// User defined list of attributes
|
||||
if (in_array($sAttCode, $aFields) || in_array($sAlias.'.'.$sAttCode, $aFields)) {
|
||||
$aList[$sAlias][$sAttCode] = $oAttDef;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Replace external key by the corresponding friendly name (if not already in the list)
|
||||
foreach ($aList[$sAlias] as $sAttCode => $oAttDef) {
|
||||
if ($oAttDef->IsExternalKey()) {
|
||||
unset($aList[$sAlias][$sAttCode]);
|
||||
$sFriendlyNameAttCode = $sAttCode.'_friendlyname';
|
||||
if (!array_key_exists(
|
||||
$sFriendlyNameAttCode,
|
||||
$aList[$sAlias]
|
||||
) && MetaModel::IsValidAttCode($sClassName, $sFriendlyNameAttCode)) {
|
||||
$oFriendlyNameAtt = MetaModel::GetAttributeDef($sClassName, $sFriendlyNameAttCode);
|
||||
$aList[$sAlias][$sFriendlyNameAttCode] = $oFriendlyNameAtt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($aList[$sAlias] as $sAttCodeEx => $oAttDef) {
|
||||
$sColLabel = $bLocalize ? MetaModel::GetLabel($sClassName, $sAttCodeEx) : $sAttCodeEx;
|
||||
|
||||
$oFinalAttDef = $oAttDef->GetFinalAttDef();
|
||||
if (get_class($oFinalAttDef) == 'AttributeDateTime') {
|
||||
$aHeader[] = $sColLabel.' ('.Dict::S('UI:SplitDateTime-Date').')';
|
||||
$aHeader[] = $sColLabel.' ('.Dict::S('UI:SplitDateTime-Time').')';
|
||||
} else {
|
||||
$aHeader[] = $sColLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sHtml = "<table border=\"1\">\n";
|
||||
$sHtml .= "<tr>\n";
|
||||
$sHtml .= "<td>".implode("</td><td>", $aHeader)."</td>\n";
|
||||
$sHtml .= "</tr>\n";
|
||||
$oSet->Seek(0);
|
||||
while ($aObjects = $oSet->FetchAssoc()) {
|
||||
$aRow = [];
|
||||
foreach ($aAuthorizedClasses as $sAlias => $sClassName) {
|
||||
$oObj = $aObjects[$sAlias];
|
||||
foreach ($aList[$sAlias] as $sAttCodeEx => $oAttDef) {
|
||||
if (is_null($oObj)) {
|
||||
$aRow[] = '<td></td>';
|
||||
} else {
|
||||
$oFinalAttDef = $oAttDef->GetFinalAttDef();
|
||||
if (get_class($oFinalAttDef) == 'AttributeDateTime') {
|
||||
$sDate = $oObj->Get($sAttCodeEx);
|
||||
if ($sDate === null) {
|
||||
$aRow[] = '<td></td>';
|
||||
$aRow[] = '<td></td>';
|
||||
} else {
|
||||
$iDate = AttributeDateTime::GetAsUnixSeconds($sDate);
|
||||
$aRow[] = '<td>'.date(
|
||||
'Y-m-d',
|
||||
$iDate
|
||||
).'</td>'; // Format kept as-is for 100% backward compatibility of the exports
|
||||
$aRow[] = '<td>'.date(
|
||||
'H:i:s',
|
||||
$iDate
|
||||
).'</td>'; // Format kept as-is for 100% backward compatibility of the exports
|
||||
}
|
||||
} else {
|
||||
if ($oAttDef instanceof AttributeCaseLog) {
|
||||
$rawValue = $oObj->Get($sAttCodeEx);
|
||||
$outputValue = str_replace(
|
||||
"\n",
|
||||
"<br/>",
|
||||
utils::EscapeHtml($rawValue->__toString())
|
||||
);
|
||||
// Trick for Excel: treat the content as text even if it begins with an equal sign
|
||||
$aRow[] = '<td x:str>'.$outputValue.'</td>';
|
||||
} else {
|
||||
$rawValue = $oObj->Get($sAttCodeEx);
|
||||
// Due to custom formatting rules, empty friendlynames may be rendered as non-empty strings
|
||||
// let's fix this and make sure we render an empty string if the key == 0
|
||||
if ($oAttDef instanceof AttributeExternalField && $oAttDef->IsFriendlyName()) {
|
||||
$sKeyAttCode = $oAttDef->GetKeyAttCode();
|
||||
if ($oObj->Get($sKeyAttCode) == 0) {
|
||||
$rawValue = '';
|
||||
}
|
||||
}
|
||||
if ($bLocalize) {
|
||||
$outputValue = utils::EscapeHtml($oFinalAttDef->GetEditValue($rawValue));
|
||||
} else {
|
||||
$outputValue = utils::EscapeHtml($rawValue);
|
||||
}
|
||||
$aRow[] = '<td>'.$outputValue.'</td>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$sHtml .= implode("\n", $aRow);
|
||||
$sHtml .= "</tr>\n";
|
||||
}
|
||||
$sHtml .= "</table>\n";
|
||||
|
||||
return $sHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oPage
|
||||
* @param \CMDBObjectSet $oSet
|
||||
@@ -3239,8 +3066,12 @@ EOF
|
||||
}
|
||||
$aAllowedValues = MetaModel::GetAllowedValues_att($sClass, $sAttCode, $aArgs);
|
||||
if (is_array($aAllowedValues) && count($aAllowedValues) == 1) {
|
||||
$aValues = array_keys($aAllowedValues);
|
||||
$this->Set($sAttCode, $aValues[0]);
|
||||
// linkset are ignored
|
||||
// N°9622 - Error in the user story life cycle
|
||||
if (!$oAttDef->IsLinkSet()) {
|
||||
$aValues = array_keys($aAllowedValues);
|
||||
$this->Set($sAttCode, $aValues[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3986,6 +3817,7 @@ HTML;
|
||||
|
||||
$aErrors = [];
|
||||
$aFinalValues = [];
|
||||
$this->aPostedValues = $aValues; // Store the values for later use (e.g. in GetAttributeFlags)
|
||||
foreach ($this->GetWriteableAttList(array_keys($aValues), $aErrors, $aAttFlags) as $sAttCode => $oAttDef) {
|
||||
$aFinalValues[$sAttCode] = $aValues[$sAttCode];
|
||||
}
|
||||
@@ -4155,6 +3987,27 @@ HTML;
|
||||
$sTagSetJson = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", null, 'raw_data');
|
||||
if ($sTagSetJson !== null) { // bulk modify, direct linked set not handled
|
||||
$value = json_decode($sTagSetJson, true);
|
||||
if ($this->IsNew()) {
|
||||
if (is_array($value['orig_value'])) {
|
||||
foreach ($value['orig_value'] as $val) {
|
||||
if (!in_array($val, $value['removed'])) {
|
||||
$value['added'][] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$aCurrentValues = $this->Get($sAttCode)->GetValues();
|
||||
foreach ($value['orig_value'] as $val) {
|
||||
if (!in_array($val, $aCurrentValues) && !in_array($val, $value['removed']) && !in_array($val, $value['added'])) {
|
||||
$value['added'][] = $val;
|
||||
}
|
||||
}
|
||||
foreach ($aCurrentValues as $val) {
|
||||
if (!in_array($val, $value['orig_value']) && !in_array($val, $value['removed']) && !in_array($val, $value['added'])) {
|
||||
$value['removed'][] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -4495,7 +4348,7 @@ HTML;
|
||||
|
||||
$oDivField = FieldUIBlockFactory::MakeLarge("");
|
||||
// UIContentBlockUIBlockFactory::MakeStandard(null,["field_container field_large"]);
|
||||
$oDivField->AddDataAttribute("attribute-type", $sAttDefClass);
|
||||
$oDivField->AddDataAttribute("attribute-type", $oAttDef->GetTypeShortClassName());
|
||||
$oDivField->AddDataAttribute("attribute-label", $sAttMetaDataLabel);
|
||||
$oDivField->AddDataAttribute("attribute-flag-hidden", false);
|
||||
$oDivField->AddDataAttribute("attribute-flag-read-only", false);
|
||||
@@ -5064,10 +4917,10 @@ EOF
|
||||
} else {
|
||||
if (count($aObjects) == 1) {
|
||||
$oObj = $aObjects[0];
|
||||
$sSubtitle = Dict::Format('UI:Delect:Confirm_Object', $oObj->GetHyperLink());
|
||||
$sSubtitle = Dict::Format('UI:Delete:Confirm_Object', $oObj->GetHyperLink());
|
||||
} else {
|
||||
$sSubtitle = Dict::Format(
|
||||
'UI:Delect:Confirm_Count_ObjectsOf_Class',
|
||||
'UI:Delete:Confirm_Count_ObjectsOf_Class',
|
||||
count($aObjects),
|
||||
MetaModel::GetName($sClass)
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -25,6 +25,20 @@ use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\iUIBlock;
|
||||
use Combodo\iTop\Application\UI\Base\UIBlock;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeDateTime;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeDecimal;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeDuration;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeEncryptedString;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeEnum;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeExternalField;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeFriendlyName;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeInteger;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeLinkedSet;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeOneWayPassword;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributePassword;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributePercentage;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeSubItem;
|
||||
use Combodo\iTop\Core\AttributeDefinition\iAttributeNoGroupBy;
|
||||
|
||||
require_once(APPROOT.'application/forms.class.inc.php');
|
||||
|
||||
@@ -440,7 +454,7 @@ EOF
|
||||
// For external fields, find the real type of the target
|
||||
$sExtFieldAttCode = $sAttCode;
|
||||
$sTargetClass = $sClass;
|
||||
while (is_a($sAttType, 'AttributeExternalField', true)) {
|
||||
while (is_a($sAttType, AttributeExternalField::class, true)) {
|
||||
$sExtKeyAttCode = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'extkey_attcode');
|
||||
$sTargetAttCode = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'target_attcode');
|
||||
$sTargetClass = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtKeyAttCode, 'targetclass');
|
||||
@@ -450,13 +464,13 @@ EOF
|
||||
}
|
||||
|
||||
$aForbidenAttType = [
|
||||
'AttributeLinkedSet',
|
||||
'AttributeFriendlyName',
|
||||
AttributeLinkedSet::class,
|
||||
AttributeFriendlyName::class,
|
||||
|
||||
'iAttributeNoGroupBy', //we cannot only use iAttributeNoGroupBy since this method is also used by the designer who do not have access to the classes' PHP reflection API. So the known classes has to be listed altogether
|
||||
'AttributeOneWayPassword',
|
||||
'AttributeEncryptedString',
|
||||
'AttributePassword',
|
||||
iAttributeNoGroupBy::class, //we cannot only use iAttributeNoGroupBy since this method is also used by the designer who do not have access to the classes' PHP reflection API. So the known classes has to be listed altogether
|
||||
AttributeOneWayPassword::class,
|
||||
AttributeEncryptedString::class,
|
||||
AttributePassword::class,
|
||||
];
|
||||
foreach ($aForbidenAttType as $sForbidenAttType) {
|
||||
if (is_a($sAttType, $sForbidenAttType, true)) {
|
||||
@@ -468,7 +482,7 @@ EOF
|
||||
if (!in_array($sLabel, $aGroupBy)) {
|
||||
$aGroupBy[$sAttCode] = $sLabel;
|
||||
|
||||
if (is_a($sAttType, 'AttributeDateTime', true)) {
|
||||
if (is_a($sAttType, AttributeDateTime::class, true)) {
|
||||
$aGroupBy[$sAttCode.':hour'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Hour', $sLabel);
|
||||
$aGroupBy[$sAttCode.':month'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Month', $sLabel);
|
||||
$aGroupBy[$sAttCode.':day_of_week'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-DayOfWeek', $sLabel);
|
||||
@@ -1102,7 +1116,7 @@ abstract class DashletGroupBy extends Dashlet
|
||||
$aAttributeTypes = $this->oModelReflection->ListAttributes($this->sClass);
|
||||
if (isset($aAttributeTypes[$this->sGroupByAttCode])) {
|
||||
$sAttributeType = $aAttributeTypes[$this->sGroupByAttCode];
|
||||
if (is_subclass_of($sAttributeType, 'AttributeDateTime') || $sAttributeType == 'AttributeDateTime') {
|
||||
if (is_subclass_of($sAttributeType, AttributeDateTime::class) || $sAttributeType == AttributeDateTime::class) {
|
||||
$this->sOrderDirection = 'asc';
|
||||
} else {
|
||||
$this->sOrderDirection = 'desc';
|
||||
@@ -1258,7 +1272,7 @@ abstract class DashletGroupBy extends Dashlet
|
||||
if ($this->oModelReflection->IsValidAttCode($sClass, $this->sGroupByAttCode)) {
|
||||
$aAttributeTypes = $this->oModelReflection->ListAttributes($sClass);
|
||||
$sAttributeType = $aAttributeTypes[$this->sGroupByAttCode];
|
||||
if (is_subclass_of($sAttributeType, 'AttributeDateTime') || $sAttributeType == 'AttributeDateTime') {
|
||||
if (is_subclass_of($sAttributeType, AttributeDateTime::class) || $sAttributeType == AttributeDateTime::class) {
|
||||
// Note: an alternative to this somewhat hardcoded way of doing things would be to implement...
|
||||
//$oExpr = Expression::FromOQL($this->sGroupByExpr);
|
||||
//$aTranslationData = array($oQuery->GetClassAlias() => array($this->sGroupByAttCode => new ScalarExpression(date('Y-m-d H:i:s', $iTime))));
|
||||
@@ -1289,7 +1303,7 @@ abstract class DashletGroupBy extends Dashlet
|
||||
foreach ($aValues as $sValue) {
|
||||
$aDisplayValues[] = ['label' => $sValue, 'value' => (int)rand(1, 15)];
|
||||
}
|
||||
} elseif (is_subclass_of($sAttributeType, 'AttributeEnum') || $sAttributeType == 'AttributeEnum') {
|
||||
} elseif (is_subclass_of($sAttributeType, AttributeEnum::class) || $sAttributeType == AttributeEnum::class) {
|
||||
$aAllowed = $this->oModelReflection->GetAllowedValues_att($sClass, $this->sGroupByAttCode);
|
||||
if ($aAllowed) { // null for non enums
|
||||
foreach ($aAllowed as $sValue => $sValueLabel) {
|
||||
@@ -1458,11 +1472,11 @@ abstract class DashletGroupBy extends Dashlet
|
||||
}
|
||||
foreach ($this->oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType) {
|
||||
switch ($sAttType) {
|
||||
case 'AttributeDecimal':
|
||||
case 'AttributeDuration':
|
||||
case 'AttributeInteger':
|
||||
case 'AttributePercentage':
|
||||
case 'AttributeSubItem': // TODO: Known limitation: no unit displayed (values in sec)
|
||||
case AttributeDecimal::class:
|
||||
case AttributeDuration::class:
|
||||
case AttributeInteger::class:
|
||||
case AttributePercentage::class:
|
||||
case AttributeSubItem::class: // TODO: Known limitation: no unit displayed (values in sec)
|
||||
$sLabel = $this->oModelReflection->GetLabel($sClass, $sAttCode);
|
||||
$aFunctionAttributes[$sAttCode] = $sLabel;
|
||||
break;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ class CoreException extends Exception
|
||||
/**
|
||||
* CoreException constructor.
|
||||
*
|
||||
* ATTENTION: Logging here will break the CI
|
||||
*
|
||||
* @param string $sIssue error message
|
||||
* @param array|null $aContextData key/value array, value MUST implements _toString
|
||||
* @param string $sImpact
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1886,7 +1886,7 @@ SQL;
|
||||
CURLOPT_HEADER => false, // don't return the headers in the output
|
||||
CURLOPT_FOLLOWLOCATION => true, // follow redirects
|
||||
CURLOPT_ENCODING => "", // handle all encodings
|
||||
CURLOPT_USERAGENT => "spider", // who am i
|
||||
CURLOPT_USERAGENT => static::GetConfig()->Get('http.request.user_agent'), // who am i
|
||||
CURLOPT_AUTOREFERER => true, // set referer on redirect
|
||||
CURLOPT_CONNECTTIMEOUT => 120, // timeout on connect
|
||||
CURLOPT_TIMEOUT => 120, // timeout on response
|
||||
@@ -1917,7 +1917,9 @@ SQL;
|
||||
$aResponseHeaders[$sName] = $sValue;
|
||||
}
|
||||
}
|
||||
curl_close($ch);
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
@@ -3020,7 +3022,6 @@ TXT
|
||||
* Note: Only works for backoffice URLs for now
|
||||
*
|
||||
* @param string $sText Text containing the mentioned objects to be found
|
||||
* @param string $sFormat {@uses static::ENUM_TEXT_FORMAT_HTML, ...}
|
||||
*
|
||||
* @return array Array of object classes / IDs for the ones found in $sText
|
||||
*
|
||||
@@ -3035,13 +3036,45 @@ TXT
|
||||
public static function GetMentionedObjectsFromText(string $sText): array
|
||||
{
|
||||
$aMentionedObjects = [];
|
||||
$aMentionMatches = [];
|
||||
$sText = html_entity_decode($sText);
|
||||
$aMentionAllowedClasses = MetaModel::GetConfig()->Get('mentions.allowed_classes');
|
||||
$oDom = new \DOMDocument();
|
||||
$bPreviousUseInternalErrors = libxml_use_internal_errors(true); // to keep processing even in case of "invalid" HTML, cf. testGetMentionedObjectsFromText
|
||||
|
||||
preg_match_all('/<a\s*([^>]*)data-object-class="([^"]*)"\s.*data-object-key="([^"]*)"/Ui', $sText, $aMentionMatches);
|
||||
foreach ($aMentionMatches[0] as $iMatchIdx => $sCompleteMatch) {
|
||||
$sMatchedClass = $aMentionMatches[2][$iMatchIdx];
|
||||
$sMatchedId = $aMentionMatches[3][$iMatchIdx];
|
||||
try {
|
||||
$oDom->loadHTML('<?xml encoding="UTF-8">'.$sText);
|
||||
} finally {
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($bPreviousUseInternalErrors);
|
||||
}
|
||||
$oXpath = new \DOMXPath($oDom);
|
||||
$oNodes = $oXpath->query('//a[@data-object-class and @data-object-key]');
|
||||
|
||||
foreach ($oNodes as $oObjNode) {
|
||||
$sMatchedClass = $oObjNode->getAttribute('data-object-class');
|
||||
$sMatchedId = $oObjNode->getAttribute('data-object-key');
|
||||
$sMatchedName = trim($oObjNode->textContent);
|
||||
|
||||
// Ensure that what we found is actually configured as a mention
|
||||
$sMentionPrefix = array_search($sMatchedClass, $aMentionAllowedClasses);
|
||||
// - No direct configuration for the matched class, let's look for a configuration on parent classes
|
||||
if (false === $sMentionPrefix) {
|
||||
$bHasMentionConfigurationForParentClass = false;
|
||||
foreach (MetaModel::EnumParentClasses($sMatchedClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false) as $aMatchedParentClass) {
|
||||
$sMentionPrefix = array_search($aMatchedParentClass, $aMentionAllowedClasses);
|
||||
if (false !== $sMentionPrefix) {
|
||||
$bHasMentionConfigurationForParentClass = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (false === $bHasMentionConfigurationForParentClass) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// - Test if the name starts with $sMentionPrefix (e.g. '@' for 'Contact' class)
|
||||
if (false === str_starts_with($sMatchedName, $sMentionPrefix)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prepare array for matched class if not already present
|
||||
if (!array_key_exists($sMatchedClass, $aMentionedObjects)) {
|
||||
@@ -3190,4 +3223,74 @@ TXT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read memory limit from the php.ini file
|
||||
*
|
||||
* @return int Memory limit in bytes
|
||||
*/
|
||||
public static function GetMemoryLimit(): int
|
||||
{
|
||||
$sLimit = ini_get('memory_limit');
|
||||
if ($sLimit == '-1') {
|
||||
return 128 * 1048576;
|
||||
}
|
||||
switch (substr($sLimit, -1)) {
|
||||
case 'M':
|
||||
case 'm':
|
||||
return (int)$sLimit * 1048576;
|
||||
case 'K':
|
||||
case 'k':
|
||||
return (int)$sLimit * 1024;
|
||||
case 'G':
|
||||
case 'g':
|
||||
return (int)$sLimit * 1073741824;
|
||||
default:
|
||||
return (int)$sLimit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.3.0 N°9875
|
||||
* Open archive and raise appropriate exception.
|
||||
* Warning: do not forget to close archive afterwhile
|
||||
* @param string $sArchiveFilePath
|
||||
* @param int|null $flags
|
||||
* @return ZipArchive
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function ZipArchiveOpen(string $sArchiveFilePath, int|null $flags = null): ZipArchive
|
||||
{
|
||||
$oZip = new ZipArchive();
|
||||
if (is_null($flags)) {
|
||||
$code = $oZip->open($sArchiveFilePath);
|
||||
} else {
|
||||
$code = $oZip->open($sArchiveFilePath, $flags);
|
||||
}
|
||||
if (true !== $code) {
|
||||
//ZipArchive::ZIP_ER_NOZIP : 19
|
||||
if ($code === 19) {
|
||||
throw new \Exception(sprintf('Cannot open zip file due to inconsistent or empty content'));
|
||||
}
|
||||
|
||||
throw new \Exception(sprintf('Cannot open zip file due to error code %s', $code));
|
||||
}
|
||||
return $oZip;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.3.0 N°9875
|
||||
* @param string $sDirectory
|
||||
* @param string $sPrefix
|
||||
* @return ZipArchive
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function ZipArchiveCreateWithTempNam(string $sDirectory, string $sPrefix): ZipArchive
|
||||
{
|
||||
$sTempnam = tempnam($sDirectory, $sPrefix);
|
||||
unlink($sTempnam);
|
||||
$sArchiveName = $sTempnam.'.zip';
|
||||
|
||||
return self::ZipArchiveOpen($sArchiveName, ZipArchive::CREATE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"type": "project",
|
||||
"license": "AGPL-3.0-only",
|
||||
"require": {
|
||||
"php": ">=8.1.0 <8.5.0",
|
||||
"php": ">=8.2.0 <8.5.0",
|
||||
"ext-ctype": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-gd": "*",
|
||||
@@ -15,18 +15,21 @@
|
||||
"apereo/phpcas": "dev-master",
|
||||
"guzzlehttp/guzzle": "^7.5.1",
|
||||
"league/oauth2-google": "^4.0.1",
|
||||
"nikic/php-parser": "^4.14.0",
|
||||
"nikic/php-parser": "dev-master",
|
||||
"pear/archive_tar": "~1.4.14",
|
||||
"pelago/emogrifier": "^7.2.0",
|
||||
"psr/log": "^3.0.0",
|
||||
"scssphp/scssphp": "dev-combodo/1.x",
|
||||
"symfony/console": "~6.4.0",
|
||||
"symfony/dotenv": "~6.4.0",
|
||||
"symfony/form": "^6.4",
|
||||
"symfony/framework-bundle": "~6.4.0",
|
||||
"symfony/http-foundation": "~6.4.0",
|
||||
"symfony/http-kernel": "~6.4.0",
|
||||
"symfony/runtime": "~6.4.0",
|
||||
"symfony/security-csrf": "~6.4.0",
|
||||
"symfony/twig-bundle": "~6.4.0",
|
||||
"symfony/validator" : "~6.4.0",
|
||||
"symfony/var-dumper": "~6.4.0",
|
||||
"symfony/yaml": "~6.4.0",
|
||||
"symfony/mailer": "~6.4.0",
|
||||
@@ -40,6 +43,10 @@
|
||||
"symfony/web-profiler-bundle": "~6.4.0"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/Combodo/PHP-Parser"
|
||||
},
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/EsupPortail/phpCAS"
|
||||
@@ -60,7 +67,7 @@
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.1.0"
|
||||
"php": "8.2.0"
|
||||
},
|
||||
"vendor-dir": "lib",
|
||||
"preferred-install": {
|
||||
@@ -80,7 +87,6 @@
|
||||
"sources"
|
||||
],
|
||||
"exclude-from-classmap": [
|
||||
"application/twigextension.class.inc.php",
|
||||
"core/oql/build/PHP/",
|
||||
"core/apc-emulation.php",
|
||||
"application/startup.inc.php",
|
||||
@@ -100,7 +106,7 @@
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "3.4.*"
|
||||
"require": "6.4.*"
|
||||
},
|
||||
"runtime": {
|
||||
"dotenv_path": "resources/symfony/.env"
|
||||
|
||||
1249
composer.lock
generated
1249
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -916,7 +916,9 @@ class CMDBSource
|
||||
$aColumn = [];
|
||||
$aData = self::QueryToArray($sSql);
|
||||
foreach ($aData as $aRow) {
|
||||
@$aColumn[] = $aRow[$col];
|
||||
if ($aRow[$col] !== null) {
|
||||
$aColumn[] = $aRow[$col];
|
||||
}
|
||||
}
|
||||
return $aColumn;
|
||||
}
|
||||
@@ -1579,8 +1581,6 @@ class CMDBSource
|
||||
if (static::GetDBVendor() === static::ENUM_DB_VENDOR_MYSQL) {
|
||||
//Mysql 5.7.0 and upper deprecated --ssl and uses --ssl-mode instead
|
||||
return version_compare(static::GetDBVersion(), '5.7.11', '>=');
|
||||
} elseif (static::GetDBVendor() === static::ENUM_DB_VENDOR_MARIADB) {
|
||||
return version_compare(static::GetDBVersion(), '10.2.6', '>=');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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)";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -178,7 +178,7 @@ class EventNotificationEmail extends EventNotification
|
||||
|
||||
// Display lists
|
||||
MetaModel::Init_SetZListItems('details', ['date', 'userinfo', 'message', 'trigger_id', 'action_id', 'object_class', 'object_id', 'to', 'cc', 'bcc', 'from', 'subject', 'body', 'attachments']); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('list', ['date', 'message', 'to', 'subject', 'attachments']); // Attributes to be displayed for a list
|
||||
MetaModel::Init_SetZListItems('list', ['date', 'message', 'to', 'cc', 'bcc', 'subject', 'attachments']); // Attributes to be displayed for a list
|
||||
|
||||
// Search criteria
|
||||
// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,7 +22,6 @@ use Combodo\iTop\Application\EventRegister\ApplicationEvents;
|
||||
use Combodo\iTop\Core\MetaModel\FriendlyNameType;
|
||||
use Combodo\iTop\Service\Events\EventData;
|
||||
use Combodo\iTop\Service\Events\EventService;
|
||||
use Combodo\iTop\Setup\ModuleDependency\Module;
|
||||
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
|
||||
|
||||
require_once APPROOT.'core/modulehandler.class.inc.php';
|
||||
@@ -1643,10 +1642,10 @@ abstract class MetaModel
|
||||
if ($sFilterCode == 'id') {
|
||||
return true;
|
||||
}
|
||||
if (array_key_exists($sClass, self::$m_aMagicFields) && array_key_exists($sFilterCode, self::$m_aMagicFields[$sClass])) {
|
||||
if (array_key_exists($sClass, self::$m_aMagicFields) && in_array($sFilterCode, self::$m_aMagicFields[$sClass])) {
|
||||
return true;
|
||||
}
|
||||
if (array_key_exists($sClass, self::$m_aFilterForbiddenAttributes) && array_key_exists($sFilterCode, self::$m_aFilterForbiddenAttributes[$sClass])) {
|
||||
if (array_key_exists($sClass, self::$m_aFilterForbiddenAttributes) && in_array($sFilterCode, self::$m_aFilterForbiddenAttributes[$sClass])) {
|
||||
return false;
|
||||
}
|
||||
return self::IsValidAttCode($sClass, $sFilterCode);
|
||||
@@ -2964,8 +2963,9 @@ abstract class MetaModel
|
||||
self::$m_aMagicFields[] = $sClass;
|
||||
}
|
||||
self::$m_aMagicFields[$sClass][] = $sCode;
|
||||
self::$m_aAttribOrigins[$sClass][$sCode] = self::$m_aAttribOrigins[$sClass][$sAttCode];
|
||||
}
|
||||
if (!$oAttDef->IsSearchable()) {
|
||||
if (!$oAttDef->IsValidFilterAttribute()) {
|
||||
if (!array_key_exists($sClass, self::$m_aFilterForbiddenAttributes)) {
|
||||
self::$m_aFilterForbiddenAttributes[] = $sClass;
|
||||
}
|
||||
@@ -4660,12 +4660,15 @@ abstract class MetaModel
|
||||
* @throws \CoreException
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
public static function DBExists($bMustBeComplete = true)
|
||||
public static function DBExists($bMustBeComplete = true, ?string $sDBName = null)
|
||||
{
|
||||
if (!CMDBSource::IsDB(self::$m_sDBName)) {
|
||||
if (is_null($sDBName)) {
|
||||
$sDBName = self::$m_sDBName;
|
||||
}
|
||||
if (!CMDBSource::IsDB($sDBName)) {
|
||||
return false;
|
||||
}
|
||||
CMDBSource::SelectDB(self::$m_sDBName);
|
||||
CMDBSource::SelectDB($sDBName);
|
||||
|
||||
$aFound = [];
|
||||
$aMissing = [];
|
||||
@@ -5759,36 +5762,37 @@ abstract class MetaModel
|
||||
|
||||
self::$m_sEnvironment = $sEnvironment;
|
||||
|
||||
try {
|
||||
if (!defined('MODULESROOT')) {
|
||||
define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/');
|
||||
if (!defined('MODULESROOT')) {
|
||||
define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/');
|
||||
|
||||
self::$m_bTraceSourceFiles = $bTraceSourceFiles;
|
||||
self::$m_bTraceSourceFiles = $bTraceSourceFiles;
|
||||
|
||||
// $config can be either a filename, or a Configuration object (volatile!)
|
||||
if ($config instanceof Config) {
|
||||
self::LoadConfig($config, $bAllowCache);
|
||||
} else {
|
||||
self::LoadConfig(new Config($config), $bAllowCache);
|
||||
}
|
||||
|
||||
if ($bModelOnly) {
|
||||
return;
|
||||
}
|
||||
// $config can be either a filename, or a Configuration object (volatile!)
|
||||
if ($config instanceof Config) {
|
||||
self::LoadConfig($config, $bAllowCache);
|
||||
} else {
|
||||
self::LoadConfig(new Config($config), $bAllowCache);
|
||||
}
|
||||
|
||||
CMDBSource::SelectDB(self::$m_sDBName);
|
||||
|
||||
foreach (MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) {
|
||||
$oPHPClass::OnMetaModelStarted();
|
||||
if ($bModelOnly) {
|
||||
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
|
||||
EventService::InitService();
|
||||
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
|
||||
return;
|
||||
}
|
||||
|
||||
ExpressionCache::Warmup();
|
||||
} finally {
|
||||
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
|
||||
EventService::InitService();
|
||||
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
|
||||
}
|
||||
|
||||
CMDBSource::SelectDB(self::$m_sDBName);
|
||||
|
||||
foreach (MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) {
|
||||
$oPHPClass::OnMetaModelStarted();
|
||||
}
|
||||
|
||||
ExpressionCache::Warmup();
|
||||
|
||||
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
|
||||
EventService::InitService();
|
||||
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,15 +60,13 @@ class OQLActualClassTreeResolver
|
||||
$aTranslateFields = [];
|
||||
foreach ($aExpectedAttributes as $sAttCode => $oExpression) {
|
||||
// 'id' is managed later
|
||||
if ($sAttCode == 'id') {
|
||||
if ($sAttCode === 'id') {
|
||||
continue;
|
||||
}
|
||||
// Attributes can be stored in attributes list or for magic ones into filter codes list.
|
||||
$sOriginClass = null;
|
||||
if (MetaModel::IsValidAttCode($sClass, $sAttCode)) {
|
||||
if (MetaModel::IsValidAttCode($sClass, $sAttCode) || MetaModel::IsValidFilterCode($sClass, $sAttCode)) {
|
||||
$sOriginClass = MetaModel::GetAttributeOrigin($sClass, $sAttCode);
|
||||
} elseif ($sAttCode == 'id') {
|
||||
$sOriginClass = $sClass;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -340,13 +340,14 @@ class ormDocument
|
||||
* @param string $sContentDisposition Either 'inline' or 'attachment'
|
||||
* @param string $sSecretField The attcode of the field containing a "secret" to be provided in order to retrieve the file
|
||||
* @param string $sSecretValue The value of the secret to be compared with the value of the attribute $sSecretField
|
||||
* @param bool $bAllowAllData If true, no rights filtering is applied
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null)
|
||||
public static function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null, $bAllowAllData = false)
|
||||
{
|
||||
try {
|
||||
$oObj = MetaModel::GetObject($sClass, $id, false, false);
|
||||
$oObj = MetaModel::GetObject($sClass, $id, false, $bAllowAllData);
|
||||
if (!is_object($oObj)) {
|
||||
// If access to the document is not granted, check if the access to the host object is allowed
|
||||
$oObj = MetaModel::GetObject($sClass, $id, false, true);
|
||||
@@ -407,7 +408,7 @@ class ormDocument
|
||||
* @param int $iMaxImageHeight Maximum height for the resized image
|
||||
* @param array|null $aFinalDimensions Image dimensions after resizing or null if unable to read the image
|
||||
* @return ormDocument The resampled image
|
||||
*
|
||||
* @since 3.3.0 N°3124
|
||||
*/
|
||||
public function ResizeImageToFit(int $iMaxWidth, int $iMaxHeight, array|null &$aFinalDimensions = null): static
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -658,6 +658,16 @@ abstract class User extends cmdbAbstractObject
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow a user to be impersonated by another one (generally an administrator) in order to troubleshoot rights issues.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function CanBeImpersonated(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1063,6 +1073,9 @@ class UserRights
|
||||
$oUser = self::FindUser($sLogin);
|
||||
if ($oUser) {
|
||||
$bRet = true;
|
||||
if (!$oUser->CanBeImpersonated()) {
|
||||
throw new Exception($oUser->GetName().' cannot be impersonated');
|
||||
}
|
||||
if (is_null(self::$m_oRealUser)) {
|
||||
// First impersonation
|
||||
self::$m_oRealUser = self::$m_oUser;
|
||||
@@ -1517,6 +1530,37 @@ class UserRights
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $oUser
|
||||
* @param array $aExcludedProfilesId Administrator by default, but can also be other proofiles depending on needs (e.g. power portal user or REST profile)
|
||||
* @return bool
|
||||
* @throws ArchivedObjectException
|
||||
* @throws CoreException
|
||||
* @throws CoreUnexpectedValue
|
||||
* @throws MySQLException
|
||||
*/
|
||||
public static function IsUserReadOnly(User $oUser, array $aExcludedProfilesId = [1]): bool
|
||||
{
|
||||
$oUserProfiles = $oUser->Get('profile_list');
|
||||
$oUserRights = UserRights::GetModuleInstance();
|
||||
while ($oUserProfile = $oUserProfiles->Fetch()) {
|
||||
$iProfileId = $oUserProfile->Get('profileid');
|
||||
if (in_array($iProfileId, $aExcludedProfilesId)) {
|
||||
return false;
|
||||
}
|
||||
foreach (MetaModel::GetClasses('bizmodel,grant_by_profile') as $sClass) {
|
||||
foreach (['w', 'bw', 'd', 'bd'] as $sWriteActionCode) {
|
||||
$bIsGranted = $oUserRights->GetProfileActionGrant($iProfileId, $sClass, $sWriteActionCode);
|
||||
if ($bIsGranted === true) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sClass
|
||||
* @param int $iActionCode see UR_ACTION_* constants
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
@@ -185,16 +185,20 @@ $ibo-panel--is-selectable--body--after--font-size: $ibo-font-size-700 !default;
|
||||
.ibo-panel--icon-img, .ibo-panel--icon-background { // second class is deprecated, remove it when dealing with N°9317
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-position: center;
|
||||
object-fit: $ibo-panel--icon-img--size--must-contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: $ibo-panel--icon-img--size--must-contain;
|
||||
}
|
||||
|
||||
.ibo-panel--icon-img--must-contain, .ibo-panel--icon-background--must-contain { // second class is deprecated, remove it when dealing with N°9317
|
||||
object-fit: $ibo-panel--icon-img--size--must-contain;
|
||||
background-size: $ibo-panel--icon-img--size--must-contain;
|
||||
}
|
||||
|
||||
.ibo-panel--icon-img--must-cover, .ibo-panel--icon-background--must-cover { // second class is deprecated, remove it when dealing with N°9317
|
||||
object-fit: $ibo-panel--icon-img--size--must-cover;
|
||||
background-size: $ibo-panel--icon-img--size--must-cover;
|
||||
}
|
||||
|
||||
|
||||
@@ -247,7 +247,8 @@ $ibo-quick-create--compartment--placeholder-hint--text-color: $ibo-color-grey-70
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
@extend a;
|
||||
color: $ibo-hyperlink-color--on-hover;
|
||||
text-decoration: $ibo-hyperlink-text-decoration--on-hover;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
|
||||
@@ -51,19 +51,23 @@ $ibo-title--icon-img--size--must-zoomout: $ibo-title--icon-background--size--mus
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-position: center;
|
||||
object-fit: $ibo-title--icon-img--size--must-contain;
|
||||
background-size: $ibo-title--icon-img--size--must-contain;
|
||||
}
|
||||
|
||||
.ibo-title--icon-img--must-contain, .ibo-title--icon-background--must-contain { // second class is deprecated, remove it when dealing with N°9317
|
||||
object-fit: $ibo-title--icon-img--size--must-contain;
|
||||
background-size: $ibo-title--icon-img--size--must-contain;
|
||||
}
|
||||
|
||||
.ibo-title--icon-img--must-cover, .ibo-title--icon-background--must-cover { // second class is deprecated, remove it when dealing with N°9317
|
||||
object-fit: $ibo-title--icon-img--size--must-cover;
|
||||
background-size: $ibo-title--icon-img--size--must-cover;
|
||||
}
|
||||
|
||||
.ibo-title--icon-img--must-zoomout, .ibo-title--icon-background--must-zoomout { // second class is deprecated, remove it when dealing with N°9317
|
||||
background-size: $ibo-title--icon-img--size--must-zoomout;
|
||||
width: $ibo-title--icon-img--size--must-zoomout;
|
||||
height: $ibo-title--icon-img--size--must-zoomout;
|
||||
}
|
||||
|
||||
.ibo-title--for-object-details {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -62,4 +62,15 @@ $ibo-extension-details--actions--button--padding-x: $ibo-button--padding-x !defa
|
||||
|
||||
.ibo-extension-details--actions:has(.toggler-install:not(:disabled)) .ibo-popover-menu--section a[data-resource-id="force_uninstall"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-extension-details .ibo-popover-menu ~ .ibo-button{
|
||||
visibility: hidden;
|
||||
}
|
||||
.ibo-extension-details .ibo-popover-menu:has(.ibo-popover-menu--item) ~ .ibo-button{
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.ibo-extension-details .ibo-toggler--wrapper:has(.ibo-toggler.ibo-is-hidden){
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,29 @@
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
$ibo-datamodel-viewer--viewer-empty--width: max(20%, #{$ibo-size-700}) !default;
|
||||
|
||||
$ibo-datamodel-viewer--parent--spacer--padding-y: $ibo-spacing-0 !default;
|
||||
$ibo-datamodel-viewer--parent--spacer--padding-x: $ibo-spacing-300 !default;
|
||||
|
||||
$ibo-datamodel-viewer--breadcrumb--margin: $ibo-spacing-200 0 !default;
|
||||
|
||||
$ibo-datamodel-viewer--classname--font-family: monospace !default;
|
||||
$ibo-datamodel-viewer--classname--color: $ibo-color-blue-grey-600 !default;
|
||||
|
||||
$ibo-datamodel-viewer--tag--category--color: $ibo-color-blue-grey-800 !default;
|
||||
$ibo-datamodel-viewer--tag--category--icon--color: $ibo-color-blue-grey-600 !default;
|
||||
|
||||
$ibo-datamodel-viewer--abstract-class-icon--margin-right: $ibo-spacing-300 !default;
|
||||
$ibo-datamodel-viewer--abstract-class-icon--after--content: 'A' !default;
|
||||
$ibo-datamodel-viewer--abstract-class-icon--after--width: 15px !default;
|
||||
$ibo-datamodel-viewer--abstract-class-icon--after--height: 15px !default;
|
||||
$ibo-datamodel-viewer--abstract-class-icon--after--line-height: 14px !default;
|
||||
$ibo-datamodel-viewer--abstract-class-icon--after--border-radius: 50% !default;
|
||||
$ibo-datamodel-viewer--abstract-class-icon--after--border: 1px solid $ibo-color-cyan-500 !default;
|
||||
$ibo-datamodel-viewer--abstract-class-icon--after--color: $ibo-color-white-100 !default;
|
||||
$ibo-datamodel-viewer--abstract-class-icon--after--background-color: $ibo-color-cyan-500 !default;
|
||||
|
||||
$ibo-datamodel-viewer--classes-list--selectize-input--background-color: $ibo-color-white-100 !default;
|
||||
$ibo-datamodel-viewer--classes-list--selectize-input--color: $ibo-color-grey-800 !default;
|
||||
$ibo-datamodel-viewer--classes-list--selectize-input--border-color: $ibo-color-grey-500 !default;
|
||||
@@ -16,8 +36,9 @@ $ibo-datamodel-viewer--origin-cell--diameter: 8px !default;
|
||||
$ibo-datamodel-viewer--origin-cell--border-radius: $ibo-border-radius-full !default;
|
||||
|
||||
$ibo-datamodel-viewer--classes-list--height: 100% !default;
|
||||
$ibo-datamodel-viewer--classes-list--width: 350px !default;
|
||||
$ibo-datamodel-viewer--classes-list--width: $ibo-size-700 !default;
|
||||
$ibo-datamodel-viewer--classes-list--padding-left: $ibo-spacing-600 !default;
|
||||
$ibo-datamodel-viewer--classes-list--padding-top: $ibo-spacing-300 !default;
|
||||
|
||||
$ibo-datamodel-viewer--lifecycle--code--color: $ibo-color-grey-700 !default;
|
||||
$ibo-datamodel-viewer--lifecycle--stimuli--color: $ibo-color-blue-900 !default;
|
||||
@@ -45,6 +66,56 @@ $ibo-datamodel-viewer--lifecycle-image--margin-bottom: $ibo-spacing-500 !default
|
||||
.ibo-panel--subtitle{
|
||||
@extend %ibo-font-ral-nor-150;
|
||||
}
|
||||
.ibo-datamodel-viewer--empty{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
svg{
|
||||
width: $ibo-datamodel-viewer--viewer-empty--width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-datamodel-viewer--empty--text {
|
||||
@extend %ibo-font-ral-med-400
|
||||
}
|
||||
|
||||
.ibo-datamodel-viewer--breadcrumb{
|
||||
@extend %ibo-font-ral-med-150;
|
||||
margin: $ibo-datamodel-viewer--breadcrumb--margin;
|
||||
.ibo-button{
|
||||
text-transform: none; // unset uppercase
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-datamodel-viewer--classname{
|
||||
color: $ibo-datamodel-viewer--classname--color;
|
||||
@extend %ibo-font-code-100;
|
||||
}
|
||||
|
||||
.ibo-datamodel-viewer--tag--category{
|
||||
color: $ibo-datamodel-viewer--tag--category--color;
|
||||
.ibo-object-details--tag-icon{
|
||||
color:$ibo-datamodel-viewer--tag--category--icon--color;
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-datamodel-viewer--icon--abstract{
|
||||
&:after{
|
||||
content: $ibo-datamodel-viewer--abstract-class-icon--after--content;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: $ibo-datamodel-viewer--abstract-class-icon--after--width;
|
||||
height: $ibo-datamodel-viewer--abstract-class-icon--after--height;
|
||||
line-height: $ibo-datamodel-viewer--abstract-class-icon--after--line-height;
|
||||
border-radius: $ibo-datamodel-viewer--abstract-class-icon--after--border-radius;
|
||||
border: $ibo-datamodel-viewer--abstract-class-icon--after--border;
|
||||
color: $ibo-datamodel-viewer--abstract-class-icon--after--color;
|
||||
background-color: $ibo-datamodel-viewer--abstract-class-icon--after--background-color;
|
||||
|
||||
@extend %ibo-font-ral-bol-50;
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-datamodel-viewer--parent--spacer{
|
||||
@@ -80,6 +151,7 @@ $ibo-datamodel-viewer--lifecycle-image--margin-bottom: $ibo-spacing-500 !default
|
||||
height: $ibo-datamodel-viewer--classes-list--height;
|
||||
width: $ibo-datamodel-viewer--classes-list--width;
|
||||
padding-left: $ibo-datamodel-viewer--classes-list--padding-left;
|
||||
padding-top: $ibo-datamodel-viewer--classes-list--padding-top;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
|
||||
16
css/backoffice/vendors/ckeditor/_overloads.scss
vendored
16
css/backoffice/vendors/ckeditor/_overloads.scss
vendored
@@ -28,6 +28,15 @@ $ibo-vendors-ckeditor--ck-mentions--item--padding-x: $ibo-spacing-300 !default;
|
||||
$ibo-vendors-ckeditor--ck-mentions--item--padding-y: $ibo-spacing-200 !default;
|
||||
|
||||
/* CSS3 variables */
|
||||
:root {
|
||||
--ck-content-font-family: inherit;
|
||||
--ck-content-font-color: inherit;
|
||||
--ck-content-font-size: inherit;
|
||||
--ck-content-line-height: inherit;
|
||||
--ck-content-word-break: inherit;
|
||||
--ck-content-overflow-wrap: inherit;
|
||||
}
|
||||
|
||||
.ck {
|
||||
--ck-color-list-button-on-background: #{$ibo-vendors-ckeditor--ck-color-list-button-on-background};
|
||||
--ck-color-list-button-on-background-focus: #{$ibo-vendors-ckeditor--ck-color-list-button-on-background-focus};
|
||||
@@ -61,7 +70,7 @@ $ibo-vendors-ckeditor--ck-mentions--item--padding-y: $ibo-spacing-200 !default;
|
||||
}
|
||||
}
|
||||
|
||||
.ck-editor__editable_inline:not(.ck-comment__input *) {
|
||||
.ck-editor__editable_inline:not(.ck-comment__input *), .ck-source-editing-area {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
@@ -75,11 +84,6 @@ $ibo-vendors-ckeditor--ck-mentions--item--padding-y: $ibo-spacing-200 !default;
|
||||
}
|
||||
}
|
||||
|
||||
// N°7552 Allow source editing area to be scrollable in full screen
|
||||
.ck-maximize_editor_main .ck-source-editing-area textarea{
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
/* Mentions */
|
||||
.ck-mentions {
|
||||
.ck-button {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
@@ -611,32 +611,6 @@ body {
|
||||
.setup-extension--missing .setup-extension--icon{
|
||||
color:#a00000;
|
||||
}
|
||||
.setup-extension-tag {
|
||||
display: inline-flex;
|
||||
background-color: grey;
|
||||
border-radius: 8px;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
margin-right: 3px;
|
||||
&.installed{
|
||||
background-color:#9eff9e
|
||||
}
|
||||
&.notinstalled{
|
||||
background-color:#ed9eff
|
||||
}
|
||||
&.tobeinstalled{
|
||||
background-color:#9ef0ff
|
||||
}
|
||||
&.tobeuninstalled{
|
||||
background-color:#ff9e9e
|
||||
}
|
||||
&.notuninstallable{
|
||||
background-color:#ffc98c
|
||||
}
|
||||
&.removed{
|
||||
background-color: #969594
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-extension-details {
|
||||
align-items: flex-start;
|
||||
@@ -699,11 +673,10 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
#progress_content {
|
||||
height: 200px;
|
||||
overflow: auto;
|
||||
text-align: center;
|
||||
#progress_content *:not(.message) + .message {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
#fresh_content{
|
||||
border: 0;
|
||||
min-height: 300px;
|
||||
|
||||
@@ -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 服务器认证',
|
||||
]);
|
||||
|
||||
@@ -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.'外部验证身份',
|
||||
|
||||
@@ -97,18 +97,19 @@
|
||||
$bDebug = isset($aServerParams['debug']) ? $aServerParams['debug'] : false;
|
||||
}
|
||||
|
||||
if (array_key_exists(LDAP_OPT_DEBUG_LEVEL, $aOptions))
|
||||
{
|
||||
// Set debug level before trying to connect, so that debug info appear in the PHP error log if ldap_connect goes wrong
|
||||
$bRet = ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, $aOptions[LDAP_OPT_DEBUG_LEVEL]);
|
||||
$this->LogInfo($bDebug, "ldap_set_option('LDAP_OPT_DEBUG_LEVEL', '{$aOptions[LDAP_OPT_DEBUG_LEVEL]}') returned ".($bRet ? 'true' : 'false'));
|
||||
}
|
||||
|
||||
$hDS = @ldap_connect($sURI);
|
||||
if ($hDS === false)
|
||||
{
|
||||
$this->LogIssue($bDebug, "ldap_authentication: can not connect to the LDAP server '$sURI'. Check the configuration file config-itop.php.");
|
||||
return false;
|
||||
}
|
||||
if (array_key_exists(LDAP_OPT_DEBUG_LEVEL, $aOptions))
|
||||
{
|
||||
// Set debug level before trying to connect, so that debug info appear in the PHP error log if ldap_connect goes wrong
|
||||
$bRet = ldap_set_option($hDS, LDAP_OPT_DEBUG_LEVEL, $aOptions[LDAP_OPT_DEBUG_LEVEL]);
|
||||
$this->LogInfo($bDebug, "ldap_set_option('LDAP_OPT_DEBUG_LEVEL', '{$aOptions[LDAP_OPT_DEBUG_LEVEL]}') returned ".($bRet ? 'true' : 'false'));
|
||||
}
|
||||
foreach($aOptions as $name => $value)
|
||||
{
|
||||
$bRet = ldap_set_option($hDS, $name, $value);
|
||||
|
||||
@@ -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+' => '',
|
||||
]);
|
||||
|
||||
@@ -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+' => '',
|
||||
]);
|
||||
|
||||
@@ -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' => 'Установка срока действия пароля "Одноразовый пароль" для своей собственной учётной записи не разрешена',
|
||||
]);
|
||||
|
||||
@@ -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' => '不允许用户为自己设置 "一次性密码" 的失效期限',
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
*
|
||||
*/
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'theme:darkmoon' => 'Dark moon~~',
|
||||
'theme:darkmoon' => 'Тёмная луна',
|
||||
]);
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
*/
|
||||
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'theme:fullmoon-high-contrast' => 'Fullmoon (High contrast)~~',
|
||||
'theme:fullmoon-high-contrast' => 'Fullmoon (высокая контрастность)',
|
||||
]);
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
*/
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'theme:fullmoon-high-contrast' => 'Fullmoon (High contrast)~~',
|
||||
'theme:fullmoon-high-contrast' => 'Fullmoon (高对比度)',
|
||||
]);
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
*/
|
||||
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (Protanopia & Deuteranopia)~~',
|
||||
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (протанопия и дейтеранопия)',
|
||||
]);
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
*/
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (Protanopia & Deuteranopia)~~',
|
||||
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (红绿色盲 & 绿色色盲)',
|
||||
]);
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
*/
|
||||
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'theme:fullmoon-tritanopia' => 'Fullmoon (Tritanopia)~~',
|
||||
'theme:fullmoon-tritanopia' => 'Fullmoon (тританопия)',
|
||||
]);
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
*/
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'theme:fullmoon-tritanopia' => 'Fullmoon (Tritanopia)~~',
|
||||
'theme:fullmoon-tritanopia' => 'Fullmoon (蓝黄色盲)',
|
||||
]);
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Combodo\\iTop\\DataFeatureRemoval\\": "src",
|
||||
"": "src/NoNamespace"
|
||||
"Combodo\\iTop\\DataFeatureRemoval\\": "src"
|
||||
}
|
||||
},
|
||||
"name": "combodo/combodo-data-feature-removal",
|
||||
|
||||
@@ -1,127 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.3">
|
||||
<classes>
|
||||
<class id="DataFeatureRemovalBackgroundOperation" _delta="define">
|
||||
<class id="ResourceDataFeatureRemoval" _delta="define">
|
||||
<parent>AbstractResource</parent>
|
||||
<properties>
|
||||
<category>bizmodel,searchable</category>
|
||||
<abstract>false</abstract>
|
||||
<db_table>datafeatureremovalbackgroundoperation</db_table>
|
||||
<style>
|
||||
<icon>
|
||||
<fileref ref="icons8-electricity_643cf7fd7a024968679dc0c35a710a03"/>
|
||||
</icon>
|
||||
<main_color>#0000ff</main_color>
|
||||
</style>
|
||||
<naming>
|
||||
<attributes/>
|
||||
</naming>
|
||||
<reconciliation>
|
||||
<attributes/>
|
||||
</reconciliation>
|
||||
<uniqueness_rules>
|
||||
<rule id="list_of_classes">
|
||||
<attributes>
|
||||
<attribute id="classes"/>
|
||||
</attributes>
|
||||
<filter><![CDATA[]]></filter>
|
||||
<disabled>false</disabled>
|
||||
<is_blocking>true</is_blocking>
|
||||
</rule>
|
||||
</uniqueness_rules>
|
||||
<comment>/* Extension management access control. */</comment>
|
||||
<abstract>true</abstract>
|
||||
<category>grant_by_profile</category>
|
||||
</properties>
|
||||
<fields>
|
||||
<field id="creation_date" xsi:type="AttributeDateTime">
|
||||
<sql>creation_date</sql>
|
||||
<default_value/>
|
||||
<is_null_allowed>false</is_null_allowed>
|
||||
<dependencies/>
|
||||
<tracking_level>all</tracking_level>
|
||||
</field>
|
||||
<field id="features_code" xsi:type="AttributeText">
|
||||
<sql>features_code</sql>
|
||||
<default_value/>
|
||||
<is_null_allowed>false</is_null_allowed>
|
||||
<validation_pattern/>
|
||||
<width/>
|
||||
<height/>
|
||||
<dependencies/>
|
||||
<tracking_level>all</tracking_level>
|
||||
</field>
|
||||
<field id="classes" xsi:type="AttributeText">
|
||||
<sql>classes</sql>
|
||||
<default_value/>
|
||||
<is_null_allowed>false</is_null_allowed>
|
||||
<validation_pattern/>
|
||||
<width/>
|
||||
<height/>
|
||||
<dependencies/>
|
||||
<tracking_level>all</tracking_level>
|
||||
</field>
|
||||
</fields>
|
||||
<presentation/>
|
||||
<methods/>
|
||||
<presentation>
|
||||
<list>
|
||||
<items>
|
||||
<item id="creation_date">
|
||||
<rank>10</rank>
|
||||
</item>
|
||||
<item id="features_code">
|
||||
<rank>20</rank>
|
||||
</item>
|
||||
<item id="classes">
|
||||
<rank>30</rank>
|
||||
</item>
|
||||
</items>
|
||||
</list>
|
||||
<search>
|
||||
<items/>
|
||||
</search>
|
||||
<details>
|
||||
<items>
|
||||
<item id="creation_date">
|
||||
<rank>10</rank>
|
||||
</item>
|
||||
<item id="features_code">
|
||||
<rank>20</rank>
|
||||
</item>
|
||||
<item id="classes">
|
||||
<rank>30</rank>
|
||||
</item>
|
||||
</items>
|
||||
</details>
|
||||
</presentation>
|
||||
<parent>cmdbAbstractObject</parent>
|
||||
</class>
|
||||
</classes>
|
||||
<dictionaries>
|
||||
<dictionary id="EN US">
|
||||
<entries>
|
||||
<entry id="Class:DataFeatureRemovalBackgroundOperation/Name" _delta="define"><![CDATA[]]></entry>
|
||||
<entry id="Class:DataFeatureRemovalBackgroundOperation/ComplementaryName" _delta="define"><![CDATA[]]></entry>
|
||||
<entry id="Class:DataFeatureRemovalBackgroundOperation" _delta="define"><![CDATA[DataFeatureRemovalBackgroundOperation]]></entry>
|
||||
<entry id="Class:DataFeatureRemovalBackgroundOperation+" _delta="define"><![CDATA[]]></entry>
|
||||
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:creation_date" _delta="define"><![CDATA[Creation date]]></entry>
|
||||
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:creation_date+" _delta="define"><![CDATA[]]></entry>
|
||||
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:features_code" _delta="define"><![CDATA[Features code]]></entry>
|
||||
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:features_code+" _delta="define"><![CDATA[]]></entry>
|
||||
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:classes" _delta="define"><![CDATA[Classes]]></entry>
|
||||
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:classes+" _delta="define"><![CDATA[]]></entry>
|
||||
</entries>
|
||||
</dictionary>
|
||||
</dictionaries>
|
||||
<files>
|
||||
<file id="icons8-electricity_643cf7fd7a024968679dc0c35a710a03" xsi:type="File" _delta="define_if_not_exists">
|
||||
<name>images/icons/icons8-electricity.svg</name>
|
||||
<mime_type>image/svg+xml</mime_type>
|
||||
<data>PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSIyNDBweCIgaGVpZ2h0PSIyNDBweCI+PHBhdGggZmlsbD0iIzM3YzZkMCIgZD0iTTI2LjQ1MTIsMzdsNy43NzY0Ni0xNS41NTI5MWExLDEsMCwwLDAtLjg5NDM1LTEuNDQ3MjJMMjIuOTUxMiwxOS45OTkyNWw2LjMzNi0xMy41NzYzNUExLDEsMCwwLDAsMjguMzgxLDVIMjIuNTk2MzlhMSwxLDAsMCwwLS45MTExMS41ODc4M2wtOC41OTUyNCwxOUExLDEsMCwwLDAsMTQuMDAxMTUsMjZoMTAuNDVsLTUuNSwxMVoiLz48cGF0aCBmaWxsPSIjMzdjNmQwIiBkPSJNMTYuMjIyODYsMzdIMjkuODY0NzFhLjU1NzM1LjU1NzM1LDAsMCwxLC4zNTgxNSwxTDE5Ljk2LDQ0LjM2N2ExLjAzMzY0LDEuMDMzNjQsMCwwLDEtMS40OTEyMS0uNDI1ODZMMTUuNzIyODYsMzcuNzVBLjUwNDUuNTA0NSwwLDAsMSwxNi4yMjI4NiwzN1oiLz48L3N2Zz4=</data>
|
||||
</file>
|
||||
</files>
|
||||
<menus>
|
||||
<menu id="DataFeatureRemovalMenu" xsi:type="WebPageMenuNode" _delta="define">
|
||||
<rank>30</rank>
|
||||
<parent>SystemTools</parent>
|
||||
<url>$pages/exec.php?exec_module=combodo-data-feature-removal&exec_page=index.php&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>
|
||||
|
||||
@@ -10,40 +10,51 @@
|
||||
*/
|
||||
|
||||
Dict::Add('EN US', 'English', 'English', [
|
||||
'Menu:DataFeatureRemovalMenu' => 'Features Removal',
|
||||
'combodo-data-feature-removal/Operation:Main/Title' => 'Features Removal',
|
||||
'Menu:DataFeatureRemovalMenu' => 'Extension management',
|
||||
'combodo-data-feature-removal/Operation:Main/Title' => 'Extension management',
|
||||
|
||||
'DataFeatureRemoval:Main:Title' => 'Features Removal',
|
||||
'DataFeatureRemoval:Main:SubTitle' => 'Prepare features you want to enable/disable in a future setup',
|
||||
'DataFeatureRemoval:Failure:Title' => 'Feature dry removal errors',
|
||||
'DataFeatureRemoval:Helper:Title' => 'Enable or disable features that are installed in your iTop.',
|
||||
'DataFeatureRemoval:Helper:Desc1' => 'It will prepare the setup step that proceeds to feature enabling or disabling.',
|
||||
'DataFeatureRemoval:Helper:Desc2' => 'Analyze if there are any data or dependency preventing you from enabling/disabling a feature.',
|
||||
'DataFeatureRemoval:Main:Title' => 'Extension management',
|
||||
'DataFeatureRemoval:Main:SubTitle' => 'Toggle extensions installed on your iTop',
|
||||
'DataFeatureRemoval:Failure:Title' => 'Extensions dry removal errors',
|
||||
'DataFeatureRemoval:Helper:Title' => 'Analyze if there are any data or dependency preventing you from adding/removing an extension.',
|
||||
|
||||
'DataFeatureRemoval:Features:Title' => 'Features',
|
||||
'DataFeatureRemoval:Features:Title' => 'Extensions',
|
||||
'DataFeatureRemoval:Result:Title' => 'Modification requested',
|
||||
'DataFeatureRemoval:NoResult:Title' => 'No modification requested',
|
||||
'DataFeatureRemoval:Execution:Title' => 'Deletion Executions',
|
||||
'DataFeatureRemoval:Analysis:Title' => 'Analysis result',
|
||||
'DataFeatureRemoval:Analysis:Subtitle' => 'Review all elements requiring attention',
|
||||
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s element(s) to clean before continuing',
|
||||
'DataFeatureRemoval:Analysis:Ok' => "No data to cleanup",
|
||||
|
||||
'DataFeatureRemoval:DeletionPlan:Title' => 'Deletion plan',
|
||||
'DataFeatureRemoval:DeletionPlan:Title' => 'Data deletion plan',
|
||||
'DataFeatureRemoval:DeletionPlan:SubTitle' => '%1$s rows to clean before continuing',
|
||||
'DataFeatureRemoval:DoDeletion:Title' => 'Do deletion',
|
||||
'DataFeatureRemoval:DoDeletion:SubTitle' => 'Remove all the entries from the database',
|
||||
'DataFeatureRemoval:DeletionPlan:ToManyOperations' => 'Too many entries to clean',
|
||||
'DataFeatureRemoval:DeletionPlan:Error:Issues' => 'Some objects must be deleted manually prior to cleanup',
|
||||
|
||||
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Element to remove',
|
||||
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Feature name',
|
||||
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Extension name',
|
||||
'DataFeatureRemoval:Table:Analysis:Module' => 'Module name',
|
||||
'DataFeatureRemoval:Table:Analysis:Occurrence' => 'Occurrence',
|
||||
|
||||
'DataFeatureRemoval:CleanupComplete:Title' => 'All clear.',
|
||||
'DataFeatureRemoval:CompilComplete' => 'Compilation successful. No Cleanup needed. You can proceed to setup.',
|
||||
|
||||
'DataFeatureRemoval:Compile:InProgress' => 'Compilation in progress...',
|
||||
'DataFeatureRemoval:Compile:Success' => 'Compilation successful',
|
||||
'DataFeatureRemoval:Compile:Error' => 'Compilation error',
|
||||
|
||||
'DataFeatureRemoval:RunAudit:InProgress' => 'Analysis in progress...',
|
||||
'DataFeatureRemoval:RunAudit:Success' => 'Analysis completed',
|
||||
'DataFeatureRemoval:RunAudit:Error' => 'Error during analysis',
|
||||
|
||||
'UI:Button:Analyze' => 'Analyze',
|
||||
'UI:Button:ModifyChoices' => 'Modify Choices',
|
||||
'UI:Button:ModifyChoices' => 'Change my selection',
|
||||
'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup',
|
||||
'UI:Button:PlanDeletion' => 'Prepare deletion plan',
|
||||
'UI:Button:DoDeletion' => 'Delete data',
|
||||
'UI:Button:DoAsyncDeletion' => 'Do asynchronous deletion',
|
||||
'UI:Button:BackToMain' => 'Back to Feature Removal',
|
||||
'UI:Button:Setup' => 'Back to setup',
|
||||
'UI:Button:PlanDeletion' => 'Proceed with deletion',
|
||||
'UI:Button:DoDeletion' => 'Proceed with deletion',
|
||||
'UI:Button:BackToMain' => 'Change my selection',
|
||||
'UI:Button:Setup' => 'Run setup',
|
||||
|
||||
'UI:Action:ForceUninstall' => 'Force uninstall',
|
||||
'UI:Action:MoreInfo' => 'More information',
|
||||
@@ -53,7 +64,7 @@ Dict::Add('EN US', 'English', 'English', [
|
||||
'DataFeatureRemoval:Column:Class' => 'Class',
|
||||
'DataFeatureRemoval:Column:DeleteCount' => 'Entries to delete',
|
||||
'DataFeatureRemoval:Column:UpdateCount' => 'Entries to update',
|
||||
'DataFeatureRemoval:Column:Issue' => 'Issue',
|
||||
'DataFeatureRemoval:Column:IssueCount' => 'Issues found preventing automatic cleanup',
|
||||
|
||||
'DataFeatureRemoval:Column:DeletedCount' => 'Deleted entries',
|
||||
'DataFeatureRemoval:Column:UpdatedCount' => 'Updated entries',
|
||||
|
||||
@@ -10,40 +10,51 @@
|
||||
*/
|
||||
|
||||
Dict::Add('FR FR', 'French', 'Français', [
|
||||
'Menu:DataFeatureRemovalMenu' => 'Suppression de fonctionnalités',
|
||||
'combodo-data-feature-removal/Operation:Main/Title' => 'Suppression de fonctionnalités',
|
||||
'Menu:DataFeatureRemovalMenu' => 'Gestion des extensions',
|
||||
'combodo-data-feature-removal/Operation:Main/Title' => 'Gestion des extensions',
|
||||
|
||||
'DataFeatureRemoval:Main:Title' => 'Suppression de fonctionnalités',
|
||||
'DataFeatureRemoval:Main:SubTitle' => 'Préparez les fonctionnalités que vous souhaitez activer ou désactiver lors d’une prochaine configuration',
|
||||
'DataFeatureRemoval:Failure:Title' => 'Erreurs lors de la simulation de suppression de fonctionnalités',
|
||||
'DataFeatureRemoval:Helper:Title' => 'Activez ou désactivez les fonctionnalités installées dans votre iTop.',
|
||||
'DataFeatureRemoval:Helper:Desc1' => 'Cette étape prépare l’assistant de configuration à activer ou désactiver des fonctionnalités.',
|
||||
'DataFeatureRemoval:Helper:Desc2' => 'Analyse si des données ou des dépendances empêchent l’activation ou la désactivation d’une fonctionnalité.',
|
||||
'DataFeatureRemoval:Main:Title' => 'Gestion des extensions',
|
||||
'DataFeatureRemoval:Main:SubTitle' => 'Sélectionner les extensions à installer sur votre iTop',
|
||||
'DataFeatureRemoval:Failure:Title' => 'Erreurs lors de la simulation de suppression d\'extensions',
|
||||
'DataFeatureRemoval:Helper:Title' => 'Activez ou désactivez les extensions installées dans votre iTop.',
|
||||
|
||||
'DataFeatureRemoval:Features:Title' => 'Fonctionnalités',
|
||||
'DataFeatureRemoval:Features:Title' => 'Extensions',
|
||||
'DataFeatureRemoval:Result:Title' => 'Modification demandée',
|
||||
'DataFeatureRemoval:NoResult:Title' => 'Aucune modification demandée',
|
||||
'DataFeatureRemoval:Execution:Title' => 'Suppressions',
|
||||
'DataFeatureRemoval:Analysis:Title' => 'Résultat de l’analyse',
|
||||
'DataFeatureRemoval:Analysis:Subtitle' => 'Vérifier les éléments à nettoyer',
|
||||
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s élément(s) à nettoyer avant de poursuivre',
|
||||
'DataFeatureRemoval:Analysis:Ok' => "Aucune donnée à nettoyer",
|
||||
|
||||
'DataFeatureRemoval:DeletionPlan:Title' => 'Plan de suppression',
|
||||
'DataFeatureRemoval:DeletionPlan:Title' => 'Plan de suppression des données',
|
||||
'DataFeatureRemoval:DeletionPlan:SubTitle' => '%1$s ligne(s) à nettoyer avant de poursuivre',
|
||||
'DataFeatureRemoval:DoDeletion:Title' => 'Exécuter la suppression',
|
||||
'DataFeatureRemoval:DoDeletion:SubTitle' => 'Supprime toutes les entrées de la base de données',
|
||||
'DataFeatureRemoval:DeletionPlan:ToManyOperations' => 'Trop d’entrées à nettoyer',
|
||||
'DataFeatureRemoval:DeletionPlan:Error:Issues' => 'Certains objets doivent être supprimés manuellement avant le nettoyage',
|
||||
|
||||
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Élément à supprimer',
|
||||
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Fonctionnalité',
|
||||
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Extension',
|
||||
'DataFeatureRemoval:Table:Analysis:Module' => 'Module',
|
||||
'DataFeatureRemoval:Table:Analysis:Occurrence' => 'Occurrence',
|
||||
|
||||
'DataFeatureRemoval:CleanupComplete:Title' => 'All clear.',
|
||||
'DataFeatureRemoval:CompilComplete' => 'Compilation successful. No Cleanup needed. You can proceed to setup.',
|
||||
|
||||
'DataFeatureRemoval:Compile:InProgress' => 'Compilation en cours...',
|
||||
'DataFeatureRemoval:Compile:Success' => 'Compilation terminée',
|
||||
'DataFeatureRemoval:Compile:Error' => 'Erreur lors de la compilation',
|
||||
|
||||
'DataFeatureRemoval:RunAudit:InProgress' => 'Analyse en cours...',
|
||||
'DataFeatureRemoval:RunAudit:Success' => 'Analyse terminée',
|
||||
'DataFeatureRemoval:RunAudit:Error' => 'Erreur lors de l\'analyse',
|
||||
|
||||
'UI:Button:Analyze' => 'Analyser',
|
||||
'UI:Button:ModifyChoices' => 'Modifier les choix',
|
||||
'UI:Button:ModifyChoices' => 'Modifier la sélection',
|
||||
'UI:Button:AnalyzeAndSetup' => 'Analyser et ouvrir l’assistant de configuration',
|
||||
'UI:Button:PlanDeletion' => 'Préparer le plan de suppression',
|
||||
'UI:Button:DoAsyncDeletion' => 'Lancer la suppression en tâche de fond',
|
||||
'UI:Button:PlanDeletion' => 'Supprimer les données',
|
||||
'UI:Button:DoDeletion' => 'Supprimer les données',
|
||||
'UI:Button:BackToMain' => 'Retour à la suppression de fonctionnalités',
|
||||
'UI:Button:Setup' => 'Retour à l’assistant de configuration',
|
||||
'UI:Button:BackToMain' => 'Modifier la sélection',
|
||||
'UI:Button:Setup' => 'Lancer le setup',
|
||||
|
||||
'UI:Action:ForceUninstall' => 'Forcer la désinstallation',
|
||||
'UI:Action:MoreInfo' => 'Plus d’informations',
|
||||
@@ -53,7 +64,7 @@ Dict::Add('FR FR', 'French', 'Français', [
|
||||
'DataFeatureRemoval:Column:Class' => 'Classe',
|
||||
'DataFeatureRemoval:Column:DeleteCount' => 'Entrées à supprimer',
|
||||
'DataFeatureRemoval:Column:UpdateCount' => 'Entrées à mettre à jour',
|
||||
'DataFeatureRemoval:Column:Issue' => 'Problème',
|
||||
'DataFeatureRemoval:Column:IssueCount' => 'Problèmes empêchant le nettoyage automatique',
|
||||
|
||||
'DataFeatureRemoval:Column:DeletedCount' => 'Entrées supprimées',
|
||||
'DataFeatureRemoval:Column:UpdatedCount' => 'Entrées mises à jour',
|
||||
|
||||
@@ -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' => 'Обновлено записей',
|
||||
]);
|
||||
@@ -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' => '已更新的条目',
|
||||
]);
|
||||
@@ -31,7 +31,6 @@ SetupWebPage::AddModule(
|
||||
'datamodel' => [
|
||||
'vendor/autoload.php',
|
||||
'model.combodo-data-feature-removal.php', // Contains the PHP code generated by the "compilation" of datamodel.combodo-data-feature-removal.xml
|
||||
'src/Hook/DataFeatureRemovalBackgroundTask.php',
|
||||
],
|
||||
'webservice' => [],
|
||||
'data.struct' => [
|
||||
|
||||
@@ -8,51 +8,64 @@
|
||||
namespace Combodo\iTop\DataFeatureRemoval\Controller;
|
||||
|
||||
require_once APPROOT.'setup/feature_removal/SetupAudit.php';
|
||||
require_once APPROOT.'setup/feature_removal/DryRemovalRuntimeEnvironment.php';
|
||||
|
||||
use Combodo\iTop\Application\Helper\Session;
|
||||
use Combodo\iTop\Application\TwigBase\Controller\Controller;
|
||||
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalConfig;
|
||||
use Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity;
|
||||
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
|
||||
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
|
||||
use Combodo\iTop\DataFeatureRemoval\Service\BackgroundOperationService;
|
||||
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
|
||||
use Combodo\iTop\DataFeatureRemoval\Service\DataCleanupService;
|
||||
use Combodo\iTop\DataFeatureRemoval\Service\DataFeatureRemoverExtensionService;
|
||||
use Combodo\iTop\DataFeatureRemoval\Service\DeletionPlanService;
|
||||
use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment;
|
||||
use Combodo\iTop\DataFeatureRemoval\Service\StaticDeletionPlan;
|
||||
use Combodo\iTop\Service\Session\SessionParameters;
|
||||
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
|
||||
use ContextTag;
|
||||
use CoreException;
|
||||
use Dict;
|
||||
use Exception;
|
||||
use IssueLog;
|
||||
use MetaModel;
|
||||
use MFCompiler;
|
||||
use RunTimeEnvironment;
|
||||
use SetupUtils;
|
||||
use utils;
|
||||
|
||||
class DataFeatureRemovalController extends Controller
|
||||
{
|
||||
private array $aSelectedExtensionsForCheck = [];
|
||||
private ?array $aExtensionsToCheck = null;
|
||||
private bool $bForcedUninstallation = false;
|
||||
private array $aCountClassesToCleanup = [];
|
||||
private array $aAnalysisDataTable = [];
|
||||
private array $aDeletionExecutionSummary = [];
|
||||
|
||||
private int $iCount = 0;
|
||||
private int $iColumnCount = 2;
|
||||
private RunTimeEnvironment $oRuntimeEnvironment;
|
||||
|
||||
public function OperationMain($sErrorMessage = null): void
|
||||
{
|
||||
$aParams = [];
|
||||
|
||||
$this->ReadRemovedExtensions();
|
||||
SetupUtils::EraseSetupToken();
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
$oParameters->Erase();
|
||||
Session::Unset('aDeletionExecutionSummary');
|
||||
Session::Set('bForceCompilation', true);
|
||||
$oParameters->SetParameter('return_application', 'DataFeatureRemoval');
|
||||
|
||||
$this->AddAnalyzeParams();
|
||||
$aParams['sTransactionId'] = utils::GetNewTransactionId();
|
||||
$aParams['aExtensions'] = $this->GetExtensionsTable();
|
||||
$aParams['aExtensionsCode'] = $this->aSelectedExtensionsForCheck;
|
||||
$aParams['iColumnCount'] = $this->iColumnCount;
|
||||
$aParams['aAvailableExtensions'] = $this->SplitArrayIntoColumns($this->GetAvailableExtensions(), $this->iColumnCount);
|
||||
$aParams['aAnalysisDataTable'] = $this->aAnalysisDataTable;
|
||||
$aParams['aClasses'] = array_keys($this->aCountClassesToCleanup);
|
||||
$aParams['DataFeatureRemovalErrorMessage'] = $sErrorMessage;
|
||||
$aParams['bHasData'] = $this->iCount > 0;
|
||||
$aParams['sSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup';
|
||||
$aParams['iCount'] = $this->iCount;
|
||||
$aParams['DataFeatureRemovalErrorMessage'] = $sErrorMessage;
|
||||
$aParams['bAnalysisOk'] = (count($this->aCountClassesToCleanup) > 0) && ($this->iCount === 0);
|
||||
|
||||
$this->AddLinkedStylesheet(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/css/DataFeatureRemoval.css');
|
||||
$this->AddLinkedScript(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/js/DataFeatureRemoval.js');
|
||||
|
||||
$this->m_sOperation = "Main";
|
||||
$this->DisplayPage($aParams);
|
||||
}
|
||||
|
||||
@@ -73,143 +86,312 @@ class DataFeatureRemovalController extends Controller
|
||||
$this->aAnalysisDataTable = $this->GetTableData('Analysis', $aColumns, $aData);
|
||||
}
|
||||
|
||||
public function OperationAnalyze(): void
|
||||
{
|
||||
$this->ReadRemovedExtensions();
|
||||
|
||||
$this->m_sOperation = 'Main';
|
||||
|
||||
try {
|
||||
if (count($this->aSelectedExtensionsForCheck) > 0) {
|
||||
$this->Analyze();
|
||||
}
|
||||
$this->OperationMain();
|
||||
} catch (Exception $e) {
|
||||
IssueLog::Error(__METHOD__, null, ['stack' => $e->getTraceAsString(), 'exception' => $e->getMessage()]);
|
||||
$this->OperationMain($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function Analyze(): void
|
||||
{
|
||||
$sSourceEnv = MetaModel::GetEnvironment();
|
||||
$oDryRemovalRuntimeEnvironment = new DryRemovalRuntimeEnvironment($sSourceEnv, $this->aSelectedExtensionsForCheck);
|
||||
$oDryRemovalRuntimeEnvironment->CompileFrom($sSourceEnv);
|
||||
|
||||
$oSetupAudit = new SetupAudit($sSourceEnv);
|
||||
$aGetRemovedClasses = $oSetupAudit->RunDataAudit();
|
||||
IssueLog::Debug(__METHOD__, null, ['aGetRemovedClasses' => $aGetRemovedClasses]);
|
||||
$this->aCountClassesToCleanup = $aGetRemovedClasses;
|
||||
}
|
||||
|
||||
public function OperationDeletionPlan(): void
|
||||
public function OperationAnalysisResult(): void
|
||||
{
|
||||
$aParams = [];
|
||||
$this->ValidateTransactionId();
|
||||
|
||||
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
|
||||
//from setup wizard/mtp
|
||||
if (!SetupUtils::IsSessionSetupTokenValid()) {
|
||||
//from same module
|
||||
$this->ValidateTransactionId();
|
||||
}
|
||||
|
||||
$aDeletionPlanSummaryEntities = DeletionPlanService::GetInstance()->GetDeletionPlanSummary($aClasses);
|
||||
$aColumns = ['Class', 'DeleteCount' , 'UpdateCount', 'Issue'];
|
||||
// Display changed extensions
|
||||
$aSetupParameterNames = [
|
||||
'selected_extensions' => '[]',
|
||||
'selected_modules' => '[]',
|
||||
'display_choices' => '',
|
||||
'added_extensions' => '[]',
|
||||
'removed_extensions' => '[]',
|
||||
'extensions_not_uninstallable' => '[]',
|
||||
'copy_setup_files' => 1,
|
||||
'force-uninstall' => '',
|
||||
'use_symbolic_links' => MFCompiler::UseSymbolicLinks() ? 'on' : '',
|
||||
'return_application' => '',
|
||||
'target_env' => ITOP_DEFAULT_ENV,
|
||||
];
|
||||
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
foreach ($aSetupParameterNames as $sInputName => $defaultValue) {
|
||||
$oParameters->SetParameter($sInputName, $oParameters->GetParameter($sInputName, $defaultValue));
|
||||
}
|
||||
|
||||
$aAddedExtensions = json_decode($oParameters->GetParameter('added_extensions', '[]'), true);
|
||||
$aRemovedExtensions = json_decode($oParameters->GetParameter('removed_extensions', '[]'), true);
|
||||
if ('[]' === $oParameters->GetParameter('selected_modules', '[]')) {
|
||||
//it does not come from setup
|
||||
// we get extensions from 1st screen uiblocks
|
||||
$this->ReadExtensionsDiff();
|
||||
$oParameters->SetParameter('force-uninstall', $this->bForcedUninstallation ? 'on' : '');
|
||||
$aAddedExtensions = $this->aExtensionsToCheck['to_be_installed'];
|
||||
$oParameters->SetParameter('added_extensions', $this->ConvertIntoSetupFormat($aAddedExtensions));
|
||||
|
||||
$aRemovedExtensions = $this->aExtensionsToCheck['to_be_removed'];
|
||||
$oParameters->SetParameter('removed_extensions', $this->ConvertIntoSetupFormat($aRemovedExtensions));
|
||||
|
||||
$aExtensionsNotUninstallable = $this->aExtensionsToCheck['extensions_not_uninstallable'];
|
||||
$oParameters->SetParameter('extensions_not_uninstallable', $this->ConvertIntoSetupFormat($aExtensionsNotUninstallable));
|
||||
}
|
||||
|
||||
DataFeatureRemovalLog::Debug(__METHOD__.' Extensions given in parameter', null, [
|
||||
'added_extensions' => $aAddedExtensions,
|
||||
'removed_extensions' => $aRemovedExtensions]);
|
||||
|
||||
$aParams['sTransactionId'] = utils::GetNewTransactionId();
|
||||
$aParams['iColumnCount'] = $this->iColumnCount;
|
||||
$aAvailableExtensions = $this->GetExtensionsDiff($aAddedExtensions, $aRemovedExtensions);
|
||||
$aParams['aAvailableExtensionsCount'] = count($aAvailableExtensions);
|
||||
$aParams['aAvailableExtensions'] = $this->SplitArrayIntoColumns($aAvailableExtensions, $this->iColumnCount);
|
||||
$aParams['sAjaxURL'] = utils::GetAbsoluteUrlModulePage(DataFeatureRemovalHelper::MODULE_NAME, 'index.php');
|
||||
|
||||
$this->DisplayPage($aParams, 'AnalysisResult');
|
||||
}
|
||||
|
||||
private function ConvertIntoSetupFormat(array $aData): string
|
||||
{
|
||||
$aNewData = [];
|
||||
foreach ($aData as $sCode => $sLabel) {
|
||||
$aNewData[] = sprintf('"%s":"%s"', $sCode, $sLabel);
|
||||
}
|
||||
|
||||
return "{".implode(',', $aNewData)."}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws \ConfigException
|
||||
* @throws \CoreException
|
||||
*/
|
||||
public function OperationAjaxCompile(): void
|
||||
{
|
||||
$aParams = [];
|
||||
//to make setup redirection work, we need to pass complex data structures to setup wizards (ie extension/module lists)
|
||||
$sSourceEnv = MetaModel::GetEnvironment();
|
||||
$oRuntimeEnvironment = new RunTimeEnvironment($sSourceEnv, false);
|
||||
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
$aSelectedModules = json_decode($oParameters->GetParameter('selected_modules', '[]'), true);
|
||||
$aSelectedExtensions = json_decode($oParameters->GetParameter('selected_extensions', '[]'), true);
|
||||
$aAddedExtensions = json_decode($oParameters->GetParameter('added_extensions', '[]'), true);
|
||||
$aRemovedExtensions = json_decode($oParameters->GetParameter('removed_extensions', '[]'), true);
|
||||
|
||||
try {
|
||||
$this->ValidateTransactionId();
|
||||
|
||||
$oConfig = MetaModel::GetConfig();
|
||||
if (count($aSelectedModules) === 0) {
|
||||
$aSelectedExtensions = DataFeatureRemoverExtensionService::GetInstance()->GetExtensionMap()->GetSelectedExtensions($oConfig, array_keys($aAddedExtensions), array_keys($aRemovedExtensions));
|
||||
$oParameters->SetParameter('selected_extensions', $this->ConvertIntoSetupFormat($aSelectedExtensions));
|
||||
}
|
||||
|
||||
$sBuildDir = APPROOT."/env-$sSourceEnv-build";
|
||||
if (! is_dir($sBuildDir)) {
|
||||
SetupUtils::builddir($sBuildDir);
|
||||
}
|
||||
$bIsDirEmpty = count(scandir($sBuildDir)) === 2;
|
||||
$bForceCompilation = Session::Get('bForceCompilation', false);
|
||||
if ($bIsDirEmpty || $bForceCompilation) {
|
||||
$oRuntimeEnvironment->CopySetupFiles();
|
||||
if (count($aSelectedModules) === 0) {
|
||||
$aSelectedModules = $oRuntimeEnvironment->GetModulesToLoadFromChoices($oConfig, $aSelectedExtensions);
|
||||
}
|
||||
|
||||
DataFeatureRemovalLog::Debug(
|
||||
__METHOD__,
|
||||
null,
|
||||
['sSourceEnv' => $sSourceEnv, 'sBuildDir' => $sBuildDir, 'bIsDirEmpty' => $bIsDirEmpty, glob("$sBuildDir/*")]
|
||||
);
|
||||
$oRuntimeEnvironment->DoCompile($aSelectedExtensions, $aRemovedExtensions, $aSelectedModules, MFCompiler::CanUseSymbolicLinks());
|
||||
Session::Unset('bForceCompilation');
|
||||
} else {
|
||||
if (count($aSelectedModules) === 0) {
|
||||
$aSelectedModules = $oRuntimeEnvironment->GetModulesToLoadFromChoices($oConfig, $aSelectedExtensions);
|
||||
}
|
||||
}
|
||||
} catch (CoreException $e) {
|
||||
$aParams['error_message'] = $e->getHtmlDesc();
|
||||
} catch (Exception $e) {
|
||||
$aParams['error_message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
$aParams['success_message'] = Dict::S('DataFeatureRemoval:Compile:Success');
|
||||
$aParams['transaction_id'] = utils::GetNewTransactionId();
|
||||
|
||||
$oParameters->SetParameter('selected_modules', json_encode($aSelectedModules));
|
||||
$this->DisplayJSONPage($aParams);
|
||||
}
|
||||
|
||||
public function OperationAjaxRunAudit(): void
|
||||
{
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
$aParams = [];
|
||||
$aPageParams = [];
|
||||
try {
|
||||
$this->ValidateTransactionId();
|
||||
|
||||
$sSourceEnv = MetaModel::GetEnvironment();
|
||||
$oSetupAudit = new SetupAudit($sSourceEnv);
|
||||
$aRemovedClasses = array_keys($oSetupAudit->RunDataAudit());
|
||||
$oParameters->SetParameter('classes', $aRemovedClasses);
|
||||
|
||||
new ContextTag(ContextTag::TAG_SETUP);
|
||||
$aPageParams['sLaunchSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup/wizard.php';
|
||||
$aPageParams['aSetupParams'] = [
|
||||
"_class" => "WizStepLandingBeforeAudit",
|
||||
"operation" => "next",
|
||||
];
|
||||
$aPageParams['sTransactionId'] = utils::GetNewTransactionId();
|
||||
|
||||
$aDeletionPlanSummaryEntities = $this->GetDeletionPlanSummaryEntities($aRemovedClasses);
|
||||
[$aPageParams['aDeletionPlanSummary'], $aPageParams['iQueryCount'], $aPageParams['bDeletionPossible']] = $this->GetDeletionPlanSummaryTable($aDeletionPlanSummaryEntities);
|
||||
[$aPageParams['aDeletionExecutionSummary'], $aPageParams['bHasDeletionExecution']] = $this->GetExecutionSummaryTable();
|
||||
$aPageParams['bDeletionNeeded'] = ($aPageParams['iQueryCount'] > 0);
|
||||
|
||||
if (!$aPageParams['bDeletionNeeded']) {
|
||||
// Erase session setup parameters
|
||||
SetupUtils::CreateSetupToken();
|
||||
}
|
||||
|
||||
$this->DisplayAjaxPage($aPageParams, 'AjaxRunAudit');
|
||||
return;
|
||||
} catch (CoreException $e) {
|
||||
$aParams['error_message'] = $e->getHtmlDesc();
|
||||
} catch (Exception $e) {
|
||||
$aParams['error_message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
$this->DisplayJSONPage($aParams);
|
||||
}
|
||||
|
||||
private function GetExecutionSummaryTable(): array
|
||||
{
|
||||
$sName = 'ExecutionSummary';
|
||||
|
||||
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary') ?? serialize([]));
|
||||
|
||||
$aTableData = [];
|
||||
if (count($this->aDeletionExecutionSummary) === 0) {
|
||||
return [$aTableData, false];
|
||||
}
|
||||
|
||||
$aColumns = ['Class', 'Total Deleted Count' , 'Total Updated Count', 'Deleted Count' , 'Updated Count'];
|
||||
$aRows = [];
|
||||
/** @var DataCleanupSummaryEntity $oSummary */
|
||||
foreach ($this->aDeletionExecutionSummary as $sClass => $oSummary) {
|
||||
$aRows[] = [
|
||||
$sClass,
|
||||
$oSummary->iTotalDeleteCount,
|
||||
$oSummary->iTotalUpdateCount,
|
||||
$oSummary->iDeleteCount,
|
||||
$oSummary->iUpdateCount,
|
||||
];
|
||||
}
|
||||
|
||||
$aTableData = $this->GetTableData($sName, $aColumns, $aRows);
|
||||
|
||||
return [$aTableData, true];
|
||||
|
||||
}
|
||||
|
||||
private function GetDeletionPlanSummaryEntities(array $aRemovedClasses): array
|
||||
{
|
||||
$oDataCleanupService = new StaticDeletionPlan();
|
||||
return $oDataCleanupService->GetCleanupSummary($aRemovedClasses);
|
||||
}
|
||||
|
||||
private function GetDeletionPlanSummaryTable(array $aDeletionPlanSummaryEntities): array
|
||||
{
|
||||
$sName = 'DeletionPlanSummary';
|
||||
$aColumns = ['Class', 'Delete Count' , 'Update Count', 'Issue Count'];
|
||||
$aRows = [];
|
||||
$iQueryCount = 0;
|
||||
$bHasIssues = false;
|
||||
foreach ($aDeletionPlanSummaryEntities as $oDeletionPlanSummaryEntity) {
|
||||
$aRows[] = [
|
||||
$oDeletionPlanSummaryEntity->sClass,
|
||||
$oDeletionPlanSummaryEntity->iDeleteCount,
|
||||
$oDeletionPlanSummaryEntity->iUpdateCount,
|
||||
$oDeletionPlanSummaryEntity->sIssue ?? '',
|
||||
$oDeletionPlanSummaryEntity->iIssueCount,
|
||||
];
|
||||
$bHasIssues |= ($oDeletionPlanSummaryEntity->iIssueCount !== 0);
|
||||
$iQueryCount += $oDeletionPlanSummaryEntity->iDeleteCount;
|
||||
$iQueryCount += $oDeletionPlanSummaryEntity->iUpdateCount;
|
||||
}
|
||||
|
||||
$aParams['sTransactionId'] = utils::GetNewTransactionId();
|
||||
$aParams['aDeletionPlanSummary'] = $this->GetTableData('Extensions', $aColumns, $aRows);
|
||||
$aParams['aClasses'] = $aClasses;
|
||||
$aParams['aExtensionsCode'] = utils::ReadPostedParam('aExtensionsCode', []);
|
||||
$aParams['iQueryCount'] = $iQueryCount;
|
||||
$aParams['bDeletionPossible'] = ($iQueryCount <= DataFeatureRemovalConfig::GetInstance()->Get('max_count_estimation_for_safe_cleanup', 100));
|
||||
|
||||
$this->DisplayPage($aParams);
|
||||
return [$this->GetTableData($sName, $aColumns, $aRows), $iQueryCount, !$bHasIssues];
|
||||
}
|
||||
|
||||
public function OperationDoDeletion(): void
|
||||
{
|
||||
$aParams = [];
|
||||
$this->ValidateTransactionId();
|
||||
|
||||
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
|
||||
$sDeletionButtonValue = utils::ReadPostedParam('btn_deletion', null);
|
||||
$bAsynchronous = ('async_deletion' === $sDeletionButtonValue);
|
||||
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary') ?? serialize([]));
|
||||
Session::Unset('aDeletionExecutionSummary');
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
$aClasses = $oParameters->GetParameter('classes', []);
|
||||
|
||||
if ($bAsynchronous) {
|
||||
BackgroundOperationService::GetInstance()->CreateOperation(utils::ReadPostedParam('aExtensionsCode', []), $aClasses);
|
||||
$this->OperationMain();
|
||||
return;
|
||||
$oDataCleanupService = new DataCleanupService();
|
||||
$aDeletionExecutionSummary = $oDataCleanupService->ExecuteCleanup($aClasses);
|
||||
foreach ($aDeletionExecutionSummary as $sClass => $oExecutionSummary) {
|
||||
if (!array_key_exists($sClass, $this->aDeletionExecutionSummary)) {
|
||||
$this->aDeletionExecutionSummary[$sClass] = new DataCleanupSummaryEntity($sClass);
|
||||
}
|
||||
$oSummary = $this->aDeletionExecutionSummary[$sClass];
|
||||
$oSummary->iDeleteCount = $oExecutionSummary->iDeleteCount;
|
||||
$oSummary->iUpdateCount = $oExecutionSummary->iUpdateCount;
|
||||
$oSummary->iTotalDeleteCount += $oExecutionSummary->iDeleteCount;
|
||||
$oSummary->iTotalUpdateCount += $oExecutionSummary->iUpdateCount;
|
||||
}
|
||||
|
||||
$aDeletionExecutionSummary = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses);
|
||||
$aColumns = ['Class', 'DeletedCount' , 'UpdatedCount'];
|
||||
$aRows = [];
|
||||
foreach ($aDeletionExecutionSummary as $oDeletionExecutionSummaryEntity) {
|
||||
$aRows[] = [
|
||||
$oDeletionExecutionSummaryEntity->sClass,
|
||||
$oDeletionExecutionSummaryEntity->iDeleteCount,
|
||||
$oDeletionExecutionSummaryEntity->iUpdateCount,
|
||||
];
|
||||
}
|
||||
|
||||
$aParams['sTransactionId'] = utils::GetNewTransactionId();
|
||||
$aParams['aDeletionExecutionSummary'] = $this->GetTableData('Extensions', $aColumns, $aRows);
|
||||
$this->DisplayPage($aParams);
|
||||
Session::Set('aDeletionExecutionSummary', serialize($this->aDeletionExecutionSummary));
|
||||
$this->OperationAnalysisResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get installed extensions from disk
|
||||
*
|
||||
* @return array structure for twig datatable
|
||||
*/
|
||||
private function GetExtensionsTable(): array
|
||||
private function GetAvailableExtensions(bool $bIncludePackageExtensions = false): array
|
||||
{
|
||||
$aExtensions = [];
|
||||
$aColumns = ['', 'Version', 'Name', 'Code'];
|
||||
$aExtensionsData = [];
|
||||
if ($bIncludePackageExtensions) {
|
||||
$aExtensionsRef = DataFeatureRemoverExtensionService::GetInstance()->GetExtensionMap()->GetAllExtensionsWithPreviouslyInstalled();
|
||||
} else {
|
||||
$aExtensionsRef = DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions();
|
||||
}
|
||||
|
||||
foreach (DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions() as $sCode => $oExtension) {
|
||||
foreach ($aExtensionsRef as $oExtension) {
|
||||
/** @var \iTopExtension $oExtension */
|
||||
$aExtensionsData[$oExtension->sCode] = [
|
||||
'version' => $oExtension->sVersion,
|
||||
'label' => $oExtension->sLabel,
|
||||
'code' => $oExtension->sCode,
|
||||
'description' => $oExtension->sDescription,
|
||||
'source' => $oExtension->GetExtensionSourceLabel(),
|
||||
'installed' => $oExtension->bInstalled,
|
||||
'extra_flags' => [
|
||||
'uninstallable' => $oExtension->CanBeUninstalled(),
|
||||
'remote' => $oExtension->IsRemote(),
|
||||
'missing' => $oExtension->bRemovedFromDisk,
|
||||
'dependency_issue' => $oExtension->HasDependencyIssue(),
|
||||
],
|
||||
|
||||
$bCleanupOngoing = BackgroundOperationService::GetInstance()->IsExtensionBeingCleaned($sCode);
|
||||
$sChecked = '';
|
||||
$sDisabledHtml = '';
|
||||
|
||||
$sLabel = $oExtension->sLabel;
|
||||
if ($bCleanupOngoing) {
|
||||
$sDisabledHtml = 'disabled=""';
|
||||
$sLabel .= <<<HTML
|
||||
<span class="ibo-spinner ibo-is-inline ibo-spinner ibo-block" data-role="ibo-spinner"><i class="ibo-spinner--icon fas fa-sync-alt fa-spin" aria-hidden="true"/></span>
|
||||
HTML;
|
||||
;
|
||||
} elseif ($oExtension->bRemovedFromDisk) {
|
||||
$sDisabledHtml = 'disabled=""';
|
||||
$sChecked = 'checked';
|
||||
} elseif (in_array($sCode, $this->aSelectedExtensionsForCheck)) {
|
||||
$sChecked = 'checked';
|
||||
}
|
||||
|
||||
$sVersion = $oExtension->sVersion;
|
||||
$sIdEnable = "aExtensions[$sCode][enable]";
|
||||
|
||||
$aExtensions[] = [
|
||||
<<<HTML
|
||||
<input type="checkbox" $sDisabledHtml class="extension_check" $sChecked id="$sIdEnable" name="$sIdEnable"/>
|
||||
HTML,
|
||||
$sVersion,
|
||||
$sLabel,
|
||||
$sCode,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->GetTableData('Extensions', $aColumns, $aExtensions);
|
||||
return $aExtensionsData;
|
||||
}
|
||||
|
||||
private function GetExtensionsDiff(array $aAddedExtensions, array $aRemovedExtensions): array
|
||||
{
|
||||
$aExtensions = [];
|
||||
foreach ($this->GetAvailableExtensions(true) as $sCode => $aExtension) {
|
||||
$aExtension['extra_flags']['disabled'] = true;
|
||||
if (isset($aAddedExtensions[$sCode])) {
|
||||
$aExtension['extra_flags']['selected'] = true;
|
||||
$aExtensions[$sCode] = $aExtension;
|
||||
} elseif (isset($aRemovedExtensions[$sCode])) {
|
||||
$aExtension['extra_flags']['selected'] = false;
|
||||
$aExtensions[$sCode] = $aExtension;
|
||||
}
|
||||
}
|
||||
|
||||
return $aExtensions;
|
||||
}
|
||||
|
||||
private function GetTableData(string $sTableName, array $aColumns, array $aData): array
|
||||
@@ -246,36 +428,67 @@ HTML,
|
||||
}
|
||||
|
||||
$sTransactionId = utils::ReadPostedParam('transaction_id', null, utils::ENUM_SANITIZATION_FILTER_TRANSACTION_ID);
|
||||
IssueLog::Debug(__FUNCTION__.": Transaction [$sTransactionId]");
|
||||
DataFeatureRemovalLog::Debug(__FUNCTION__.": Transaction [$sTransactionId]");
|
||||
if (empty($sTransactionId) || !utils::IsTransactionValid($sTransactionId, false)) {
|
||||
throw new DataFeatureRemovalException(Dict::S("iTopUpdate:Error:InvalidToken"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* Read extensions selected from posted parameters
|
||||
*/
|
||||
public function ReadRemovedExtensions(): array
|
||||
public function ReadExtensionsDiff(): void
|
||||
{
|
||||
if (count($this->aSelectedExtensionsForCheck) > 0) {
|
||||
return $this->aSelectedExtensionsForCheck;
|
||||
if (!is_null($this->aExtensionsToCheck)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$aSelectedExtensionsFromUI = utils::ReadPostedParam('aExtensions', []);
|
||||
foreach ($aSelectedExtensionsFromUI as $sCode => $aData) {
|
||||
$sValue = $aData['enable'] ?? 'off';
|
||||
if (($sValue) === 'on') {
|
||||
$this->aSelectedExtensionsForCheck[] = $sCode;
|
||||
$aAvailableExtensions = $this->GetAvailableExtensions();
|
||||
$aSelectedExtensionsFromUI = utils::ReadPostedParam('aSelectedExtensions', []);
|
||||
$this->aExtensionsToCheck = [
|
||||
'to_be_installed' => [],
|
||||
'to_be_removed' => [],
|
||||
'extensions_not_uninstallable' => [],
|
||||
];
|
||||
foreach ($aAvailableExtensions as $sCode => &$aExtensionData) {
|
||||
if (!isset($aSelectedExtensionsFromUI[$sCode])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($aExtensionData['installed'] && $aSelectedExtensionsFromUI[$sCode] !== 'on') {
|
||||
$aExtensionData['extra_flags']['selected'] = false;
|
||||
$sLabel = $aAvailableExtensions[$sCode]['label'];
|
||||
$this->aExtensionsToCheck['to_be_removed'][$sCode] = $sLabel;
|
||||
if (! $this->bForcedUninstallation && $aExtensionData['extra_flags']['uninstallable']) {
|
||||
$this->bForcedUninstallation = true;
|
||||
}
|
||||
if (false === $aExtensionData['extra_flags']['uninstallable'] || true === $aExtensionData['extra_flags']['remote']) {
|
||||
$this->aExtensionsToCheck['extensions_not_uninstallable'][] = $sCode;
|
||||
}
|
||||
} elseif (!$aExtensionData['installed'] && $aSelectedExtensionsFromUI[$sCode] === 'on') {
|
||||
$aExtensionData['extra_flags']['selected'] = true;
|
||||
$sLabel = $aAvailableExtensions[$sCode]['label'];
|
||||
$this->aExtensionsToCheck['to_be_installed'][$sCode] = $sLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add source removed to check
|
||||
foreach (DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions() as $sCode => $oExtension) {
|
||||
if ($oExtension->bRemovedFromDisk) {
|
||||
$this->aSelectedExtensionsForCheck[] = $sCode;
|
||||
}
|
||||
/**
|
||||
* Divide an array into several sub arrays, distributing elements so that every sub array has an equal amount of elements
|
||||
* @param mixed[] $aInput
|
||||
* @param int $iColNumber
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
private function SplitArrayIntoColumns(array $aInput, int $iColNumber)
|
||||
{
|
||||
$aOutput = array_fill(0, $iColNumber, []);
|
||||
$iIndex = 0;
|
||||
foreach ($aInput as $mItem) {
|
||||
//Split extensions in $iColNumber columns
|
||||
$aOutput[$iIndex % $this->iColumnCount][] = $mItem;
|
||||
$iIndex++;
|
||||
}
|
||||
|
||||
return $this->aSelectedExtensionsForCheck;
|
||||
return $aOutput;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,14 @@
|
||||
|
||||
namespace Combodo\iTop\DataFeatureRemoval\Entity;
|
||||
|
||||
class DeletionPlanSummaryEntity
|
||||
class DataCleanupSummaryEntity
|
||||
{
|
||||
public string $sClass;
|
||||
|
||||
/**
|
||||
* @var int : DEL_MANUAL|DEL_AUTO|DEL_SILENT|DEL_MOVEUP|DEL_NONE
|
||||
* @see \AttributeDefinition DEL_xxx
|
||||
*/
|
||||
public int $iMode = 0;
|
||||
public ?string $sIssue = null;
|
||||
public int $iIssueCount = 0;
|
||||
public int $iUpdateCount = 0;
|
||||
public int $iDeleteCount = 0;
|
||||
public int $iTotalUpdateCount = 0;
|
||||
public int $iTotalDeleteCount = 0;
|
||||
|
||||
/**
|
||||
* @param string $sClass
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
namespace Combodo\iTop\DataFeatureRemoval\Helper;
|
||||
|
||||
use Config;
|
||||
use MetaModel;
|
||||
use utils;
|
||||
|
||||
@@ -21,11 +20,11 @@ class DataFeatureRemovalConfig
|
||||
|
||||
final public static function GetInstance(): DataFeatureRemovalConfig
|
||||
{
|
||||
if (!isset(static::$oInstance)) {
|
||||
static::$oInstance = new DataFeatureRemovalConfig();
|
||||
if (!isset(self::$oInstance)) {
|
||||
self::$oInstance = new DataFeatureRemovalConfig();
|
||||
}
|
||||
|
||||
return static::$oInstance;
|
||||
return self::$oInstance;
|
||||
}
|
||||
|
||||
public function Get(string $sParamName, $default = null)
|
||||
@@ -50,22 +49,4 @@ class DataFeatureRemovalConfig
|
||||
$oConfig = utils::GetConfig();
|
||||
$oConfig->SetModuleSetting(DataFeatureRemovalHelper::MODULE_NAME, $sParamName, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Config|null $oConfig
|
||||
*
|
||||
* @return void
|
||||
* @throws \ConfigException
|
||||
* @throws \CoreException
|
||||
*/
|
||||
public function SaveItopConfiguration(Config $oConfig = null)
|
||||
{
|
||||
if (is_null($oConfig)) {
|
||||
$oConfig = utils::GetConfig();
|
||||
}
|
||||
$sConfigFile = APPROOT.'conf/'.utils::GetCurrentEnvironment().'/config-itop.php';
|
||||
@chmod($sConfigFile, 0770); // Allow overwriting the file
|
||||
$oConfig->WriteToFile($sConfigFile);
|
||||
@chmod($sConfigFile, 0444); // Read-only
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,4 @@ namespace Combodo\iTop\DataFeatureRemoval\Helper;
|
||||
class DataFeatureRemovalHelper
|
||||
{
|
||||
public const MODULE_NAME = 'combodo-data-feature-removal';
|
||||
|
||||
public static function IsTimeLimitExceeded(int $iUnixTimeLimit): bool
|
||||
{
|
||||
if ($iUnixTimeLimit === 0) {
|
||||
//no time limit
|
||||
return false;
|
||||
}
|
||||
|
||||
return (time() > $iUnixTimeLimit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalConfig;
|
||||
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
|
||||
use Combodo\iTop\DataFeatureRemoval\Service\BackgroundOperationService;
|
||||
use Combodo\iTop\DataFeatureRemoval\Service\DeletionPlanService;
|
||||
|
||||
class DataFeatureRemovalBackgroundTask implements iBackgroundProcess
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function GetPeriodicity()
|
||||
{
|
||||
return DataFeatureRemovalConfig::GetInstance()->Get('cron_periodicity_in_s', 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function Process($iUnixTimeLimit)
|
||||
{
|
||||
$iCount = 0;
|
||||
while ($oBackgroundOperation = BackgroundOperationService::GetInstance()->GetNext()) {
|
||||
$aClasses = BackgroundOperationService::GetInstance()->GetClasses($oBackgroundOperation);
|
||||
$aRes = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses, $iUnixTimeLimit);
|
||||
|
||||
IssueLog::Info(__METHOD__, null, $aRes);
|
||||
|
||||
IssueLog::Debug(__METHOD__, null, [
|
||||
'$iUnixTimeLimit' => $iUnixTimeLimit,
|
||||
'time' => time(),
|
||||
'timeout reached' => DataFeatureRemovalHelper::IsTimeLimitExceeded($iUnixTimeLimit),
|
||||
]);
|
||||
|
||||
if (DataFeatureRemovalHelper::IsTimeLimitExceeded($iUnixTimeLimit)) {
|
||||
//timeout reached
|
||||
return "Handled $iCount operations.";
|
||||
}
|
||||
|
||||
//execution finished before timeout: nothing left to remove
|
||||
$oBackgroundOperation->DBDelete();
|
||||
$iCount++;
|
||||
}
|
||||
|
||||
return "Handled $iCount operations.";
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\DataFeatureRemoval\Service;
|
||||
|
||||
use DataFeatureRemovalBackgroundOperation;
|
||||
use DBObjectSet;
|
||||
use DBSearch;
|
||||
use MetaModel;
|
||||
|
||||
class BackgroundOperationService
|
||||
{
|
||||
private static BackgroundOperationService $oInstance;
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
final public static function GetInstance(): BackgroundOperationService
|
||||
{
|
||||
if (!isset(self::$oInstance)) {
|
||||
self::$oInstance = new BackgroundOperationService();
|
||||
}
|
||||
|
||||
return self::$oInstance;
|
||||
}
|
||||
|
||||
final public static function SetInstance(?BackgroundOperationService $oInstance): void
|
||||
{
|
||||
static::$oInstance = $oInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DataFeatureRemovalBackgroundOperation
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
public function GetNext(): ?DataFeatureRemovalBackgroundOperation
|
||||
{
|
||||
$sOQL = <<<OQL
|
||||
SELECT DataFeatureRemovalBackgroundOperation
|
||||
OQL;
|
||||
|
||||
$oSet = new DBObjectSet(DBSearch::FromOQL($sOQL), ['creation_date' => true]);
|
||||
/** @var DataFeatureRemovalBackgroundOperation $oDBObject */
|
||||
$oDBObject = $oSet->Fetch();
|
||||
|
||||
return $oDBObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $aExtensionsCodes
|
||||
* @param array $aClasses
|
||||
* @return void
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreCannotSaveObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \CoreWarning
|
||||
* @throws \MySQLException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
public function CreateOperation(array $aExtensionsCodes, array $aClasses): void
|
||||
{
|
||||
sort($aExtensionsCodes);
|
||||
sort($aClasses);
|
||||
|
||||
$aValues = [
|
||||
'creation_date' => time(),
|
||||
'features_code' => '|'.implode('|', $aExtensionsCodes).'|',
|
||||
'classes' => implode(',', $aClasses),
|
||||
];
|
||||
$oObj = MetaModel::NewObject('DataFeatureRemovalBackgroundOperation', $aValues);
|
||||
$oObj->DBWrite();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sExtensionCode
|
||||
* @return bool
|
||||
* @throws \CoreException
|
||||
* @throws \MissingQueryArgument
|
||||
* @throws \MySQLException
|
||||
* @throws \MySQLHasGoneAwayException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
public function IsExtensionBeingCleaned(string $sExtensionCode): bool
|
||||
{
|
||||
$sOQL = <<<OQL
|
||||
SELECT DataFeatureRemovalBackgroundOperation WHERE features_code LIKE '%|$sExtensionCode|%'
|
||||
OQL;
|
||||
|
||||
$oSet = new DBObjectSet(DBSearch::FromOQL($sOQL));
|
||||
return $oSet->Count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DataFeatureRemovalBackgroundOperation $oBackgroundOperation
|
||||
* @return array
|
||||
*/
|
||||
public function GetClasses(DataFeatureRemovalBackgroundOperation $oBackgroundOperation): array
|
||||
{
|
||||
return explode(',', $oBackgroundOperation->Get('classes'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\DataFeatureRemoval\Service;
|
||||
|
||||
use CMDBObjectSet;
|
||||
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
|
||||
use Combodo\iTop\Service\Limits\ExecutionLimits;
|
||||
use DBObject;
|
||||
use DBObjectSearch;
|
||||
use MetaModel;
|
||||
|
||||
class DataCleanupService
|
||||
{
|
||||
private array $aVisited = [];
|
||||
private iObjectService $oObjectService;
|
||||
private ExecutionLimits $oExecutionLimits;
|
||||
|
||||
public function __construct(int $iMaxExecutionTime = 30, int $iMaxMemoryPercent = 80)
|
||||
{
|
||||
DataFeatureRemovalLog::Enable();
|
||||
$this->oExecutionLimits = new ExecutionLimits($iMaxExecutionTime, $iMaxMemoryPercent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a summary of the deletion plan computed for the classes.
|
||||
* The result is used for display
|
||||
*
|
||||
* @param array|null $aClasses
|
||||
*
|
||||
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity>
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
|
||||
*/
|
||||
public function GetCleanupSummary(?array $aClasses): array
|
||||
{
|
||||
return $this->ExecuteCleanup($aClasses ?? [], oObjectService: new ObjectServiceSummary());
|
||||
}
|
||||
|
||||
private function GetNextObjectToDelete(array $aClasses): ?DBObject
|
||||
{
|
||||
foreach ($aClasses as $sClass) {
|
||||
$oFilter = new DBObjectSearch($sClass);
|
||||
$oFilter->AllowAllData();
|
||||
$oSet = new \DBObjectSet($oFilter);
|
||||
while ($oObject = $oSet->Fetch()) {
|
||||
if (!$this->IsVisited($oObject)) {
|
||||
return $oObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $aClasses
|
||||
* @param \Combodo\iTop\DataFeatureRemoval\Service\iObjectService|null $oObjectService
|
||||
*
|
||||
* @return array execution summary
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
public function ExecuteCleanup(array $aClasses, ?iObjectService $oObjectService = null): array
|
||||
{
|
||||
$this->oObjectService = $oObjectService ?? new ObjectService();
|
||||
|
||||
$this->aVisited = [];
|
||||
|
||||
while ($oObject = $this->GetNextObjectToDelete($aClasses)) {
|
||||
if ($this->RecursiveDeletion($oObject) === false) {
|
||||
// Timeout, stop here
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $this->oObjectService->GetSummary();
|
||||
|
||||
}
|
||||
|
||||
private function MarkObjectAsVisited(DBObject $oObject): void
|
||||
{
|
||||
$sClass = get_class($oObject);
|
||||
$sId = $oObject->GetKey();
|
||||
$sKey = "$sClass-$sId";
|
||||
$this->aVisited[$sKey] = true;
|
||||
}
|
||||
|
||||
private function IsVisited(DBObject $oObject): bool
|
||||
{
|
||||
$sClass = get_class($oObject);
|
||||
$sId = $oObject->GetKey();
|
||||
$sKey = "$sClass-$sId";
|
||||
|
||||
$bRes = $this->aVisited[$sKey] ?? false;
|
||||
DataFeatureRemovalLog::Debug('Checking if object is visited', null, [$sKey, $bRes]);
|
||||
return $bRes;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param \DBObject $oObjectToClean
|
||||
*
|
||||
* @return bool true if deletion is complete, false in case of timeout or memory limit reached
|
||||
*
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
private function RecursiveDeletion(DBObject $oObjectToClean): bool
|
||||
{
|
||||
$this->MarkObjectAsVisited($oObjectToClean);
|
||||
$sClass = get_class($oObjectToClean);
|
||||
|
||||
$aReferencingMe = MetaModel::EnumReferencingClasses($sClass);
|
||||
foreach ($aReferencingMe as $sRemoteClass => $aExtKeys) {
|
||||
/** @var \AttributeExternalKey $oExtKeyAttDef */
|
||||
foreach ($aExtKeys as $sExtKeyAttCode => $oExtKeyAttDef) {
|
||||
// skip if this external key is behind an external field
|
||||
if (!$oExtKeyAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oSearch = new DBObjectSearch($sRemoteClass);
|
||||
$oSearch->AddCondition($sExtKeyAttCode, $oObjectToClean->GetKey(), '=');
|
||||
$oSearch->AllowAllData();
|
||||
$oSet = new CMDBObjectSet($oSearch);
|
||||
$oSet->OptimizeColumnLoad([$sRemoteClass => [$oExtKeyAttDef->GetCode()]]);
|
||||
/** @var DBObject $oDependentObj */
|
||||
while ($oDependentObj = $oSet->Fetch()) {
|
||||
$iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption();
|
||||
|
||||
if ($oExtKeyAttDef->IsNullAllowed()) {
|
||||
// Optional external key, list to reset
|
||||
if (($iDeletePropagationOption == DEL_MOVEUP) && ($oExtKeyAttDef->IsHierarchicalKey())) {
|
||||
// Move the child up one level i.e. set the same parent as the current object
|
||||
$iParentId = $oObjectToClean->Get($oExtKeyAttDef->GetCode());
|
||||
$this->oObjectService->Update($oDependentObj, $oExtKeyAttDef->GetCode(), $iParentId);
|
||||
} else {
|
||||
$this->oObjectService->Update($oDependentObj, $oExtKeyAttDef->GetCode(), 0);
|
||||
}
|
||||
if ($this->oExecutionLimits->ShouldStopExecution()) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Mandatory external key
|
||||
if ($iDeletePropagationOption == DEL_MANUAL) {
|
||||
// Cannot be deleted automatically, must be handled manually
|
||||
$this->oObjectService->SetIssue(get_class($oDependentObj));
|
||||
continue;
|
||||
}
|
||||
// Propagate deletion only if not visited
|
||||
if ($this->IsVisited($oDependentObj)) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->RecursiveDeletion($oDependentObj)) {
|
||||
// Timeout
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->oObjectService->Delete($sClass, $oObjectToClean->GetKey());
|
||||
|
||||
if ($this->oExecutionLimits->ShouldStopExecution()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use MetaModel;
|
||||
class DataFeatureRemoverExtensionService
|
||||
{
|
||||
private static DataFeatureRemoverExtensionService $oInstance;
|
||||
|
||||
private ?iTopExtensionsMap $oMap = null;
|
||||
private array $aItopExtensions = [];
|
||||
private array $aIncludingExtensionsByModuleName = [];
|
||||
|
||||
@@ -60,15 +60,25 @@ class DataFeatureRemoverExtensionService
|
||||
return $this->aIncludingExtensionsByModuleName[$sModuleName] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \iTopExtensionsMap
|
||||
*/
|
||||
public function GetExtensionMap(): iTopExtensionsMap
|
||||
{
|
||||
if (is_null($this->oMap)) {
|
||||
$this->oMap = iTopExtensionsMap::GetExtensionsMap();
|
||||
$this->oMap->LoadInstalledExtensionsFromDatabase(MetaModel::GetConfig());
|
||||
}
|
||||
return $this->oMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iTopExtension[]
|
||||
*/
|
||||
public function ReadItopExtensions(): array
|
||||
{
|
||||
if (count($this->aItopExtensions) === 0) {
|
||||
$oExtensionsMap = new iTopExtensionsMap();
|
||||
$oExtensionsMap->LoadInstalledExtensionsFromDatabase(MetaModel::GetConfig());
|
||||
$this->aItopExtensions = $oExtensionsMap->GetAllExtensionsToDisplayInSetup(true);
|
||||
$this->aItopExtensions = $this->GetExtensionMap()->GetAllExtensionsToDisplayInSetup(true);
|
||||
|
||||
uasort($this->aItopExtensions, function (iTopExtension $oiTopExtension1, iTopExtension $oiTopExtension2) {
|
||||
return strcmp($oiTopExtension1->sLabel, $oiTopExtension2->sLabel);
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\DataFeatureRemoval\Service;
|
||||
|
||||
use CMDBSource;
|
||||
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity;
|
||||
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
|
||||
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
|
||||
use DBObjectSearch;
|
||||
use DeletionPlan;
|
||||
use MetaModel;
|
||||
|
||||
class DeletionPlanService
|
||||
{
|
||||
private static DeletionPlanService $oInstance;
|
||||
|
||||
public int $iExecutionCount = 0;
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
final public static function GetInstance(): DeletionPlanService
|
||||
{
|
||||
if (!isset(self::$oInstance)) {
|
||||
self::$oInstance = new DeletionPlanService();
|
||||
}
|
||||
|
||||
return self::$oInstance;
|
||||
}
|
||||
|
||||
final public static function SetInstance(?DeletionPlanService $oInstance): void
|
||||
{
|
||||
self::$oInstance = $oInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a summary of the deletion plan computed for the classes.
|
||||
* The result is used for display
|
||||
*
|
||||
* @param array|null $aClasses
|
||||
*
|
||||
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity>
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
public function GetDeletionPlanSummary(?array $aClasses): array
|
||||
{
|
||||
$aSummary = [];
|
||||
if (is_null($aClasses)) {
|
||||
return $aSummary;
|
||||
}
|
||||
|
||||
$oDeletionPlan = $this->GetDeletionPlan($aClasses);
|
||||
|
||||
foreach ($oDeletionPlan->ListUpdates() as $sClass => $aUpdates) {
|
||||
$oDeletionPlanSummaryEntity = new DeletionPlanSummaryEntity($sClass);
|
||||
$oDeletionPlanSummaryEntity->iUpdateCount = count($aUpdates);
|
||||
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
|
||||
}
|
||||
|
||||
foreach ($oDeletionPlan->ListDeletes() as $sClass => $aDeletes) {
|
||||
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
|
||||
$oDeletionPlanSummaryEntity->iDeleteCount = count($aDeletes);
|
||||
|
||||
$aDelete = array_shift($aDeletes);
|
||||
$oDeletionPlanSummaryEntity->iMode = $aDelete['mode'];
|
||||
$oDeletionPlanSummaryEntity->sIssue = $aDelete['issue'] ?? null;
|
||||
|
||||
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
|
||||
}
|
||||
|
||||
return $aSummary;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sClass
|
||||
*
|
||||
* @return \DBObject[]
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function GetAllObjects(string $sClass): array
|
||||
{
|
||||
$oFilter = new DBObjectSearch($sClass);
|
||||
$oFilter->AllowAllData();
|
||||
$oSet = new \DBObjectSet($oFilter);
|
||||
return $oSet->ToArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.3.0
|
||||
* @param array $aClasses
|
||||
* @param int $iUnixTimeLimit : max execution time in seconds since Epoch before stopping deletion. by default: no limit (ie remove all without stop)
|
||||
* @param int $iMaxExecutionCount : max execution count before stopping deletion. by default: no limit (ie remove all without stop)
|
||||
*
|
||||
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity>
|
||||
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
public function ExecuteDeletionPlan(array $aClasses, int $iUnixTimeLimit = 0, int $iMaxExecutionCount = -1): array
|
||||
{
|
||||
$oDeletionPlan = $this->GetDeletionPlan($aClasses);
|
||||
|
||||
if (count($oDeletionPlan->GetIssues()) > 0) {
|
||||
throw new DataFeatureRemovalException("Deletion Plan cannot be executed due to issues");
|
||||
}
|
||||
|
||||
$this->iExecutionCount = 0;
|
||||
|
||||
$aSummary = [];
|
||||
foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate) {
|
||||
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
|
||||
|
||||
foreach ($aToUpdate as $aData) {
|
||||
if ($this->IsTimeLimitExceeded($iUnixTimeLimit, $iMaxExecutionCount)) {
|
||||
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
|
||||
return $aSummary;
|
||||
}
|
||||
|
||||
$this->iExecutionCount++;
|
||||
$oToUpdate = $aData['to_reset'];
|
||||
/** @var \DBObject $oToUpdate */
|
||||
foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef) {
|
||||
$oToUpdate->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]);
|
||||
}
|
||||
$oToUpdate->DBUpdate();
|
||||
$oDeletionPlanSummaryEntity->iUpdateCount++;
|
||||
}
|
||||
|
||||
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
|
||||
}
|
||||
|
||||
foreach ($oDeletionPlan->ListDeletes() as $sClass => $aDeletes) {
|
||||
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
|
||||
|
||||
foreach ($aDeletes as $sId => $aDelete) {
|
||||
if ($this->IsTimeLimitExceeded($iUnixTimeLimit, $iMaxExecutionCount)) {
|
||||
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
|
||||
return $aSummary;
|
||||
}
|
||||
|
||||
$this->iExecutionCount++;
|
||||
|
||||
try {
|
||||
CMDBSource::Query('START TRANSACTION');
|
||||
// Delete any existing change tracking about the current object
|
||||
$oFilter = new DBObjectSearch('CMDBChangeOp');
|
||||
$oFilter->AddCondition('objclass', $sClass, '=');
|
||||
$oFilter->AddCondition('objkey', $sId, '=');
|
||||
MetaModel::PurgeData($oFilter);
|
||||
|
||||
// Delete the entry
|
||||
$aClassesToRemove = array_merge(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL), MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false));
|
||||
foreach ($aClassesToRemove as $sParentClass) {
|
||||
$oFilter = DBObjectSearch::FromOQL_AllData("SELECT $sParentClass WHERE id=:id");
|
||||
$sQuery = $oFilter->MakeDeleteQuery(['id' => $sId]);
|
||||
CMDBSource::DeleteFrom($sQuery);
|
||||
}
|
||||
|
||||
CMDBSource::Query('COMMIT');
|
||||
} catch (\Exception $e) {
|
||||
\IssueLog::Exception(__METHOD__.": Cleanup failed", $e);
|
||||
CMDBSource::Query('ROLLBACK');
|
||||
throw $e;
|
||||
}
|
||||
$oDeletionPlanSummaryEntity->iDeleteCount++;
|
||||
}
|
||||
|
||||
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
|
||||
}
|
||||
|
||||
return $aSummary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a deletion plan for all the objects of the classes
|
||||
*
|
||||
* @param array $aClasses array of class names to clean
|
||||
*
|
||||
* @return \DeletionPlan
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
public function GetDeletionPlan(array $aClasses): DeletionPlan
|
||||
{
|
||||
$oDeletionPlan = new DeletionPlan();
|
||||
foreach ($aClasses as $sClass) {
|
||||
$aObjects = $this->GetAllObjects($sClass);
|
||||
foreach ($aObjects as $oObject) {
|
||||
$oObject->CheckToDelete($oDeletionPlan);
|
||||
}
|
||||
}
|
||||
|
||||
return $oDeletionPlan;
|
||||
}
|
||||
|
||||
public function IsTimeLimitExceeded(int $iUnixTimeLimit, int $iMaxExecutionCount = -1): bool
|
||||
{
|
||||
if (($iMaxExecutionCount !== -1) && ($iMaxExecutionCount <= $this->iExecutionCount)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return DataFeatureRemovalHelper::IsTimeLimitExceeded($iUnixTimeLimit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2026 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\DataFeatureRemoval\Service;
|
||||
|
||||
use CMDBSource;
|
||||
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
|
||||
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
|
||||
use DBObject;
|
||||
use DBObjectSearch;
|
||||
use MetaModel;
|
||||
|
||||
class ObjectService extends ObjectServiceSummary
|
||||
{
|
||||
public function Update(DBObject $oToUpdate, string $sAttCode, $value): void
|
||||
{
|
||||
$oToUpdate->Set($sAttCode, $value);
|
||||
$oToUpdate->DBUpdate();
|
||||
parent::Update($oToUpdate, $sAttCode, $value);
|
||||
}
|
||||
|
||||
public function Delete(string $sClass, string $sId): void
|
||||
{
|
||||
try {
|
||||
CMDBSource::Query('START TRANSACTION');
|
||||
// Delete any existing change tracking about the current object
|
||||
$oFilter = new DBObjectSearch('CMDBChangeOp');
|
||||
$oFilter->AddCondition('objclass', $sClass, '=');
|
||||
$oFilter->AddCondition('objkey', $sId, '=');
|
||||
MetaModel::PurgeData($oFilter);
|
||||
|
||||
// Delete the entry
|
||||
$aClassesToRemove = array_merge(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL), MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false));
|
||||
foreach ($aClassesToRemove as $sParentClass) {
|
||||
/** @var DBObjectSearch $oFilter */
|
||||
$oFilter = DBObjectSearch::FromOQL_AllData("SELECT $sParentClass WHERE id=:id");
|
||||
$sQuery = $oFilter->MakeDeleteQuery(['id' => $sId]);
|
||||
CMDBSource::DeleteFrom($sQuery);
|
||||
}
|
||||
|
||||
CMDBSource::Query('COMMIT');
|
||||
parent::Delete($sClass, $sId);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DataFeatureRemovalLog::Exception(__METHOD__.': Cleanup failed', $e);
|
||||
CMDBSource::Query('ROLLBACK');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function SetIssue(string $sClass): void
|
||||
{
|
||||
throw new DataFeatureRemovalException('Deletion Plan cannot be executed due to issues');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2026 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\DataFeatureRemoval\Service;
|
||||
|
||||
use Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity;
|
||||
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
|
||||
use DBObject;
|
||||
|
||||
/**
|
||||
* Manage operation summary instead of doing the actual update or delete
|
||||
*
|
||||
* The summary is an array [class => DeletionPlanSummaryEntity]
|
||||
*/
|
||||
class ObjectServiceSummary implements iObjectService
|
||||
{
|
||||
private array $aSummary = [];
|
||||
|
||||
public function Update(DBObject $oToUpdate, string $sAttCode, $value): void
|
||||
{
|
||||
$sClass = get_class($oToUpdate);
|
||||
DataFeatureRemovalLog::Debug('Object to update', null, ['class' => $sClass, 'id' => $oToUpdate->GetKey(), 'code' => $sAttCode, 'value' => "$value"]);
|
||||
if (! array_key_exists($sClass, $this->aSummary)) {
|
||||
$this->aSummary[$sClass] = new DataCleanupSummaryEntity($sClass);
|
||||
}
|
||||
$oDeletionPlanSummaryEntity = $this->aSummary[$sClass];
|
||||
$oDeletionPlanSummaryEntity->iUpdateCount++;
|
||||
$oDeletionPlanSummaryEntity->iTotalUpdateCount++;
|
||||
}
|
||||
|
||||
public function Delete(string $sClass, string $sId): void
|
||||
{
|
||||
DataFeatureRemovalLog::Debug('Object to delete', null, ['class' => $sClass, 'id' => $sId]);
|
||||
if (!array_key_exists($sClass, $this->aSummary)) {
|
||||
$this->aSummary[$sClass] = new DataCleanupSummaryEntity($sClass);
|
||||
}
|
||||
$oDeletionPlanSummaryEntity = $this->aSummary[$sClass];
|
||||
$oDeletionPlanSummaryEntity->iDeleteCount++;
|
||||
$oDeletionPlanSummaryEntity->iTotalDeleteCount++;
|
||||
}
|
||||
|
||||
public function SetIssue(string $sClass): void
|
||||
{
|
||||
DataFeatureRemovalLog::Debug('Issue on object', null, ['class' => $sClass]);
|
||||
if (!array_key_exists($sClass, $this->aSummary)) {
|
||||
$this->aSummary[$sClass] = new DataCleanupSummaryEntity($sClass);
|
||||
}
|
||||
$oDeletionPlanSummaryEntity = $this->aSummary[$sClass];
|
||||
$oDeletionPlanSummaryEntity->iIssueCount++;
|
||||
}
|
||||
|
||||
public function GetSummary(): array
|
||||
{
|
||||
return $this->aSummary;
|
||||
}
|
||||
|
||||
public function SetSummary(array $aSummary): void
|
||||
{
|
||||
foreach ($aSummary as $sClass => $oPreviousSummaryEntity) {
|
||||
$oSummaryEntity = new DataCleanupSummaryEntity($sClass);
|
||||
$oSummaryEntity->iTotalUpdateCount = $oPreviousSummaryEntity->iTotalUpdateCount;
|
||||
$oSummaryEntity->iTotalDeleteCount = $oPreviousSummaryEntity->iTotalDeleteCount;
|
||||
|
||||
$this->aSummary[$sClass] = $oSummaryEntity;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2026 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\DataFeatureRemoval\Service;
|
||||
|
||||
use DBObject;
|
||||
|
||||
interface iObjectService
|
||||
{
|
||||
public function Update(DBObject $oToUpdate, string $sAttCode, $value): void;
|
||||
|
||||
public function Delete(string $sClass, string $sId): void;
|
||||
|
||||
public function SetIssue(string $sClass): void;
|
||||
|
||||
public function GetSummary(): array;
|
||||
}
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,55 @@
|
||||
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
|
||||
{# @license http://opensource.org/licenses/AGPL-3.0 #}
|
||||
|
||||
{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:Analysis:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:Analysis:Subtitle'|dict_s} %}
|
||||
{% if null != DataFeatureRemovalErrorMessage %}
|
||||
<div id="feature_removal_error_msg_div" style="display:block">
|
||||
{% UIAlert ForFailure { sTitle:'DataFeatureRemoval:Failure:Title'|dict_s, sId: 'feature_removal_error_msg', sContent:DataFeatureRemovalErrorMessage } %}
|
||||
{% EndUIAlert %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% UIAlert ForInformation { sTitle: '', sId: 'ajax_compile_in_progress_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}
|
||||
{{ 'DataFeatureRemoval:Compile:InProgress'|dict_s }}
|
||||
{% UISpinner Standard { } %}
|
||||
{% EndUIAlert %}
|
||||
{% UIAlert ForSuccess { sTitle:'success', sId: 'ajax_compile_success_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
|
||||
{% UIAlert ForFailure { sTitle:'error', sId: 'ajax_compile_error_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
|
||||
|
||||
{% UIAlert ForInformation { sTitle: '', sId: 'ajax_run_audit_in_progress_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}
|
||||
{{ 'DataFeatureRemoval:RunAudit:InProgress'|dict_s }}
|
||||
{% UISpinner Standard { } %}
|
||||
{% EndUIAlert %}
|
||||
{% UIAlert ForSuccess { sTitle:'DataFeatureRemoval:RunAudit:Success'|dict_s, sId: 'ajax_run_audit_success_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
|
||||
{% UIAlert ForFailure { sTitle:'error', sId: 'ajax_run_audit_error_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
|
||||
|
||||
{% if aAvailableExtensionsCount == 0 %}
|
||||
{% UITitle Neutral { sTitle:'DataFeatureRemoval:NoResult:Title'|dict_s, iLevel:2 } %}{% EndUITitle %}
|
||||
{% else %}
|
||||
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Result:Title'|dict_s, sSubTitle: '' } %}
|
||||
{% UIMultiColumn Standard {} %}
|
||||
{% for iColumnIndex in 0..iColumnCount-1 %}
|
||||
{% UIColumn Standard {} %}
|
||||
{% for aExtension in aAvailableExtensions[iColumnIndex] %}
|
||||
{% if aExtension['installed'] %}
|
||||
{% UIExtensionDetails Installed { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
|
||||
{% else %}
|
||||
{% UIExtensionDetails NotInstalled { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% EndUIColumn %}
|
||||
{% endfor %}
|
||||
{% EndUIMultiColumn %}
|
||||
{% EndUIPanel %}
|
||||
{% endif %}
|
||||
|
||||
<div id="ajax_run_audit" class="ibo-block"></div>
|
||||
|
||||
{% UIForm Standard {} %}
|
||||
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
|
||||
{% UIInput ForHidden { sName:'operation', sValue:'Main'} %}
|
||||
{% UIToolbar ForButton {} %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:BackToMain'|dict_s, sName:'btn_back', sId:'btn_back', bIsSubmit:true} %}
|
||||
{% EndUIToolbar %}
|
||||
{% EndUIForm %}
|
||||
{% EndUIPanel %}
|
||||
@@ -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();
|
||||
@@ -1,35 +0,0 @@
|
||||
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
|
||||
{# @license http://opensource.org/licenses/AGPL-3.0 #}
|
||||
|
||||
{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:DeletionPlan:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:DeletionPlan:SubTitle'|dict_format(iQueryCount) } %}
|
||||
{% UIDataTable ForForm { sRef:'aDeletionPlanSummary', aColumns:aDeletionPlanSummary.Columns, aData:aDeletionPlanSummary.Data} %}{% EndUIDataTable %}
|
||||
{% EndUIPanel %}
|
||||
|
||||
{% if bDeletionPossible %}
|
||||
{% UIForm Standard {} %}
|
||||
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
|
||||
{% UIInput ForHidden { sName:'operation', sValue:'DoDeletion'} %}
|
||||
{% for sKey, sClass in aClasses %}
|
||||
{% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %}
|
||||
{% endfor %}
|
||||
|
||||
{% for sKey, sCode in aExtensionsCode %}
|
||||
{% UIInput ForHidden { sName:"aExtensionsCode[" ~ sKey ~ "]", sValue:sCode } %}
|
||||
{% endfor %}
|
||||
|
||||
{% UIToolbar ForButton {} %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoAsyncDeletion'|dict_s, sName:'btn_deletion', sId:'btn_async_deletion', sValue: 'async_deletion', bIsSubmit:true} %}
|
||||
{% EndUIToolbar %}
|
||||
{% EndUIForm %}
|
||||
{% else %}
|
||||
{{ 'DataFeatureRemoval:DeletionPlan:ToManyOperations'|dict_s }}
|
||||
{% endif %}
|
||||
|
||||
{% UIForm Standard {} %}
|
||||
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
|
||||
{% UIInput ForHidden { sName:'operation', sValue:'Main'} %}
|
||||
{% UIToolbar ForButton {} %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:BackToMain'|dict_s, sName:'btn_back', sId:'btn_back', bIsSubmit:true} %}
|
||||
{% EndUIToolbar %}
|
||||
{% EndUIForm %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user