mirror of
https://github.com/Combodo/iTop.git
synced 2026-07-27 09:06:41 +02:00
Compare commits
265 Commits
feature/91
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,27 @@ 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 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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3986,6 +3997,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];
|
||||
}
|
||||
@@ -4495,7 +4507,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 +5076,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();
|
||||
|
||||
@@ -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, '=');
|
||||
@@ -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);
|
||||
|
||||
@@ -1917,7 +1917,9 @@ SQL;
|
||||
$aResponseHeaders[$sName] = $sValue;
|
||||
}
|
||||
}
|
||||
curl_close($ch);
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
@@ -3190,4 +3192,30 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -4660,12 +4659,15 @@ abstract class MetaModel
|
||||
* @throws \CoreException
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
public static function DBExists($bMustBeComplete = true)
|
||||
public static function DBExists($bMustBeComplete = true, ?string $sDBName = null)
|
||||
{
|
||||
if (!CMDBSource::IsDB(self::$m_sDBName)) {
|
||||
if (is_null($sDBName)) {
|
||||
$sDBName = self::$m_sDBName;
|
||||
}
|
||||
if (!CMDBSource::IsDB($sDBName)) {
|
||||
return false;
|
||||
}
|
||||
CMDBSource::SelectDB(self::$m_sDBName);
|
||||
CMDBSource::SelectDB($sDBName);
|
||||
|
||||
$aFound = [];
|
||||
$aMissing = [];
|
||||
@@ -5759,36 +5761,37 @@ abstract class MetaModel
|
||||
|
||||
self::$m_sEnvironment = $sEnvironment;
|
||||
|
||||
try {
|
||||
if (!defined('MODULESROOT')) {
|
||||
define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/');
|
||||
if (!defined('MODULESROOT')) {
|
||||
define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/');
|
||||
|
||||
self::$m_bTraceSourceFiles = $bTraceSourceFiles;
|
||||
self::$m_bTraceSourceFiles = $bTraceSourceFiles;
|
||||
|
||||
// $config can be either a filename, or a Configuration object (volatile!)
|
||||
if ($config instanceof Config) {
|
||||
self::LoadConfig($config, $bAllowCache);
|
||||
} else {
|
||||
self::LoadConfig(new Config($config), $bAllowCache);
|
||||
}
|
||||
|
||||
if ($bModelOnly) {
|
||||
return;
|
||||
}
|
||||
// $config can be either a filename, or a Configuration object (volatile!)
|
||||
if ($config instanceof Config) {
|
||||
self::LoadConfig($config, $bAllowCache);
|
||||
} else {
|
||||
self::LoadConfig(new Config($config), $bAllowCache);
|
||||
}
|
||||
|
||||
CMDBSource::SelectDB(self::$m_sDBName);
|
||||
|
||||
foreach (MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) {
|
||||
$oPHPClass::OnMetaModelStarted();
|
||||
if ($bModelOnly) {
|
||||
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
|
||||
EventService::InitService();
|
||||
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
|
||||
return;
|
||||
}
|
||||
|
||||
ExpressionCache::Warmup();
|
||||
} finally {
|
||||
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
|
||||
EventService::InitService();
|
||||
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
|
||||
}
|
||||
|
||||
CMDBSource::SelectDB(self::$m_sDBName);
|
||||
|
||||
foreach (MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) {
|
||||
$oPHPClass::OnMetaModelStarted();
|
||||
}
|
||||
|
||||
ExpressionCache::Warmup();
|
||||
|
||||
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
|
||||
EventService::InitService();
|
||||
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
* 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('ZH CN', 'Chinese', '简体中文', [
|
||||
'theme:fullmoon-high-contrast' => 'Fullmoon (High contrast)~~',
|
||||
'theme:fullmoon-high-contrast' => '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('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",
|
||||
|
||||
@@ -10,38 +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: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: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',
|
||||
@@ -51,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,38 +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: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: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',
|
||||
@@ -51,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',
|
||||
|
||||
@@ -8,37 +8,55 @@
|
||||
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\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['iColumnCount'] = $this->iColumnCount;
|
||||
$aParams['aAvailableExtensions'] = $this->SplitArrayIntoColumns($this->GetAvailableExtensions(), $this->iColumnCount);
|
||||
$aParams['aAnalysisDataTable'] = $this->aAnalysisDataTable;
|
||||
$aParams['aClasses'] = array_keys($this->aCountClassesToCleanup);
|
||||
$aParams['DataFeatureRemovalErrorMessage'] = $sErrorMessage;
|
||||
@@ -68,126 +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['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);
|
||||
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary') ?? serialize([]));
|
||||
Session::Unset('aDeletionExecutionSummary');
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
$aClasses = $oParameters->GetParameter('classes', []);
|
||||
|
||||
$aDeletionExecutionSummary = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses);
|
||||
$aColumns = ['Class', 'DeletedCount' , 'UpdatedCount'];
|
||||
$aRows = [];
|
||||
foreach ($aDeletionExecutionSummary as $oDeletionExecutionSummaryEntity) {
|
||||
$aRows[] = [
|
||||
$oDeletionExecutionSummaryEntity->sClass,
|
||||
$oDeletionExecutionSummaryEntity->iDeleteCount,
|
||||
$oDeletionExecutionSummaryEntity->iUpdateCount,
|
||||
];
|
||||
$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;
|
||||
}
|
||||
|
||||
$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(),
|
||||
],
|
||||
|
||||
$sChecked = '';
|
||||
$sDisabledHtml = '';
|
||||
if ($oExtension->bRemovedFromDisk) {
|
||||
$sDisabledHtml = 'disabled=""';
|
||||
$sChecked = 'checked';
|
||||
} elseif (in_array($sCode, $this->aSelectedExtensionsForCheck)) {
|
||||
$sChecked = 'checked';
|
||||
}
|
||||
|
||||
$sLabel = $oExtension->sLabel;
|
||||
$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
|
||||
@@ -224,34 +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 void
|
||||
* Read extensions selected from posted parameters
|
||||
*/
|
||||
public function ReadRemovedExtensions(): void
|
||||
public function ReadExtensionsDiff(): void
|
||||
{
|
||||
if (count($this->aSelectedExtensionsForCheck) > 0) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Add source removed to check
|
||||
foreach (DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions() as $sCode => $oExtension) {
|
||||
if ($oExtension->bRemovedFromDisk) {
|
||||
$this->aSelectedExtensionsForCheck[] = $sCode;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $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);
|
||||
}
|
||||
}
|
||||
@@ -20,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)
|
||||
|
||||
@@ -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,182 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\DataFeatureRemoval\Service;
|
||||
|
||||
use CMDBSource;
|
||||
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity;
|
||||
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
|
||||
use DBObjectSearch;
|
||||
use DeletionPlan;
|
||||
use MetaModel;
|
||||
|
||||
class DeletionPlanService
|
||||
{
|
||||
private static DeletionPlanService $oInstance;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $aClasses
|
||||
*
|
||||
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity>
|
||||
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
public function ExecuteDeletionPlan(array $aClasses): array
|
||||
{
|
||||
$oDeletionPlan = $this->GetDeletionPlan($aClasses);
|
||||
|
||||
if (count($oDeletionPlan->GetIssues()) > 0) {
|
||||
throw new DataFeatureRemovalException("Deletion Plan cannot be executed due to issues");
|
||||
}
|
||||
|
||||
$aSummary = [];
|
||||
foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate) {
|
||||
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
|
||||
|
||||
foreach ($aToUpdate as $aData) {
|
||||
$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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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,29 +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 %}
|
||||
{% UIToolbar ForButton {} %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_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 %}
|
||||
@@ -1,19 +0,0 @@
|
||||
{# @copyright Copyright (C) 2010-2024 Combodo SAS #}
|
||||
{# @license http://opensource.org/licenses/AGPL-3.0 #}
|
||||
|
||||
{% if bHasData %}
|
||||
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Analysis:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:Analysis:SubTitle'|dict_format(iCount) } %}
|
||||
{% UIDataTable ForForm { sRef:'aAnalysisDataTable', aColumns:aAnalysisDataTable.Columns, aData:aAnalysisDataTable.Data} %}{% EndUIDataTable %}
|
||||
{% EndUIPanel %}
|
||||
|
||||
{% UIForm Standard {} %}
|
||||
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
|
||||
{% UIInput ForHidden { sName:'operation', sValue:'DeletionPlan'} %}
|
||||
{% for sKey, sClass in aClasses %}
|
||||
{% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %}
|
||||
{% endfor %}
|
||||
{% UIToolbar ForButton {} %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:PlanDeletion'|dict_s, sName:'btn_plandeletion', sId:'btn_plandeletion', bIsSubmit:true} %}
|
||||
{% EndUIToolbar %}
|
||||
{% EndUIForm %}
|
||||
{% endif %}
|
||||
@@ -3,12 +3,24 @@
|
||||
|
||||
|
||||
{% UIForm Standard {} %}
|
||||
{% UIInput ForHidden {sName:'operation', sValue:'Analyze'} %}
|
||||
{% UIInput ForHidden {sName:'operation', sValue:'AnalysisResult'} %}
|
||||
{% UIInput ForHidden {sName:'transaction_id', sValue:sTransactionId} %}
|
||||
|
||||
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:Features:Title'|dict_s} %}
|
||||
{% UIDataTable ForForm { sRef:'aExtensions', aColumns:aExtensions.Columns, aData:aExtensions.Data} %}{% EndUIDataTable %}
|
||||
{% EndUIFieldSet %}
|
||||
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Features: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 %}
|
||||
|
||||
{% UIToolbar ForButton {} %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Analyze'|dict_s, sName:'btn_apply', sId:'btn_apply', bIsSubmit:true} %}
|
||||
|
||||
@@ -11,25 +11,7 @@
|
||||
{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:Main:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:Main:SubTitle'|dict_s } %}
|
||||
|
||||
{% UIAlert ForInformation { sTitle:'DataFeatureRemoval:Helper:Title'|dict_s } %}
|
||||
{{ 'DataFeatureRemoval:Helper:Desc1'|dict_s }}<BR>
|
||||
{{ 'DataFeatureRemoval:Helper:Desc2'|dict_s }}
|
||||
{% EndUIAlert %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% include 'Features.html.twig' %}
|
||||
{% include 'ExtensionRemovalData.html.twig' %}
|
||||
|
||||
{% if not bHasData %}
|
||||
{% UIToolbar ForButton {} %}
|
||||
<a href="{{ sSetupUrl }}">
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Setup'|dict_s, sName:'btn_setup', sId:'btn_setup', bIsSubmit:false} %}
|
||||
</a>
|
||||
{% EndUIToolbar %}
|
||||
{% endif %}
|
||||
{% EndUIPanel %}
|
||||
|
||||
396
datamodels/2.x/combodo-data-feature-removal/vendor/composer/InstalledVersions.php
vendored
Normal file
396
datamodels/2.x/combodo-data-feature-removal/vendor/composer/InstalledVersions.php
vendored
Normal file
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Composer\Semver\VersionParser;
|
||||
|
||||
/**
|
||||
* This class is copied in every Composer installed project and available to all
|
||||
*
|
||||
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||
*
|
||||
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class InstalledVersions
|
||||
{
|
||||
/**
|
||||
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
|
||||
* @internal
|
||||
*/
|
||||
private static $selfDir = null;
|
||||
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private static $installedIsLocalDir;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private static $canGetVendors;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static $installedByVendor = array();
|
||||
|
||||
/**
|
||||
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||
*
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackages()
|
||||
{
|
||||
$packages = array();
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
$packages[] = array_keys($installed['versions']);
|
||||
}
|
||||
|
||||
if (1 === \count($packages)) {
|
||||
return $packages[0];
|
||||
}
|
||||
|
||||
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all package names with a specific type e.g. 'library'
|
||||
*
|
||||
* @param string $type
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackagesByType($type)
|
||||
{
|
||||
$packagesByType = array();
|
||||
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
foreach ($installed['versions'] as $name => $package) {
|
||||
if (isset($package['type']) && $package['type'] === $type) {
|
||||
$packagesByType[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $packagesByType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package is installed
|
||||
*
|
||||
* This also returns true if the package name is provided or replaced by another package
|
||||
*
|
||||
* @param string $packageName
|
||||
* @param bool $includeDevRequirements
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (isset($installed['versions'][$packageName])) {
|
||||
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package satisfies a version constraint
|
||||
*
|
||||
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||
*
|
||||
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||
*
|
||||
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||
* @param string $packageName
|
||||
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||
* @return bool
|
||||
*/
|
||||
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||
{
|
||||
$constraint = $parser->parseConstraints((string) $constraint);
|
||||
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||
|
||||
return $provided->matches($constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||
*
|
||||
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||
* whether a given version of a package is installed, and not just whether it exists
|
||||
*
|
||||
* @param string $packageName
|
||||
* @return string Version constraint usable with composer/semver
|
||||
*/
|
||||
public static function getVersionRanges($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ranges = array();
|
||||
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||
}
|
||||
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||
}
|
||||
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||
}
|
||||
|
||||
return implode(' || ', $ranges);
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getPrettyVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||
*/
|
||||
public static function getReference($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['reference'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||
*/
|
||||
public static function getInstallPath($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||
*/
|
||||
public static function getRootPackage()
|
||||
{
|
||||
$installed = self::getInstalled();
|
||||
|
||||
return $installed[0]['root'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw installed.php data for custom implementations
|
||||
*
|
||||
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||
* @return array[]
|
||||
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
|
||||
*/
|
||||
public static function getRawData()
|
||||
{
|
||||
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
self::$installed = include __DIR__ . '/installed.php';
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
return self::$installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||
*
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
public static function getAllRawData()
|
||||
{
|
||||
return self::getInstalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets you reload the static array from another file
|
||||
*
|
||||
* This is only useful for complex integrations in which a project needs to use
|
||||
* this class but then also needs to execute another project's autoloader in process,
|
||||
* and wants to ensure both projects have access to their version of installed.php.
|
||||
*
|
||||
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||
* the data it needs from this class, then call reload() with
|
||||
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||
* the project in which it runs can then also use this class safely, without
|
||||
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||
*
|
||||
* @param array[] $data A vendor/composer/installed.php data set
|
||||
* @return void
|
||||
*
|
||||
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
||||
*/
|
||||
public static function reload($data)
|
||||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
|
||||
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
|
||||
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
|
||||
// so we have to assume it does not, and that may result in duplicate data being returned when listing
|
||||
// all installed packages for example
|
||||
self::$installedIsLocalDir = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private static function getSelfDir()
|
||||
{
|
||||
if (self::$selfDir === null) {
|
||||
self::$selfDir = strtr(__DIR__, '\\', '/');
|
||||
}
|
||||
|
||||
return self::$selfDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static function getInstalled()
|
||||
{
|
||||
if (null === self::$canGetVendors) {
|
||||
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||
}
|
||||
|
||||
$installed = array();
|
||||
$copiedLocalDir = false;
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
$selfDir = self::getSelfDir();
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
$vendorDir = strtr($vendorDir, '\\', '/');
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
self::$installedByVendor[$vendorDir] = $required;
|
||||
$installed[] = $required;
|
||||
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
|
||||
self::$installed = $required;
|
||||
self::$installedIsLocalDir = true;
|
||||
}
|
||||
}
|
||||
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
|
||||
$copiedLocalDir = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require __DIR__ . '/installed.php';
|
||||
self::$installed = $required;
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
if (self::$installed !== array() && !$copiedLocalDir) {
|
||||
$installed[] = self::$installed;
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,18 @@ $baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => $baseDir . '/src/Controller/DataFeatureRemovalController.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanSummaryEntity' => $baseDir . '/src/Entity/DeletionPlanSummaryEntity.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DataCleanupSummaryEntity' => $baseDir . '/src/Entity/DataCleanupSummaryEntity.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanEntity' => $baseDir . '/src/Entity/DeletionPlanEntity.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanItem' => $baseDir . '/src/Entity/DeletionPlanItem.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalConfig' => $baseDir . '/src/Helper/DataFeatureRemovalConfig.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => $baseDir . '/src/Helper/DataFeatureRemovalException.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => $baseDir . '/src/Helper/DataFeatureRemovalHelper.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalLog' => $baseDir . '/src/Helper/DataFeatureRemovalLog.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataCleanupService' => $baseDir . '/src/Service/DataCleanupService.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => $baseDir . '/src/Service/DataFeatureRemoverExtensionService.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DeletionPlanService' => $baseDir . '/src/Service/DeletionPlanService.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectService' => $baseDir . '/src/Service/ObjectService.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectServiceSummary' => $baseDir . '/src/Service/ObjectServiceSummary.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\StaticDeletionPlan' => $baseDir . '/src/Service/StaticDeletionPlan.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\iObjectService' => $baseDir . '/src/Service/iObjectService.php',
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
);
|
||||
|
||||
@@ -7,5 +7,4 @@ $baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\' => array($baseDir . '/src'),
|
||||
'' => array($baseDir . '/src/NoNamespace'),
|
||||
);
|
||||
|
||||
@@ -20,19 +20,21 @@ class ComposerStaticInit4f96a7199e2c0d90e547333758b26464
|
||||
),
|
||||
);
|
||||
|
||||
public static $fallbackDirsPsr4 = array (
|
||||
0 => __DIR__ . '/../..' . '/src/NoNamespace',
|
||||
);
|
||||
|
||||
public static $classMap = array (
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => __DIR__ . '/../..' . '/src/Controller/DataFeatureRemovalController.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanSummaryEntity' => __DIR__ . '/../..' . '/src/Entity/DeletionPlanSummaryEntity.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DataCleanupSummaryEntity' => __DIR__ . '/../..' . '/src/Entity/DataCleanupSummaryEntity.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanEntity' => __DIR__ . '/../..' . '/src/Entity/DeletionPlanEntity.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanItem' => __DIR__ . '/../..' . '/src/Entity/DeletionPlanItem.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalConfig' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalConfig.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalException.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalHelper.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalLog' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalLog.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataCleanupService' => __DIR__ . '/../..' . '/src/Service/DataCleanupService.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => __DIR__ . '/../..' . '/src/Service/DataFeatureRemoverExtensionService.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DeletionPlanService' => __DIR__ . '/../..' . '/src/Service/DeletionPlanService.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectService' => __DIR__ . '/../..' . '/src/Service/ObjectService.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectServiceSummary' => __DIR__ . '/../..' . '/src/Service/ObjectServiceSummary.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\StaticDeletionPlan' => __DIR__ . '/../..' . '/src/Service/StaticDeletionPlan.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\iObjectService' => __DIR__ . '/../..' . '/src/Service/iObjectService.php',
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
);
|
||||
|
||||
@@ -41,7 +43,6 @@ class ComposerStaticInit4f96a7199e2c0d90e547333758b26464
|
||||
return \Closure::bind(function () use ($loader) {
|
||||
$loader->prefixLengthsPsr4 = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$prefixLengthsPsr4;
|
||||
$loader->prefixDirsPsr4 = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$prefixDirsPsr4;
|
||||
$loader->fallbackDirsPsr4 = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$fallbackDirsPsr4;
|
||||
$loader->classMap = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$classMap;
|
||||
|
||||
}, null, ClassLoader::class);
|
||||
|
||||
5
datamodels/2.x/combodo-data-feature-removal/vendor/composer/installed.json
vendored
Normal file
5
datamodels/2.x/combodo-data-feature-removal/vendor/composer/installed.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"packages": [],
|
||||
"dev": true,
|
||||
"dev-package-names": []
|
||||
}
|
||||
23
datamodels/2.x/combodo-data-feature-removal/vendor/composer/installed.php
vendored
Normal file
23
datamodels/2.x/combodo-data-feature-removal/vendor/composer/installed.php
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php return array(
|
||||
'root' => array(
|
||||
'name' => 'combodo/combodo-data-feature-removal',
|
||||
'pretty_version' => 'dev-develop',
|
||||
'version' => 'dev-develop',
|
||||
'reference' => '19bbf6759bb4f6f5814d9ec1b0b5514208efc0b2',
|
||||
'type' => 'itop-extension',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev' => true,
|
||||
),
|
||||
'versions' => array(
|
||||
'combodo/combodo-data-feature-removal' => array(
|
||||
'pretty_version' => 'dev-develop',
|
||||
'version' => 'dev-develop',
|
||||
'reference' => '19bbf6759bb4f6f5814d9ec1b0b5514208efc0b2',
|
||||
'type' => 'itop-extension',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -4,7 +4,7 @@
|
||||
* Localized data
|
||||
*
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license https://opensource.org/licenses/AGPL-3.0
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
@@ -21,14 +21,15 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
// Database inconsistencies
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
// Dictionary entries go here
|
||||
'Menu:DBToolsMenu' => '数据库工具',
|
||||
'Menu:DBToolsMenu' => '数据库完整性',
|
||||
'DBTools:Class' => '类型',
|
||||
'DBTools:Title' => '数据库维护工具',
|
||||
'DBTools:Title' => '数据库完整性检查',
|
||||
'DBTools:ErrorsFound' => '发现错误',
|
||||
'DBTools:Indication' => '重要: 修复数据库错误后, 可能会出现新的不一致, 您必须重新运行一次分析.',
|
||||
'DBTools:Indication' => '重要提示: 修复数据库错误后, 可能会出现新的不一致, 您必须重新运行一次分析.',
|
||||
'DBTools:Disclaimer' => '免责申明: 在应用修复之前, 应先备份数据库',
|
||||
'DBTools:Error' => '错误',
|
||||
'DBTools:Count' => '个数',
|
||||
@@ -42,17 +43,20 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'DBTools:IntegrityCheck' => '完整性检查',
|
||||
'DBTools:FetchCheck' => '提取检查 (耗时长)',
|
||||
'DBTools:SelectAnalysisType' => '请选择分析类型',
|
||||
|
||||
'DBTools:Analyze' => '分析',
|
||||
'DBTools:Details' => '显示详情',
|
||||
'DBTools:ShowAll' => '显示所有错误',
|
||||
|
||||
'DBTools:Inconsistencies' => '数据库不一致',
|
||||
'DBTools:DetailedErrorTitle' => '%2$s个错误在类型%1$s: %3$s',
|
||||
'DBTools:DetailedErrorLimit' => '列表限制为%1$s错误',
|
||||
|
||||
'DBAnalyzer-Integrity-OrphanRecord' => '孤立记录位于 `%1$s`, 其应该有副本位于表 `%2$s`',
|
||||
'DBAnalyzer-Integrity-InvalidExtKey' => '无效的外键%1$s (列: `%2$s.%3$s`)',
|
||||
'DBAnalyzer-Integrity-MissingExtKey' => '外键丢失%1$s (列: `%2$s.%3$s`)',
|
||||
'DBAnalyzer-Integrity-InvalidValue' => '无效的值%1$s (列: `%2$s.%3$s`)',
|
||||
'DBAnalyzer-Integrity-UsersWithoutProfile' => '一些用户账号没有角色',
|
||||
'DBAnalyzer-Integrity-UsersWithoutProfile' => '有的账号没有角色',
|
||||
'DBAnalyzer-Integrity-HKInvalid' => '损坏的层级链 `%1$s`',
|
||||
'DBAnalyzer-Fetch-Count-Error' => '读取计数出错于 `%1$s`, %2$d个记录已读取 / %3$d已',
|
||||
'DBAnalyzer-Integrity-FinalClass' => '字段 `%2$s`.`%1$s` 必须是相同的值, 而不是 `%3$s`.`%1$s`',
|
||||
@@ -70,19 +74,24 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'DBTools:LostAttachments' => '附件缺失',
|
||||
'DBTools:LostAttachments:Disclaimer' => '可以在此搜索数据库中丢失或错放的附件. 这不是数据恢复工具, 其无法恢复已删除的数据.',
|
||||
|
||||
'DBTools:LostAttachments:Button:Analyze' => '分析',
|
||||
'DBTools:LostAttachments:Button:Restore' => '还原',
|
||||
'DBTools:LostAttachments:Button:Restore:Confirm' => '此操作无法回退, 请确认是否继续还原.',
|
||||
'DBTools:LostAttachments:Button:Busy' => '请稍候...',
|
||||
|
||||
'DBTools:LostAttachments:Step:Analyze' => '首先, 通过分析数据库来搜索丢失或误挪动的附件.',
|
||||
|
||||
'DBTools:LostAttachments:Step:AnalyzeResults' => '分析结果:',
|
||||
'DBTools:LostAttachments:Step:AnalyzeResults:None' => '非常好! 所有附件都是正常的.',
|
||||
'DBTools:LostAttachments:Step:AnalyzeResults:Some' => '某些附件 (%1$d) 看起来放错了位置. 请检查下面的列表并选择要挪动的文件.',
|
||||
'DBTools:LostAttachments:Step:AnalyzeResults:Item:Filename' => '文件名',
|
||||
'DBTools:LostAttachments:Step:AnalyzeResults:Item:CurrentLocation' => '当前位置',
|
||||
'DBTools:LostAttachments:Step:AnalyzeResults:Item:TargetLocation' => '移动到...',
|
||||
|
||||
'DBTools:LostAttachments:Step:RestoreResults' => '还原结果:',
|
||||
'DBTools:LostAttachments:Step:RestoreResults:Results' => '%1$d/%2$d的附件被还原.',
|
||||
|
||||
'DBTools:LostAttachments:StoredAsInlineImage' => '存储为内嵌图像',
|
||||
'DBTools:LostAttachments:History' => '附件 "%1$s" 已使用数据库工具还原',
|
||||
]);
|
||||
|
||||
@@ -27,12 +27,6 @@
|
||||
<module>combodo-backoffice-fullmoon-protanopia-deuteranopia-theme</module>
|
||||
<module>combodo-backoffice-fullmoon-tritanopia-theme</module>
|
||||
<module>itop-themes-compat</module>
|
||||
<module>combodo-my-account</module>
|
||||
<module>combodo-my-account-user-info</module>
|
||||
<module>combodo-oauth2-client</module>
|
||||
<module>itop-attribute-class-set</module>
|
||||
<module>itop-attribute-encrypted-password</module>
|
||||
<module>itop-ui-copypaste</module>
|
||||
</modules>
|
||||
<mandatory>true</mandatory>
|
||||
</choice>
|
||||
@@ -54,6 +48,15 @@
|
||||
</modules>
|
||||
<default>true</default>
|
||||
</choice>
|
||||
<choice>
|
||||
<extension_code>itop-flow-map</extension_code>
|
||||
<title>Application Data Flows</title>
|
||||
<description>Modelize flows between your applications, with impacts analysis</description>
|
||||
<modules type="array">
|
||||
<module>itop-flow-map</module>
|
||||
</modules>
|
||||
<default>true</default>
|
||||
</choice>
|
||||
<choice>
|
||||
<extension_code>itop-config-mgmt-storage</extension_code>
|
||||
<title>Storage Devices</title>
|
||||
@@ -80,7 +83,7 @@
|
||||
<modules type="array">
|
||||
<module>itop-container-mgmt</module>
|
||||
</modules>
|
||||
<default>false</default>
|
||||
<default>true</default>
|
||||
</choice>
|
||||
</options>
|
||||
</sub_options>
|
||||
|
||||
@@ -85,11 +85,13 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', [
|
||||
Dict::Add('CS CZ', 'Czech', 'Čeština', [
|
||||
'Class:TriggerOnAttachmentDownload' => 'Trigger (on object\'s attachment download)~~',
|
||||
'Class:TriggerOnAttachmentDownload+' => 'Trigger on object\'s attachment download of [a child class of] the given class~~',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment create)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment create~~',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment creation)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment creation~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => 'Add file in email~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'If checked, the file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment delete)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment delete~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment deletion)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment deletion~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Add deleted file in email~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'If checked, the deleted file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Trigger on object is not allowed on class Attachment. Please use specific trigger~~',
|
||||
]);
|
||||
|
||||
@@ -84,11 +84,13 @@ Dict::Add('DA DA', 'Danish', 'Dansk', [
|
||||
Dict::Add('DA DA', 'Danish', 'Dansk', [
|
||||
'Class:TriggerOnAttachmentDownload' => 'Trigger (on object\'s attachment download)~~',
|
||||
'Class:TriggerOnAttachmentDownload+' => 'Trigger on object\'s attachment download of [a child class of] the given class~~',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment create)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment create~~',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment creation)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment creation~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => 'Add file in email~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'If checked, the file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment delete)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment delete~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment deletion)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment deletion~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Add deleted file in email~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'If checked, the deleted file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Trigger on object is not allowed on class Attachment. Please use specific trigger~~',
|
||||
]);
|
||||
|
||||
@@ -84,11 +84,13 @@ Dict::Add('DE DE', 'German', 'Deutsch', [
|
||||
Dict::Add('DE DE', 'German', 'Deutsch', [
|
||||
'Class:TriggerOnAttachmentDownload' => 'Trigger (beim Herunterladen eines Attachment eines Objekts)',
|
||||
'Class:TriggerOnAttachmentDownload+' => 'Trigger für das Herunterladen des Attachments der angegebenen Klasse oder einer Unterklasse',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment create)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment create~~',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment creation)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment creation~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => 'Add file in email~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'If checked, the file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment delete)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment delete~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment deletion)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment deletion~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Add deleted file in email~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'If checked, the deleted file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Trigger on object is not allowed on class Attachment. Please use specific trigger~~',
|
||||
]);
|
||||
|
||||
@@ -91,11 +91,13 @@ Dict::Add('EN US', 'English', 'English', [
|
||||
Dict::Add('EN US', 'English', 'English', [
|
||||
'Class:TriggerOnAttachmentDownload' => 'Trigger (on object\'s attachment download)',
|
||||
'Class:TriggerOnAttachmentDownload+' => 'Trigger on object\'s attachment download of [a child class of] the given class',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment create)',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment create',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment delete)',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment delete',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment creation)',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment creation',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment deletion)',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment deletion',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => 'Add file in email',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'If checked, the file will be automatically attached to the email when an email action is triggered',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Add deleted file in email',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'If checked, the deleted file will be automatically attached to the email when an email action is triggered',
|
||||
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Trigger on object is not allowed on class Attachment. Please use specific trigger',
|
||||
]);
|
||||
|
||||
@@ -81,11 +81,13 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', [
|
||||
Dict::Add('ES CR', 'Spanish', 'Español, Castellano', [
|
||||
'Class:TriggerOnAttachmentDownload' => 'Disparador (al descargar el archivo adjunto del objeto)',
|
||||
'Class:TriggerOnAttachmentDownload+' => 'Disparador al descargar el archivo adjunto del objeto de [una clase secundaria de] la clase dada',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment create)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment create~~',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment creation)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment creation~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => 'Add file in email~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'If checked, the file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment delete)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment delete~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment deletion)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment deletion~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Add deleted file in email~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'If checked, the deleted file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Trigger on object is not allowed on class Attachment. Please use specific trigger~~',
|
||||
]);
|
||||
|
||||
@@ -89,5 +89,7 @@ Dict::Add('FR FR', 'French', 'Français', [
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'Si coché, le fichier sera automatiquement attaché à l\'email quand l\'action email est lancée',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Déclencheur sur la suppression d\'une pièce jointe',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Déclencheur sur la suppression d\'une pièce jointe d\'un objet',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Ajoute le fichier supprimé dans l\'email',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'Si coché, le fichier supprimé sera automatiquement joint à l\'email quand une action email sera lancée',
|
||||
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Les Triggers sur les objets ne sont pas autorisés sur la classe Attachement. Veuillez utiliser les triggers spécifiques pour cette classe',
|
||||
]);
|
||||
|
||||
@@ -81,11 +81,13 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', [
|
||||
Dict::Add('HU HU', 'Hungarian', 'Magyar', [
|
||||
'Class:TriggerOnAttachmentDownload' => 'Trigger (on object\'s attachment download)~~',
|
||||
'Class:TriggerOnAttachmentDownload+' => 'Trigger on object\'s attachment download of [a child class of] the given class~~',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment create)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment create~~',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment creation)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment creation~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => 'Add file in email~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'If checked, the file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment delete)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment delete~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment deletion)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment deletion~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Add deleted file in email~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'If checked, the deleted file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Trigger on object is not allowed on class Attachment. Please use specific trigger~~',
|
||||
]);
|
||||
|
||||
@@ -83,11 +83,13 @@ Dict::Add('IT IT', 'Italian', 'Italiano', [
|
||||
Dict::Add('IT IT', 'Italian', 'Italiano', [
|
||||
'Class:TriggerOnAttachmentDownload' => 'Trigger (al download di un allegato dell\'oggetto)',
|
||||
'Class:TriggerOnAttachmentDownload+' => 'Trigger al download di un allegato di un oggetto di [una sottoclasse di] la classe data',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment create)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment create~~',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment creation)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment creation~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => 'Add file in email~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'If checked, the file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment delete)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment delete~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment deletion)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment deletion~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Add deleted file in email~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'If checked, the deleted file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Trigger on object is not allowed on class Attachment. Please use specific trigger~~',
|
||||
]);
|
||||
|
||||
@@ -83,11 +83,13 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
|
||||
Dict::Add('JA JP', 'Japanese', '日本語', [
|
||||
'Class:TriggerOnAttachmentDownload' => 'Trigger (on object\'s attachment download)~~',
|
||||
'Class:TriggerOnAttachmentDownload+' => 'Trigger on object\'s attachment download of [a child class of] the given class~~',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment create)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment create~~',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment creation)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment creation~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => 'Add file in email~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'If checked, the file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment delete)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment delete~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment deletion)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment deletion~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Add deleted file in email~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'If checked, the deleted file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Trigger on object is not allowed on class Attachment. Please use specific trigger~~',
|
||||
]);
|
||||
|
||||
@@ -91,5 +91,7 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', [
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'If checked, the file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (Bij het verwijderen van een bijlage)',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger bij het verwijderen van een bijlage van een object van de opgegeven klasse (of subklasse ervan)',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Bestand toevoegen in e-mail',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'If checked, the deleted file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Trigger on object is not allowed on class Attachment. Please use specific trigger~~',
|
||||
]);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user