Compare commits
521
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a98a47db0 | ||
|
|
18a9da080a | ||
|
|
6104efdd47 | ||
|
|
7d400e7742 | ||
|
|
8295f833f8 | ||
|
|
8d6645b0ef | ||
|
|
a0cb9b2826 | ||
|
|
b2ff910a92 | ||
|
|
b70d2b482d | ||
|
|
ac22eef92f | ||
|
|
24ef23e133 | ||
|
|
c8fb306e67 | ||
|
|
667c2f5a47 | ||
|
|
8230efd722 | ||
|
|
f420b0b894 | ||
|
|
b196ee89b7 | ||
|
|
b998afcd93 | ||
|
|
8c0bdb4b0b | ||
|
|
66d090b4e3 | ||
|
|
b779123946 | ||
|
|
ed36b31576 | ||
|
|
4871c0bab1 | ||
|
|
bfe567f075 | ||
|
|
328c43c225 | ||
|
|
56de595457 | ||
|
|
7505efd433 | ||
|
|
934126ae6e | ||
|
|
632f04afd2 | ||
|
|
edae01a152 | ||
|
|
d418f4f4d5 | ||
|
|
da677c42ed | ||
|
|
467db7736c | ||
|
|
aced6916ff | ||
|
|
dc0005f394 | ||
|
|
0da1146fcd | ||
|
|
a7b4481c6c | ||
|
|
90ec85386b | ||
|
|
9baaad40d1 | ||
|
|
7dcc7303c2 | ||
|
|
7edc6ec11d | ||
|
|
f2589b5af5 | ||
|
|
23b09b1a7e | ||
|
|
951f9a9e46 | ||
|
|
8949fd2d22 | ||
|
|
5d6f4f2135 | ||
|
|
21520eebd6 | ||
|
|
87e20ceec6 | ||
|
|
f7b874d3ef | ||
|
|
dedc77c883 | ||
|
|
8110b80318 | ||
|
|
9de905cba3 | ||
|
|
802fa70154 | ||
|
|
ebf397702d | ||
|
|
388e5f9239 | ||
|
|
d20cdd4930 | ||
|
|
a604b2053e | ||
|
|
c177c305ba | ||
|
|
e840a0c7c4 | ||
|
|
c9d5c7f95b | ||
|
|
4c9d02e90b | ||
|
|
d39395a1ff | ||
|
|
d45ddc315a | ||
|
|
7d87ce35bc | ||
|
|
d4805faee7 | ||
|
|
3deccdbcf3 | ||
|
|
9ad24ac2a7 | ||
|
|
0751728b05 | ||
|
|
e504011b76 | ||
|
|
4d9ea1e2e3 | ||
|
|
6796829908 | ||
|
|
67f7d99696 | ||
|
|
27e3184550 | ||
|
|
c98fd7b219 | ||
|
|
301be352ae | ||
|
|
7c180f6e4f | ||
|
|
e119186e34 | ||
|
|
a282002c30 | ||
|
|
a2c36d602f | ||
|
|
a45917f132 | ||
|
|
fe358b8eab | ||
|
|
60f84f7378 | ||
|
|
45f7b7ad98 | ||
|
|
85aa6d3b57 | ||
|
|
33e201c4b5 | ||
|
|
c2890e15c3 | ||
|
|
784e09e240 | ||
|
|
214f3fbc64 | ||
|
|
b622e7e4fb | ||
|
|
a352c42742 | ||
|
|
8eff28b80c | ||
|
|
9f37db18d1 | ||
|
|
0d94f8bfaf | ||
|
|
509a2e18d0 | ||
|
|
22f748161a | ||
|
|
59ad07209b | ||
|
|
67abecc252 | ||
|
|
ebd0017f6e | ||
|
|
417f15197e | ||
|
|
ce40103c4f | ||
|
|
2748fdfc48 | ||
|
|
53cc663bde | ||
|
|
13a2cd71c4 | ||
|
|
a60ccd389b | ||
|
|
fd95dc3915 | ||
|
|
990e379ea8 | ||
|
|
1f5c21c631 | ||
|
|
b4144564c8 | ||
|
|
4ad18dc963 | ||
|
|
881535af7d | ||
|
|
1e66fb6ab3 | ||
|
|
e1b9e5efad | ||
|
|
f6dbe0f33e | ||
|
|
28570b43e8 | ||
|
|
a01382d756 | ||
|
|
1d6e7f15b9 | ||
|
|
97cc3ca581 | ||
|
|
0bcae3e87b | ||
|
|
ebd228c3d7 | ||
|
|
4b83a74bdb | ||
|
|
b348b84b63 | ||
|
|
7f4a8d5974 | ||
|
|
4d8ca457ec | ||
|
|
d677e948f1 | ||
|
|
d4e6af50bd | ||
|
|
c609cafa9b | ||
|
|
5556b0fe0c | ||
|
|
eee04f4703 | ||
|
|
127c6c4b53 | ||
|
|
7d6a1e54e6 | ||
|
|
8433b2b637 | ||
|
|
87b2153794 | ||
|
|
0fc5b2a95c | ||
|
|
6c2f93e85d | ||
|
|
4f1604fea1 | ||
|
|
059766291b | ||
|
|
e471c1fc8b | ||
|
|
0fb91ef859 | ||
|
|
c0a0a34cbb | ||
|
|
3d44a6fdf9 | ||
|
|
0eee307622 | ||
|
|
6a4aa04b47 | ||
|
|
643f073945 | ||
|
|
5ba40c65fc | ||
|
|
dcbbed4160 | ||
|
|
e94e165593 | ||
|
|
34709be6cc | ||
|
|
83107bb31a | ||
|
|
3caa43a5bf | ||
|
|
1efe860a89 | ||
|
|
d49965549c | ||
|
|
8d85259025 | ||
|
|
097d0456cc | ||
|
|
a62fa93c77 | ||
|
|
a22dcf05f2 | ||
|
|
afa6fa5ca2 | ||
|
|
c6effef622 | ||
|
|
ab31fbb0aa | ||
|
|
7d9a2c6e70 | ||
|
|
a486d5df33 | ||
|
|
3c8a621817 | ||
|
|
0e0aca55af | ||
|
|
0f02584143 | ||
|
|
a3db45609a | ||
|
|
d60a2a217c | ||
|
|
3e9ff6b5cb | ||
|
|
05a7d337de | ||
|
|
fdca086a47 | ||
|
|
42a81beeac | ||
|
|
9284b9e0b1 | ||
|
|
eb91f4fef4 | ||
|
|
68acc2aa51 | ||
|
|
5570583f70 | ||
|
|
eb979625d1 | ||
|
|
e1e5f527e4 | ||
|
|
f016dbd223 | ||
|
|
b2aa04ae1a | ||
|
|
e270adc50f | ||
|
|
fd5b8de533 | ||
|
|
e3d9b042c2 | ||
|
|
f4ae796b30 | ||
|
|
367d0386b6 | ||
|
|
5b54bed406 | ||
|
|
d3b60277d7 | ||
|
|
2ec6ff4ebc | ||
|
|
78a8d017a2 | ||
|
|
1a205d8727 | ||
|
|
396b5ea211 | ||
|
|
3cf9099f56 | ||
|
|
451b5f96e7 | ||
|
|
15ebae5025 | ||
|
|
32bfbadb8e | ||
|
|
5ed09ebefa | ||
|
|
eaea7a67ba | ||
|
|
de2114b8c5 | ||
|
|
64607ddbaa | ||
|
|
db1d999721 | ||
|
|
18047a7cfb | ||
|
|
def7ebd48c | ||
|
|
8c2b1ebdc5 | ||
|
|
9227517a50 | ||
|
|
5322d0f7b5 | ||
|
|
a282cc5691 | ||
|
|
048f8bcdf9 | ||
|
|
7bcbc748d4 | ||
|
|
f30e6cfe7f | ||
|
|
026e80e026 | ||
|
|
a3b77cff63 | ||
|
|
4ca68d6995 | ||
|
|
280cb7f2b1 | ||
|
|
095a73132c | ||
|
|
cfb0fc23ac | ||
|
|
6620aca9fe | ||
|
|
2422a02b23 | ||
|
|
b4a9dc0770 | ||
|
|
e7411244fd | ||
|
|
9f357173c3 | ||
|
|
7dc6e3ed5b | ||
|
|
5dfff212d0 | ||
|
|
17615f966b | ||
|
|
7a2d6b6c63 | ||
|
|
343867f6ee | ||
|
|
b9837ac77c | ||
|
|
bb73ec6c68 | ||
|
|
ee2d7bbb1b | ||
|
|
5d48bc7371 | ||
|
|
59a4649528 | ||
|
|
f54b6033ab | ||
|
|
bf340cd7d4 | ||
|
|
313417cb91 | ||
|
|
7850078623 | ||
|
|
44bf3b77b3 | ||
|
|
8485458896 | ||
|
|
3d253f5f06 | ||
|
|
64287d7e1a | ||
|
|
edc78a5a4c | ||
|
|
1a18d6c035 | ||
|
|
3e877182b5 | ||
|
|
0e4f883bc1 | ||
|
|
265cc62ab8 | ||
|
|
800c16970e | ||
|
|
6bd11bf233 | ||
|
|
a2fd82a3f3 | ||
|
|
7567990da1 | ||
|
|
c7d157bbfd | ||
|
|
2161edb871 | ||
|
|
e77e9c5691 | ||
|
|
e934a943b5 | ||
|
|
95eb58587b | ||
|
|
25628a8b76 | ||
|
|
7552e315fb | ||
|
|
d15b5b221e | ||
|
|
60e3fa97a9 | ||
|
|
fd1bd51aff | ||
|
|
b12620af9c | ||
|
|
d1af59fdf0 | ||
|
|
1b050ebc47 | ||
|
|
ff3e457c58 | ||
|
|
2ffaede07b | ||
|
|
2a83f57262 | ||
|
|
5be9193978 | ||
|
|
d3f264fc54 | ||
|
|
04ea002bf3 | ||
|
|
8f42a3a0ea | ||
|
|
4dd8fbe25d | ||
|
|
ee727c43e1 | ||
|
|
395213946b | ||
|
|
ed0b236208 | ||
|
|
8d56e75257 | ||
|
|
b3c27415d1 | ||
|
|
9839b0ee66 | ||
|
|
f3f57f9748 | ||
|
|
85d16a4354 | ||
|
|
c0517c3562 | ||
|
|
92e535a1c9 | ||
|
|
7411c2552b | ||
|
|
f2410f376e | ||
|
|
a9313c1367 | ||
|
|
5b3b9d770a | ||
|
|
4a54215fa1 | ||
|
|
52fbb34ddd | ||
|
|
c96dc7eaa1 | ||
|
|
f19409ba5b | ||
|
|
679c42ef90 | ||
|
|
3304c6f472 | ||
|
|
34f09fa260 | ||
|
|
fd4efd38cd | ||
|
|
e4e9bfc3e9 | ||
|
|
3774dbe38c | ||
|
|
4ef50642ad | ||
|
|
f95fb78022 | ||
|
|
0f9a8bde61 | ||
|
|
129d369c1a | ||
|
|
dd2c591a5b | ||
|
|
16749a724d | ||
|
|
a627930b58 | ||
|
|
404a62aa0b | ||
|
|
2d9cfc880b | ||
|
|
ee8bdfc4ac | ||
|
|
b55d484863 | ||
|
|
ed0058a503 | ||
|
|
b0aed393d6 | ||
|
|
a1ea203281 | ||
|
|
4590739fa5 | ||
|
|
7c617f2976 | ||
|
|
9141f61708 | ||
|
|
ba4120050f | ||
|
|
cbd4dc6e0d | ||
|
|
c7014e159b | ||
|
|
9febbc7db1 | ||
|
|
ec8e10f406 | ||
|
|
3233b24a4e | ||
|
|
5775e9cb18 | ||
|
|
570005f5e1 | ||
|
|
fbaa3440f2 | ||
|
|
e662468ecc | ||
|
|
c8efc30654 | ||
|
|
79d117026f | ||
|
|
2709054bdb | ||
|
|
2d4230c001 | ||
|
|
f7729508cf | ||
|
|
31e1acce67 | ||
|
|
aa1f3df76b | ||
|
|
3431f76320 | ||
|
|
1bc5466a8d | ||
|
|
d83daf44d3 | ||
|
|
2ad5ecd5d5 | ||
|
|
35c51a029a | ||
|
|
8c1128c85b | ||
|
|
c9021ab2d2 | ||
|
|
67a71f8f82 | ||
|
|
97d74f39e7 | ||
|
|
edd0a4a4a9 | ||
|
|
8a22672c7f | ||
|
|
11270eaaf5 | ||
|
|
1e32d59ecb | ||
|
|
eab27123dd | ||
|
|
a6253957c8 | ||
|
|
a8f5631144 | ||
|
|
6d98162e19 | ||
|
|
ee37ad0f4b | ||
|
|
2d71b5a63b | ||
|
|
f4f83bccbe | ||
|
|
30b114d538 | ||
|
|
c3db3457e7 | ||
|
|
7787af2df1 | ||
|
|
3730109cce | ||
|
|
b814608c06 | ||
|
|
9bab74d30e | ||
|
|
63a487bc0b | ||
|
|
a40eadcfce | ||
|
|
9b19d63a6a | ||
|
|
6939472f37 | ||
|
|
0387307301 | ||
|
|
25a5dde8e9 | ||
|
|
2e068bb73e | ||
|
|
842cacde19 | ||
|
|
6a453a0910 | ||
|
|
ff9903b14d | ||
|
|
d64bf5de22 | ||
|
|
9300d867fb | ||
|
|
eb0f8b5c24 | ||
|
|
d58768f26d | ||
|
|
3667217053 | ||
|
|
b237285703 | ||
|
|
a6c5581942 | ||
|
|
9a088d36b9 | ||
|
|
407734ff4a | ||
|
|
a74d963cfa | ||
|
|
02a47695e7 | ||
|
|
0f37cd3a45 | ||
|
|
5fe18bb78c | ||
|
|
b16dfa9ca1 | ||
|
|
3e6116fcba | ||
|
|
ef3bf496e9 | ||
|
|
bff293213d | ||
|
|
a47a1775f2 | ||
|
|
b2a2df7a8a | ||
|
|
34179f5b7d | ||
|
|
d38b744c08 | ||
|
|
ef58416814 | ||
|
|
62b7c9468e | ||
|
|
bafdb21b45 | ||
|
|
79080ffa93 | ||
|
|
f99f821a6d | ||
|
|
3b51475150 | ||
|
|
b41de5764b | ||
|
|
bb1c36f591 | ||
|
|
09f16c33d1 | ||
|
|
9d94658372 | ||
|
|
81f36df066 | ||
|
|
7afc2b9963 | ||
|
|
2e3369a09a | ||
|
|
212b2c999f | ||
|
|
2761f5cd20 | ||
|
|
d5831fea94 | ||
|
|
71afa21701 | ||
|
|
2736a513a3 | ||
|
|
bb654360bf | ||
|
|
ee0fd7dd5f | ||
|
|
dfe41d7f76 | ||
|
|
be81586b46 | ||
|
|
c33b06274d | ||
|
|
d72fc5c4e8 | ||
|
|
a41555cc0d | ||
|
|
c1bf3a7e15 | ||
|
|
5f6be40bdf | ||
|
|
8dc02fa081 | ||
|
|
a7ff32891e | ||
|
|
f6deaca452 | ||
|
|
ef01c27fad | ||
|
|
d8b4361936 | ||
|
|
0afb4f7283 | ||
|
|
4e746d367e | ||
|
|
e3d65967aa | ||
|
|
73408f1cce | ||
|
|
6e3a6917c8 | ||
|
|
b2b112eb8b | ||
|
|
56710d5b5b | ||
|
|
00ca9e0d2d | ||
|
|
65c76fcfab | ||
|
|
a686ba2a0e | ||
|
|
344245b1ea | ||
|
|
82b4f506c2 | ||
|
|
5e31fde416 | ||
|
|
b60665d096 | ||
|
|
24abc117fe | ||
|
|
5ae5fe338a | ||
|
|
5f8eac0464 | ||
|
|
fc951ee100 | ||
|
|
78bf04ef67 | ||
|
|
ae7db56f77 | ||
|
|
3dace4fc9d | ||
|
|
32912b0c35 | ||
|
|
f15be3ed65 | ||
|
|
46e99910e7 | ||
|
|
c3e7dad236 | ||
|
|
100f2c3bcd | ||
|
|
0aaac78cfa | ||
|
|
5795cf119a | ||
|
|
528583430f | ||
|
|
696a71166c | ||
|
|
5f082df3ab | ||
|
|
eb6bb99eaf | ||
|
|
fd2138c1fd | ||
|
|
0f3160f727 | ||
|
|
04ed514a21 | ||
|
|
4f382abc66 | ||
|
|
60f656006d | ||
|
|
5c777f3182 | ||
|
|
0ee0db8071 | ||
|
|
9bacd13ce2 | ||
|
|
c17d4e1a60 | ||
|
|
5f24de3085 | ||
|
|
d8df405db4 | ||
|
|
94cef504a3 | ||
|
|
ab32bac5d9 | ||
|
|
18765465ae | ||
|
|
ba9ddd11da | ||
|
|
cfcdc9c98d | ||
|
|
23d0a6b9b2 | ||
|
|
fee222daff | ||
|
|
edb516f71a | ||
|
|
b6031dea1a | ||
|
|
5b46c718e6 | ||
|
|
599ec18ecc | ||
|
|
cae106bfe8 | ||
|
|
119fde2033 | ||
|
|
0980350172 | ||
|
|
a27a4dcd84 | ||
|
|
adc2435073 | ||
|
|
a325ba339b | ||
|
|
096991ac79 | ||
|
|
a6a76d1815 | ||
|
|
6379f2fd35 | ||
|
|
2bb56f1b81 | ||
|
|
e993c23d5c | ||
|
|
71e16d20ef | ||
|
|
682b7656bc | ||
|
|
d765e69713 | ||
|
|
43920f9adc | ||
|
|
fdea8c28c8 | ||
|
|
ce372abf8a | ||
|
|
b1c3473856 | ||
|
|
52c21b0fca | ||
|
|
b7533b57b4 | ||
|
|
0dc13cf23f | ||
|
|
d3b96a163f | ||
|
|
95aba6d696 | ||
|
|
9030b1c19f | ||
|
|
79d241a910 | ||
|
|
c48d6f5a38 | ||
|
|
d9e890e954 | ||
|
|
ecdbd9653b | ||
|
|
e0bb4b6a23 | ||
|
|
2c41388096 | ||
|
|
0ec9423e0a | ||
|
|
efe24289bf | ||
|
|
03aa17e602 | ||
|
|
99d5a7e609 | ||
|
|
383617ec17 | ||
|
|
a39030f4ba | ||
|
|
e267b41575 | ||
|
|
6e3496d3f5 | ||
|
|
14f3336941 | ||
|
|
da43d2f08c | ||
|
|
827fa188f4 | ||
|
|
352b6fa45b | ||
|
|
8789a643ec | ||
|
|
7e1d1a1968 | ||
|
|
a2511ff09b | ||
|
|
8e837d387d | ||
|
|
801fb5f885 | ||
|
|
4a977b85a2 | ||
|
|
2fbda6e64b | ||
|
|
ac5539a8be | ||
|
|
d8b0616434 | ||
|
|
d97cbf486d | ||
|
|
90864d8e58 | ||
|
|
7813acfa2b | ||
|
|
3344274616 | ||
|
|
ce769ffff4 |
@@ -1,4 +1,3 @@
|
||||
**/*
|
||||
!tests/Umbraco.Tests.Integration/bin/**
|
||||
!tests/Umbraco.Tests.UnitTests/bin/**
|
||||
**/node_modules
|
||||
|
||||
@@ -13,16 +13,6 @@ If the answer is yes, please read on. Otherwise, make sure to head on over [to t
|
||||
|
||||
↖️ You can jump to any section by using the "table of contents" button (  ) above.
|
||||
|
||||
## Getting Started:
|
||||
To run umbraco, we first need to initialize the client git submodule:
|
||||
* Execute `git submodule init` and then `git submodule update` to get the files into Umbraco.Web.UI.Client project
|
||||
* If you are going to work on the Backoffice, you can either go to the Umbraco.Web.UI.Client folder and check out a new branch or set it up in your IDE, which will allow you to commit to each repository simultaneously:
|
||||
* **Rider**: Preferences -> Version Control -> Directory Mappings -> Click the '+' sign
|
||||
* If you get a white page delete Umbraco.Cms.StaticAssets\wwwroot\umbraco folder and run `npm ci && npm run build:for:cms` inside Umbraco.Web.UI.Client folder to clear out any leftover files from older versions.
|
||||
|
||||
### Latest version
|
||||
* If you want to get the latest changes from the client repository, run `git submodule update` again which will pull the latest main branch.
|
||||
|
||||
|
||||
## Debugging source locally
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# New backoffice
|
||||
|
||||
> **Warning**:
|
||||
> This is an early WIP and is set not to be packable since we don't want to release this yet. There will be breaking changes in these projects.
|
||||
|
||||
This solution folder contains the projects for the new backoffice. If you're looking to fix or improve the existing CMS, this is not the place to do it, although we do very much appreciate your efforts.
|
||||
|
||||
### Project structure
|
||||
|
||||
Since the new backoffice API is still very much a work in progress, we've created new projects for the new backoffice API:
|
||||
|
||||
* Umbrao.Cms.ManagementApi - The "presentation layer" for the management API
|
||||
* "New" versions of existing projects, should be merged with the existing projects when the new API is released:
|
||||
* Umbraco.New.Cms.Core
|
||||
* Umbraco.New.Cms.Infrastructure
|
||||
* Umbraco.New.Cms.Web.Common
|
||||
|
||||
This also means that we have to use "InternalsVisibleTo" for the new projects since these should be able to access the internal classes since they will when they get merged.
|
||||
@@ -23,7 +23,6 @@ env:
|
||||
DOTNET_GENERATE_ASPNET_CERTIFICATE: false
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
@@ -38,7 +37,6 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
+5
-11
@@ -50,6 +50,10 @@ tools/docfx/
|
||||
# Ignore rule for clearing out Belle (avoid rebuilding all the time)
|
||||
preserve.belle
|
||||
|
||||
# Ignore rule for output of generated documentation files from grunt docserve
|
||||
/src/Umbraco.Web.UI.Docs/api/
|
||||
/src/Umbraco.Web.UI.Docs/package-lock.json
|
||||
|
||||
# csharp-docs
|
||||
/build/csharp-docs/api/
|
||||
/build/csharp-docs/_site/
|
||||
@@ -64,13 +68,7 @@ preserve.belle
|
||||
/build/docs.zip
|
||||
/build/ui-docs.zip
|
||||
/build/csharp-docs.zip
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/auth
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/backoffice
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/js
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/lib
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/views
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/login
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/
|
||||
|
||||
# Environment specific data
|
||||
/src/Umbraco.Web.UI.Client/[Bb]uild/
|
||||
@@ -93,7 +91,6 @@ preserve.belle
|
||||
|
||||
# Tests
|
||||
/tests/Umbraco.Tests.AcceptanceTest/.env
|
||||
/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth
|
||||
/tests/Umbraco.Tests.Integration.SqlCe/DatabaseContextTests.sdf
|
||||
/tests/Umbraco.Tests.Integration.SqlCe/[Uu]mbraco/[Dd]ata/TEMP/
|
||||
/tests/Umbraco.Tests.Integration/appsettings.Tests.Local.json
|
||||
@@ -106,13 +103,10 @@ preserve.belle
|
||||
# Ignore auto-generated schema
|
||||
/src/Umbraco.Cms.Targets/tasks/
|
||||
/src/Umbraco.Cms.Targets/appsettings-schema.*.json
|
||||
/src/Umbraco.Cms.Targets/umbraco-package-schema.json
|
||||
/src/Umbraco.Web.UI/appsettings-schema.json
|
||||
/src/Umbraco.Web.UI/appsettings-schema.*.json
|
||||
/src/Umbraco.Web.UI/umbraco-package-schema.json
|
||||
/tests/Umbraco.Tests.Integration/appsettings-schema.json
|
||||
/tests/Umbraco.Tests.Integration/appsettings-schema.*.json
|
||||
/tests/Umbraco.Tests.Integration/umbraco-package-schema.json
|
||||
/src/Umbraco.Cms/appsettings-schema.json
|
||||
playwright-report
|
||||
trace.zip
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "src/Umbraco.Web.UI.Client"]
|
||||
path = src/Umbraco.Web.UI.Client
|
||||
url = https://github.com/umbraco/Umbraco.CMS.Backoffice.git
|
||||
@@ -28,14 +28,14 @@
|
||||
|
||||
<!-- Package Validation -->
|
||||
<PropertyGroup>
|
||||
<GenerateCompatibilitySuppressionFile>true</GenerateCompatibilitySuppressionFile>
|
||||
<EnablePackageValidation>false</EnablePackageValidation>
|
||||
<PackageValidationBaselineVersion>14.0.0</PackageValidationBaselineVersion>
|
||||
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
|
||||
<EnablePackageValidation>true</EnablePackageValidation>
|
||||
<PackageValidationBaselineVersion>13.0.0</PackageValidationBaselineVersion>
|
||||
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
|
||||
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Calculate version only once for the whole repository -->
|
||||
<!-- Calculate version only once for the whole repository -->
|
||||
<PropertyGroup>
|
||||
<GitVersionBaseDirectory>$(MSBuildThisFileDirectory)</GitVersionBaseDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
+46
-32
@@ -5,34 +5,36 @@
|
||||
</PropertyGroup>
|
||||
<!-- Global packages (private, build-time packages for all projects) -->
|
||||
<ItemGroup>
|
||||
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.6.133" />
|
||||
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.6.139" />
|
||||
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
|
||||
<GlobalPackageReference Include="Umbraco.Code" Version="2.1.0" />
|
||||
<GlobalPackageReference Include="Umbraco.Code" Version="2.2.0" />
|
||||
<GlobalPackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.2.0" />
|
||||
</ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.10.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.10.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
<!-- Umbraco packages -->
|
||||
<ItemGroup>
|
||||
@@ -41,50 +43,62 @@
|
||||
</ItemGroup>
|
||||
<!-- Third-party packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.0.0" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.0.0" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="7.1.1" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="7.1.0" />
|
||||
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
|
||||
<PackageVersion Include="Examine" Version="3.2.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.2.0" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.57" />
|
||||
<PackageVersion Include="JsonPatch.Net" Version="2.1.0" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.6" />
|
||||
<PackageVersion Include="MailKit" Version="4.3.0" />
|
||||
<PackageVersion Include="Examine" Version="3.8.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.8.0" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.74" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
|
||||
<PackageVersion Include="MailKit" Version="4.8.0" />
|
||||
<PackageVersion Include="Markdown" Version="2.2.1" />
|
||||
<PackageVersion Include="MessagePack" Version="2.5.140" />
|
||||
<PackageVersion Include="MessagePack" Version="2.5.192" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
|
||||
<PackageVersion Include="MiniProfiler.Shared" Version="4.3.8" />
|
||||
<PackageVersion Include="ncrontab" Version="3.3.3" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageVersion Include="NPoco" Version="5.7.1" />
|
||||
<PackageVersion Include="NPoco.SqlServer" Version="5.7.1" />
|
||||
<PackageVersion Include="OpenIddict.Abstractions" Version="4.10.1" />
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="4.10.1" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="4.10.1" />
|
||||
<PackageVersion Include="Serilog" Version="3.1.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Process" Version="2.0.2" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.4" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.3" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.5" />
|
||||
<PackageVersion Include="Smidge.InMemory" Version="4.6.0" />
|
||||
<PackageVersion Include="Smidge.Nuglify" Version="4.6.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
|
||||
</ItemGroup>
|
||||
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
|
||||
<ItemGroup>
|
||||
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Azure.Identity -->
|
||||
<PackageVersion Include="Azure.Identity" Version="1.10.4" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.13.2" />
|
||||
<!-- Dazinator.Extensions.FileProviders brings in a vulnerable version of System.Net.Http -->
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<!-- Examine brings in a vulnerable version of System.Security.Cryptography.Xml -->
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.3" />
|
||||
<!-- Both Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc bring in a vulnerable version of System.Text.RegularExpressions -->
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
|
||||
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.7.1" />
|
||||
<!-- Examine.Lucene bring in a vulnerable version of Lucene.Net.Replicator -->
|
||||
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
|
||||
<!-- Both OpenIddict.AspNetCore, Microsoft.EntityFrameworkCore.* bring in a vulnerable version of Microsoft.Extensions.Caching.Memory -->
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||
<!-- Both Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer,NPoco.SqlServer, and more bring in a vulnerable version of System.Text.Json -->
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.6" />
|
||||
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Microsoft.Data.SqlClient -->
|
||||
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+224
-187
@@ -17,10 +17,6 @@ parameters:
|
||||
displayName: Deploy to NuGet
|
||||
type: boolean
|
||||
default: false
|
||||
- name: npmDeploy
|
||||
displayName: Deploy to Npm
|
||||
type: boolean
|
||||
default: false
|
||||
- name: buildApiDocs
|
||||
displayName: Build API docs
|
||||
type: boolean
|
||||
@@ -29,6 +25,10 @@ parameters:
|
||||
displayName: Upload API docs
|
||||
type: boolean
|
||||
default: false
|
||||
- name: uploadDependencyTrack
|
||||
displayName: Upload BOMs to Dependency Track
|
||||
type: boolean
|
||||
default: false
|
||||
- name: forceReleaseTestFilter
|
||||
displayName: Force to use the release test filters
|
||||
type: boolean
|
||||
@@ -71,39 +71,61 @@ stages:
|
||||
- stage: Build
|
||||
variables:
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_client
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
jobs:
|
||||
- job: A
|
||||
displayName: Build Umbraco CMS
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
vmImage: 'windows-latest'
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: true
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 500
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js $(nodeVersion)
|
||||
retryCountOnTaskFailure: 3
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
- task: Cache@2
|
||||
displayName: Cache node_modules
|
||||
inputs:
|
||||
key: '"npm_client" | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/package-lock.json'
|
||||
restoreKeys: |
|
||||
"npm_client" | "$(Agent.OS)"
|
||||
"npm_client"
|
||||
path: $(npm_config_cache)
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
displayName: Run npm ci (Backoffice)
|
||||
- powershell: |
|
||||
npm install --global @cyclonedx/cyclonedx-npm
|
||||
cyclonedx-npm -o $(Build.ArtifactStagingDirectory)\bom\bom-backoffice.xml --ignore-npm-errors --verbose
|
||||
displayName: Generate Backoffice UI BOM
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
- task: gulp@0
|
||||
displayName: Run gulp build (Backoffice)
|
||||
inputs:
|
||||
gulpFile: src/Umbraco.Web.UI.Client/gulpfile.js
|
||||
targets: coreBuild
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
- task: npm@1
|
||||
displayName: Run npm ci (Login)
|
||||
inputs:
|
||||
command: custom
|
||||
workingDir: src/Umbraco.Web.UI.Login
|
||||
verbose: false
|
||||
customCommand: ci
|
||||
- powershell: |
|
||||
cyclonedx-npm -o $(Build.ArtifactStagingDirectory)\bom\bom-login.xml --ignore-npm-errors --verbose
|
||||
displayName: Generate Login UI BOM
|
||||
workingDirectory: src/Umbraco.Web.UI.Login
|
||||
- script: npm run build
|
||||
- task: npm@1
|
||||
displayName: Run npm build (Login)
|
||||
workingDirectory: src/Umbraco.Web.UI.Login
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
displayName: Run npm ci (Bellissima)
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
- script: npm run generate:api-local
|
||||
displayName: Generate API models (Bellissima)
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
enabled: false
|
||||
- script: npm run build:for:cms
|
||||
displayName: Run build (Bellissima)
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
- script: npm run build
|
||||
displayName: Run Login Build (Bellissima)
|
||||
workingDirectory: src/Umbraco.Web.UI.Client/apps/auth
|
||||
inputs:
|
||||
command: custom
|
||||
workingDir: src/Umbraco.Web.UI.Login
|
||||
verbose: false
|
||||
customCommand: run build
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
@@ -120,6 +142,10 @@ stages:
|
||||
command: build
|
||||
projects: $(solution)
|
||||
arguments: '--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg'
|
||||
- powershell: |
|
||||
dotnet tool install --global CycloneDX
|
||||
dotnet-CycloneDX $(solution) --output $(Build.ArtifactStagingDirectory)/bom --filename bom-dotnet.xml
|
||||
displayName: 'Generate Backend BOM'
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish nupkg
|
||||
inputs:
|
||||
@@ -130,24 +156,35 @@ stages:
|
||||
inputs:
|
||||
targetPath: $(Build.SourcesDirectory)
|
||||
artifactName: build_output
|
||||
- bash: |
|
||||
echo "##[command]Running npm version"
|
||||
echo "##[debug]Version: $PACKAGE_VERSION"
|
||||
echo "##[command]Running npm pack"
|
||||
echo "##[debug]Output directory: $(Build.ArtifactStagingDirectory)"
|
||||
npm version $PACKAGE_VERSION --allow-same-version --no-git-tag-version
|
||||
mkdir $(Build.ArtifactStagingDirectory)/npm
|
||||
npm pack --pack-destination $(Build.ArtifactStagingDirectory)/npm
|
||||
mv .npmrc $(Build.ArtifactStagingDirectory)/npm/
|
||||
displayName: Prepare Bellissima npm package
|
||||
env:
|
||||
PACKAGE_VERSION: $(build.NBGV_NpmPackageVersion)
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish Bellissima npm artifact
|
||||
displayName: Publish Backend BOM
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)/npm
|
||||
artifactName: npm
|
||||
targetPath: $(Build.ArtifactStagingDirectory)/bom
|
||||
artifactName: bom-build
|
||||
|
||||
- stage: E2E_BOM
|
||||
displayName: E2E Tests BOM Generation
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- job:
|
||||
displayName: E2E Generate BOM
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: 500
|
||||
- template: templates/e2e-install.yml
|
||||
parameters:
|
||||
nodeVersion: ${{ variables.nodeVersion }}
|
||||
npm_config_cache: ${{ variables.npm_config_cache }}
|
||||
- powershell: |
|
||||
npm install --global @cyclonedx/cyclonedx-npm
|
||||
cyclonedx-npm -o $(Build.ArtifactStagingDirectory)/bom/bom-e2e.xml --ignore-npm-errors --verbose
|
||||
displayName: Generate E2E Tests BOM
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
- publish: $(Build.ArtifactStagingDirectory)/bom
|
||||
artifact: bom-e2e
|
||||
displayName: 'Publish E2E BOM'
|
||||
|
||||
- stage: Build_Docs
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.buildApiDocs}}))
|
||||
@@ -210,41 +247,34 @@ stages:
|
||||
displayName: Build js API Reference
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
variables:
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_client
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
BASE_PATH: /v$(umbracoMajorVersion)/ui
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: true
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 1
|
||||
fetchFilter: tree:0
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js $(nodeVersion)
|
||||
displayName: Use Node.js 10.15.x
|
||||
retryCountOnTaskFailure: 3
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
- task: Cache@2
|
||||
displayName: Cache node_modules
|
||||
inputs:
|
||||
key: '"npm_client" | "$(Agent.OS)"| $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/package-lock.json'
|
||||
restoreKeys: |
|
||||
"npm_client" | "$(Agent.OS)"
|
||||
"npm_client"
|
||||
path: $(npm_config_cache)
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
displayName: Run npm ci
|
||||
- script: npm run storybook:build
|
||||
displayName: Build Storybook
|
||||
env:
|
||||
VITE_BASE_PATH: $(BASE_PATH)/
|
||||
workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client
|
||||
- script: sed -i "s|/umbraco/backoffice|$(BASE_PATH)/umbraco/backoffice|" assets/*.js
|
||||
displayName: Replace BASE_PATH on assets
|
||||
workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/storybook-static
|
||||
versionSpec: 10.15.x # Won't work with higher versions
|
||||
- script: |
|
||||
npm ci --no-fund --no-audit --prefer-offline
|
||||
npx gulp docs
|
||||
|
||||
major="$(umbracoMajorVersion)"
|
||||
echo "major version: $major"
|
||||
|
||||
baseUrl="https://apidocs.umbraco.com/v$major/ui/"
|
||||
echo "baseUrl: $baseUrl"
|
||||
|
||||
sed -i "s|baseUrl = .*|baseUrl = '$baseUrl',|" api/index.html
|
||||
displayName: Generate js Docs
|
||||
workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Docs
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Archive js Docs
|
||||
inputs:
|
||||
rootFolderOrFile: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/storybook-static
|
||||
rootFolderOrFile: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Docs/api
|
||||
includeRootFolder: false
|
||||
archiveFile: $(Build.ArtifactStagingDirectory)/ui-docs.zip
|
||||
- task: PublishPipelineArtifact@1
|
||||
@@ -274,6 +304,11 @@ stages:
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 1
|
||||
fetchFilter: tree:0
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download build artifacts
|
||||
inputs:
|
||||
@@ -313,6 +348,11 @@ stages:
|
||||
variables:
|
||||
Tests__Database__DatabaseType: 'Sqlite'
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 1
|
||||
fetchFilter: tree:0
|
||||
# Setup test environment
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download build artifacts
|
||||
@@ -343,8 +383,9 @@ stages:
|
||||
|
||||
# Integration Tests (SQL Server)
|
||||
- job:
|
||||
timeoutInMinutes: 180
|
||||
# condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerIntegrationTests}}) # Outcommented due to timeouts
|
||||
timeoutInMinutes: 120
|
||||
# We are currently encountering issues when running SQL Server Linux tests Microsoft.Data.SqlClient.SqlException (0x80131904)
|
||||
# condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerIntegrationTests}})
|
||||
condition: eq(${{parameters.sqlServerIntegrationTests}}, True)
|
||||
displayName: Integration Tests (SQL Server)
|
||||
strategy:
|
||||
@@ -441,10 +482,24 @@ stages:
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
|
||||
strategy:
|
||||
matrix:
|
||||
Linux:
|
||||
vmImage: 'ubuntu-latest'
|
||||
Windows:
|
||||
vmImage: 'windows-latest'
|
||||
LinuxPart1Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testCommand: "npm run test -- --shard=1/3"
|
||||
LinuxPart2Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testCommand: "npm run test -- --shard=2/3"
|
||||
LinuxPart3Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testCommand: "npm run test -- --shard=3/3"
|
||||
WindowsPart1Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run test -- --shard=1/3"
|
||||
WindowsPart2Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run test -- --shard=2/3"
|
||||
WindowsPart3Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run test -- --shard=3/3"
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
@@ -455,37 +510,18 @@ stages:
|
||||
artifact: nupkg
|
||||
path: $(Agent.BuildDirectory)/app/nupkg
|
||||
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js $(nodeVersion)
|
||||
retryCountOnTaskFailure: 3
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
|
||||
- pwsh: |
|
||||
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
|
||||
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
URL=$(ASPNETCORE_URLS)" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Cache and restore NPM packages
|
||||
- task: Cache@2
|
||||
displayName: Cache NPM packages
|
||||
inputs:
|
||||
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
|
||||
restoreKeys: |
|
||||
npm_e2e | "$(Agent.OS)"
|
||||
npm_e2e
|
||||
path: $(npm_config_cache)
|
||||
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
displayName: Restore NPM packages
|
||||
- template: templates/e2e-install.yml
|
||||
parameters:
|
||||
nodeVersion: $(nodeVersion)
|
||||
npm_config_cache: $(npm_config_cache)
|
||||
PlaywrightUserEmail: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
|
||||
PlaywrightPassword: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
ASPNETCORE_URLS: $(ASPNETCORE_URLS)
|
||||
|
||||
# Build application
|
||||
- pwsh: |
|
||||
@@ -522,12 +558,12 @@ stages:
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Install Playwright and dependencies
|
||||
- pwsh: npx playwright install --with-deps
|
||||
displayName: Install Playwright
|
||||
- pwsh: npx playwright install chromium
|
||||
displayName: Install Playwright only with Chromium browser
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Test
|
||||
- pwsh: npm run test --ignore-certificate-errors
|
||||
- pwsh: $(testCommand)
|
||||
displayName: Run Playwright tests
|
||||
continueOnError: true
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
@@ -548,35 +584,62 @@ stages:
|
||||
# Copy artifacts
|
||||
- pwsh: |
|
||||
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
|
||||
}
|
||||
displayName: Copy Playwright results
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Publish
|
||||
# Publish test artifacts
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish test artifacts
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
|
||||
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
|
||||
|
||||
# Publish test results
|
||||
- task: PublishTestResults@2
|
||||
displayName: "Publish test results"
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
testResultsFormat: 'JUnit'
|
||||
testResultsFiles: '*.xml'
|
||||
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
|
||||
testRunTitle: "$(Agent.JobName)"
|
||||
|
||||
- job:
|
||||
displayName: E2E Tests (SQL Server)
|
||||
# condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerAcceptanceTests}}) # Outcommented due to timeouts
|
||||
condition: eq(${{parameters.sqlServerAcceptanceTests}}, True)
|
||||
condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerAcceptanceTests}})
|
||||
variables:
|
||||
# Connection string
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
strategy:
|
||||
matrix:
|
||||
Linux:
|
||||
vmImage: 'ubuntu-latest'
|
||||
LinuxPart1Of3:
|
||||
testCommand: "npm run testSqlite -- --shard=1/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: 'Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True'
|
||||
Windows:
|
||||
vmImage: 'windows-latest'
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
LinuxPart2Of3:
|
||||
testCommand: "npm run testSqlite -- --shard=2/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
LinuxPart3Of3:
|
||||
testCommand: "npm run testSqlite -- --shard=3/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
WindowsPart1Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run testSqlite -- --shard=1/3"
|
||||
WindowsPart2Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run testSqlite -- --shard=2/3"
|
||||
WindowsPart3Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run testSqlite -- --shard=3/3"
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
@@ -662,14 +725,13 @@ stages:
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Install Playwright and dependencies
|
||||
- pwsh: npx playwright install --with-deps
|
||||
displayName: Install Playwright
|
||||
- pwsh: npx playwright install chromium
|
||||
displayName: Install Playwright only with Chromium browser
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Test
|
||||
- pwsh: npm run test --ignore-certificate-errors
|
||||
- pwsh: $(testCommand)
|
||||
displayName: Run Playwright tests
|
||||
continueOnError: true
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
env:
|
||||
CI: true
|
||||
@@ -697,18 +759,56 @@ stages:
|
||||
# Copy artifacts
|
||||
- pwsh: |
|
||||
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
|
||||
}
|
||||
displayName: Copy Playwright results
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Publish
|
||||
# Publish test artifacts
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish test artifacts
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
|
||||
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
|
||||
|
||||
# Publish test results
|
||||
- task: PublishTestResults@2
|
||||
displayName: "Publish test results"
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
testResultsFormat: 'JUnit'
|
||||
testResultsFiles: '*.xml'
|
||||
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
|
||||
testRunTitle: "$(Agent.JobName)"
|
||||
|
||||
- stage: Dependency_Track
|
||||
displayName: Dependency Track
|
||||
dependsOn:
|
||||
- Build
|
||||
- E2E_BOM
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadDependencyTrack}}))
|
||||
variables:
|
||||
# Determine Umbraco version based on whether it's a public release or not. If public release, use major version, else use full NuGet package version.
|
||||
umbracoVersion: $[ iif(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'], stageDependencies.Build.A.outputs['build.NBGV_NuGetPackageVersion']) ]
|
||||
jobs:
|
||||
- template: templates/dependency-track.yml
|
||||
parameters:
|
||||
projectName: "Umbraco-CMS"
|
||||
umbracoVersion: $(umbracoVersion)
|
||||
projects:
|
||||
- name: "Backend"
|
||||
artifact: "bom-build"
|
||||
bomFilePath: "bom-dotnet.xml"
|
||||
- name: "Login"
|
||||
artifact: "bom-build"
|
||||
bomFilePath: "bom-login.xml"
|
||||
- name: "Backoffice"
|
||||
artifact: "bom-build"
|
||||
bomFilePath: "bom-backoffice.xml"
|
||||
- name: "E2E"
|
||||
artifact: "bom-e2e"
|
||||
bomFilePath: "bom-e2e.xml"
|
||||
|
||||
###############################################
|
||||
## Release
|
||||
@@ -718,10 +818,12 @@ stages:
|
||||
dependsOn:
|
||||
- Unit
|
||||
- Integration
|
||||
# - E2E
|
||||
# - E2E # TODO: Enable when stable.
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}}))
|
||||
jobs:
|
||||
- job:
|
||||
pool:
|
||||
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
|
||||
displayName: Push to pre-release feed
|
||||
steps:
|
||||
- checkout: none
|
||||
@@ -740,41 +842,6 @@ stages:
|
||||
publishFeedCredentials: 'MyGet - Umbraco Nightly'
|
||||
${{ else }}:
|
||||
publishFeedCredentials: 'MyGet - Pre-releases'
|
||||
- job:
|
||||
displayName: Push to pre-release feed (npm)
|
||||
steps:
|
||||
- checkout: none
|
||||
- download: current
|
||||
artifact: npm
|
||||
- bash: |
|
||||
# Check if we are on a nightly build
|
||||
if [ $isNightly = "False" ]; then
|
||||
echo "##[debug]Prerelease build detected"
|
||||
registry="https://www.myget.org/F/umbracoprereleases/npm/"
|
||||
else
|
||||
echo "##[debug]Nightly build detected"
|
||||
registry="https://www.myget.org/F/umbraconightly/npm/"
|
||||
fi
|
||||
echo "@umbraco-cms:registry=$registry" >> .npmrc
|
||||
env:
|
||||
isNightly: ${{parameters.isNightly}}
|
||||
workingDirectory: $(Pipeline.Workspace)/npm
|
||||
displayName: Add scoped registry to .npmrc
|
||||
- task: npmAuthenticate@0
|
||||
displayName: Authenticate with npm (MyGet)
|
||||
inputs:
|
||||
workingFile: '$(Pipeline.Workspace)/npm/.npmrc'
|
||||
customEndpoint: 'MyGet (npm) - Umbracoprereleases, MyGet (npm) - Umbraconightly'
|
||||
- bash: |
|
||||
# Setup temp npm project to load in defaults from the local .npmrc
|
||||
npm init -y
|
||||
|
||||
# Find the first .tgz file in the current directory and publish it
|
||||
files=( ./*.tgz )
|
||||
npm publish --access public "${files[0]}"
|
||||
displayName: Push to npm (MyGet)
|
||||
workingDirectory: $(Pipeline.Workspace)/npm
|
||||
|
||||
- stage: Deploy_NuGet
|
||||
displayName: NuGet release
|
||||
dependsOn:
|
||||
@@ -783,6 +850,8 @@ stages:
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}}))
|
||||
jobs:
|
||||
- job:
|
||||
pool:
|
||||
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
|
||||
displayName: Push to NuGet
|
||||
steps:
|
||||
- checkout: none
|
||||
@@ -799,45 +868,15 @@ stages:
|
||||
nuGetFeedType: 'external'
|
||||
publishFeedCredentials: 'NuGet - Umbraco.*'
|
||||
|
||||
- stage: Deploy_Npm
|
||||
displayName: Npm release
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.npmDeploy}}))
|
||||
dependsOn:
|
||||
- Deploy_NuGet
|
||||
jobs:
|
||||
- job: Publish
|
||||
displayName: Push to NPM
|
||||
steps:
|
||||
- checkout: none
|
||||
- download: current
|
||||
artifact: npm
|
||||
- bash: echo "@umbraco-cms:registry=https://registry.npmjs.org" >> .npmrc
|
||||
workingDirectory: $(Pipeline.Workspace)/npm
|
||||
displayName: Add scoped registry to .npmrc
|
||||
- task: npmAuthenticate@0
|
||||
displayName: Authenticate with npm
|
||||
inputs:
|
||||
workingFile: $(Pipeline.Workspace)/npm/.npmrc
|
||||
customEndpoint: 'NPM - Umbraco Backoffice'
|
||||
- script: |
|
||||
# Setup temp npm project to load in defaults from the local .npmrc
|
||||
npm init -y
|
||||
|
||||
# Find the first .tgz file in the current directory and publish it
|
||||
files=( ./*.tgz )
|
||||
npm publish --tag $npmTag --access public "${files[0]}"
|
||||
displayName: Push to npm
|
||||
workingDirectory: $(Pipeline.Workspace)/npm
|
||||
|
||||
- stage: Upload_API_Docs
|
||||
pool:
|
||||
vmImage: 'windows-latest' # Apparently AzureFileCopy is windows only :(
|
||||
variables:
|
||||
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'] ]
|
||||
displayName: Upload API Documentation
|
||||
displayName: Upload API Documention
|
||||
dependsOn:
|
||||
- Build
|
||||
- Deploy_MyGet # Change to "Deploy_Nuget" after release of v14
|
||||
- Deploy_NuGet
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadApiDocs}}))
|
||||
jobs:
|
||||
- job:
|
||||
@@ -862,7 +901,6 @@ stages:
|
||||
storage: umbracoapidocs
|
||||
ContainerName: '$web'
|
||||
BlobPrefix: v$(umbracoMajorVersion)/csharp
|
||||
CleanTargetBeforeCopy: true
|
||||
- job:
|
||||
displayName: Upload js Docs
|
||||
steps:
|
||||
@@ -885,4 +923,3 @@ stages:
|
||||
storage: umbracoapidocs
|
||||
ContainerName: '$web'
|
||||
BlobPrefix: v$(umbracoMajorVersion)/ui
|
||||
CleanTargetBeforeCopy: true
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
parameters:
|
||||
- name: projectName
|
||||
type: string
|
||||
- name: umbracoVersion
|
||||
type: string
|
||||
- name: projects
|
||||
type: object
|
||||
|
||||
jobs:
|
||||
- job: Create_DT_Project
|
||||
displayName: Create Dependency Track Project
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
- bash: |
|
||||
project_id=$(curl --no-progress-meter -H "X-Api-Key: $(DT_API_KEY)" "$(DT_API_URL)/v1/project/lookup?name=${{ parameters.projectName }}&version=${{ parameters.umbracoVersion }}" | jq -r '.uuid')
|
||||
if [ "$project_id" != "null" ] && [ -n "$project_id" ]; then
|
||||
echo "Project '${{ parameters.projectName }}' with version '${{ parameters.umbracoVersion }}' already exists (ID: $project_id)."
|
||||
else
|
||||
project_id=$(curl --no-progress-meter \
|
||||
-X PUT "$(DT_API_URL)/v1/project" \
|
||||
-H "X-Api-Key: $(DT_API_KEY)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "${{ parameters.projectName }}", "version": "${{ parameters.umbracoVersion }}", "collectionLogic": "AGGREGATE_DIRECT_CHILDREN"}' \
|
||||
| jq -r '.uuid')
|
||||
if [ -z "$project_id" ] || [ "$project_id" == "null" ]; then
|
||||
echo "Failed to create project '${{ parameters.projectName }}' version '${{ parameters.umbracoVersion }}'."
|
||||
exit 1
|
||||
fi
|
||||
echo "Created project '${{ parameters.projectName }}' with version '${{ parameters.umbracoVersion }}' (ID: $project_id)."
|
||||
fi
|
||||
displayName: Ensure main project exists in Dependency Track
|
||||
|
||||
- ${{ each project in parameters.projects }}:
|
||||
- job:
|
||||
displayName: Upload ${{ project.name }} BOM
|
||||
dependsOn: Create_DT_Project
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
- download: current
|
||||
artifact: ${{ project.artifact }}
|
||||
displayName: Download ${{ project.artifact }} artifact
|
||||
|
||||
- script: |
|
||||
curl --no-progress-meter --fail-with-body \
|
||||
-X POST "$(DT_API_URL)/v1/bom" \
|
||||
-H "X-Api-Key: $(DT_API_KEY)" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "autoCreate=true" \
|
||||
-F "projectName=${{ parameters.projectName }}-${{ project.name }}" \
|
||||
-F "projectVersion=${{ parameters.umbracoVersion }}" \
|
||||
-F "parentName=${{ parameters.projectName }}" \
|
||||
-F "parentVersion=${{ parameters.umbracoVersion }}" \
|
||||
-F "bom=@$(Pipeline.Workspace)/${{ project.artifact }}/${{ project.bomFilePath }}"
|
||||
displayName: Upload ${{ project.name }} BOM to Dependency Track
|
||||
@@ -0,0 +1,47 @@
|
||||
parameters:
|
||||
- name: nodeVersion
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: npm_config_cache
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: PlaywrightUserEmail
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: PlaywrightPassword
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: ASPNETCORE_URLS
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js $(nodeVersion)
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
|
||||
- pwsh: |
|
||||
"UMBRACO_USER_LOGIN=${{ parameters.PlaywrightUserEmail }}
|
||||
UMBRACO_USER_PASSWORD=${{ parameters.PlaywrightPassword }}
|
||||
URL=${{ parameters.ASPNETCORE_URLS }}" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Cache and restore NPM packages
|
||||
- task: Cache@2
|
||||
displayName: Cache NPM packages
|
||||
inputs:
|
||||
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
|
||||
restoreKeys: |
|
||||
npm_e2e | "$(Agent.OS)"
|
||||
npm_e2e
|
||||
path: ${{ parameters.npm_config_cache }}
|
||||
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
displayName: Restore NPM packages
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.100",
|
||||
"version": "8.0.415",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace Umbraco.Cms.Api.Common.Attributes;
|
||||
|
||||
public abstract class ShortGenericSchemaNameAttribute : Attribute
|
||||
{
|
||||
public Type[] GenericTypes { get; set; }
|
||||
|
||||
public string SchemaName { get; set; }
|
||||
|
||||
public ShortGenericSchemaNameAttribute(string schemaName, Type[] genericTypes)
|
||||
{
|
||||
GenericTypes = genericTypes;
|
||||
SchemaName = schemaName;
|
||||
}
|
||||
}
|
||||
|
||||
public class ShortGenericSchemaNameAttribute<T1, T2> : ShortGenericSchemaNameAttribute
|
||||
{
|
||||
public ShortGenericSchemaNameAttribute(string schemaName)
|
||||
: base(schemaName, new[] { typeof(T1), typeof(T2) })
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Builders;
|
||||
|
||||
@@ -9,8 +8,6 @@ public class ProblemDetailsBuilder
|
||||
private string? _title;
|
||||
private string? _detail;
|
||||
private string? _type;
|
||||
private string? _operationStatus;
|
||||
private IDictionary<string, object>? _extensions;
|
||||
|
||||
public ProblemDetailsBuilder WithTitle(string title)
|
||||
{
|
||||
@@ -30,45 +27,11 @@ public class ProblemDetailsBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProblemDetailsBuilder WithOperationStatus<TEnum>(TEnum operationStatus)
|
||||
where TEnum : Enum
|
||||
{
|
||||
_operationStatus = operationStatus.ToString();
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProblemDetailsBuilder WithRequestModelErrors(IDictionary<string, string[]> errors)
|
||||
=> WithExtension(nameof(HttpValidationProblemDetails.Errors).ToFirstLowerInvariant(), errors);
|
||||
|
||||
public ProblemDetailsBuilder WithExtension(string key, object value)
|
||||
{
|
||||
_extensions ??= new Dictionary<string, object>();
|
||||
_extensions[key] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProblemDetails Build()
|
||||
{
|
||||
var problemDetails = new ProblemDetails
|
||||
public ProblemDetails Build() =>
|
||||
new()
|
||||
{
|
||||
Title = _title,
|
||||
Detail = _detail,
|
||||
Type = _type ?? "Error",
|
||||
};
|
||||
|
||||
if (_operationStatus is not null)
|
||||
{
|
||||
problemDetails.Extensions["operationStatus"] = _operationStatus;
|
||||
}
|
||||
|
||||
if (_extensions is not null)
|
||||
{
|
||||
foreach (KeyValuePair<string, object> extension in _extensions)
|
||||
{
|
||||
problemDetails.Extensions[extension.Key] = extension.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return problemDetails;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Umbraco.Cms.Api.Common.Security;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
|
||||
using Umbraco.Cms.Infrastructure.HostedServices;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
public static class UmbracoBuilderAuthExtensions
|
||||
{
|
||||
private static bool _initialized;
|
||||
|
||||
public static IUmbracoBuilder AddUmbracoOpenIddict(this IUmbracoBuilder builder)
|
||||
{
|
||||
if (builder.Services.Any(x=>x.ImplementationType == typeof(OpenIddictCleanupJob)) is false)
|
||||
if (_initialized is false)
|
||||
{
|
||||
ConfigureOpenIddict(builder);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
return builder;
|
||||
@@ -33,17 +34,15 @@ public static class UmbracoBuilderAuthExtensions
|
||||
// FIXME: swap paths here so member API is first (see comment above)
|
||||
options
|
||||
.SetAuthorizationEndpointUris(
|
||||
Paths.MemberApi.AuthorizationEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
|
||||
Paths.BackOfficeApi.AuthorizationEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
Paths.MemberApi.AuthorizationEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetTokenEndpointUris(
|
||||
Paths.MemberApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
|
||||
Paths.BackOfficeApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
Paths.MemberApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetLogoutEndpointUris(
|
||||
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
|
||||
Paths.BackOfficeApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetRevocationEndpointUris(
|
||||
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
|
||||
Paths.BackOfficeApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
|
||||
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetUserinfoEndpointUris(
|
||||
Paths.MemberApi.UserinfoEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
|
||||
|
||||
// Enable authorization code flow with PKCE
|
||||
options
|
||||
@@ -55,7 +54,8 @@ public static class UmbracoBuilderAuthExtensions
|
||||
options
|
||||
.UseAspNetCore()
|
||||
.EnableAuthorizationEndpointPassthrough()
|
||||
.EnableLogoutEndpointPassthrough();
|
||||
.EnableLogoutEndpointPassthrough()
|
||||
.EnableUserinfoEndpointPassthrough();
|
||||
|
||||
// Enable reference tokens
|
||||
// - see https://documentation.openiddict.com/configuration/token-storage.html
|
||||
@@ -104,6 +104,6 @@ public static class UmbracoBuilderAuthExtensions
|
||||
options.UseDataProtection();
|
||||
});
|
||||
|
||||
builder.Services.AddRecurringBackgroundJob<OpenIddictCleanupJob>();
|
||||
builder.Services.AddHostedService<OpenIddictCleanup>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Mvc.ActionResults;
|
||||
|
||||
/// <summary>
|
||||
/// A "created at" action result with no response body.
|
||||
/// </summary>
|
||||
public sealed class EmptyCreatedAtActionResult : ActionResult
|
||||
{
|
||||
private readonly string _actionName;
|
||||
private readonly string _controllerName;
|
||||
private readonly object _routeValues;
|
||||
private readonly string _resourceIdentifier;
|
||||
|
||||
public EmptyCreatedAtActionResult(string actionName, string controllerName, object routeValues, string resourceIdentifier)
|
||||
{
|
||||
_actionName = actionName;
|
||||
_controllerName = controllerName;
|
||||
_routeValues = routeValues;
|
||||
_resourceIdentifier = resourceIdentifier;
|
||||
}
|
||||
|
||||
public override void ExecuteResult(ActionContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
HttpRequest request = context.HttpContext.Request;
|
||||
IUrlHelper urlHelper = context.HttpContext.RequestServices.GetRequiredService<IUrlHelperFactory>().GetUrlHelper(context);
|
||||
|
||||
var url = urlHelper.Action(
|
||||
_actionName,
|
||||
_controllerName,
|
||||
_routeValues,
|
||||
request.Scheme,
|
||||
request.Host.ToUriComponent());
|
||||
|
||||
if (string.IsNullOrEmpty(url))
|
||||
{
|
||||
throw new InvalidOperationException("No routes could be found that matched the provided route components");
|
||||
}
|
||||
|
||||
context.HttpContext.Response.StatusCode = StatusCodes.Status201Created;
|
||||
context.HttpContext.Response.Headers.Location = url;
|
||||
context.HttpContext.Response.Headers[Constants.Headers.GeneratedResource] = _resourceIdentifier;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,16 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
public class OperationIdSelector : IOperationIdSelector
|
||||
{
|
||||
private readonly UmbracoOperationIdSettings _umbracoOperationIdSettings;
|
||||
|
||||
[Obsolete("Use non obsolete constructor")]
|
||||
public OperationIdSelector() : this(StaticServiceProvider.Instance.GetRequiredService<IOptions<UmbracoOperationIdSettings>>())
|
||||
{
|
||||
}
|
||||
|
||||
public OperationIdSelector(IOptions<UmbracoOperationIdSettings> umbracoOperationIdSettings)
|
||||
{
|
||||
_umbracoOperationIdSettings = umbracoOperationIdSettings.Value;
|
||||
}
|
||||
|
||||
public virtual string? OperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions)
|
||||
{
|
||||
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var controllerTypeInfoNamespace = controllerActionDescriptor.ControllerTypeInfo.Namespace;
|
||||
if (controllerTypeInfoNamespace is not null && _umbracoOperationIdSettings.NameSpacePrefixes.Any(prefix => controllerTypeInfoNamespace.StartsWith(prefix)) is false)
|
||||
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor
|
||||
|| controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith("Umbraco.Cms.Api") is not true)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Umbraco.Cms.Api.Common.Attributes;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
@@ -11,9 +10,18 @@ public class SchemaIdSelector : ISchemaIdSelector
|
||||
|
||||
protected string UmbracoSchemaId(Type type)
|
||||
{
|
||||
var name = SanitizedTypeName(type);
|
||||
string SanitizedTypeName(Type t) => t.Name
|
||||
// first grab the "non generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel")
|
||||
.Split('`').First()
|
||||
// then remove the "ViewModel" postfix from type names
|
||||
.TrimEnd("ViewModel");
|
||||
|
||||
name = HandleGenerics(name, type);
|
||||
var name = SanitizedTypeName(type);
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
// append the generic type names, ultimately turning i.e. "PagedViewModel<RelationItemViewModel>" into "PagedRelationItem"
|
||||
name = $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}";
|
||||
}
|
||||
|
||||
if (name.EndsWith("Model") == false)
|
||||
{
|
||||
@@ -25,58 +33,4 @@ public class SchemaIdSelector : ISchemaIdSelector
|
||||
// make absolutely sure we don't pass any invalid named by removing all non-word chars
|
||||
return Regex.Replace(name, @"[^\w]", string.Empty);
|
||||
}
|
||||
|
||||
private string SanitizedTypeName(Type t) => t.Name
|
||||
// first grab the "non generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel")
|
||||
.Split('`').First()
|
||||
// then remove the "ViewModel" postfix from type names
|
||||
.TrimEnd("ViewModel");
|
||||
|
||||
private string HandleGenerics(string name, Type type)
|
||||
{
|
||||
if (!type.IsGenericType)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
// find all types that implement this type and have an matching attribute
|
||||
var assignableTypesWithAttributeInfo = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(assembly => assembly.FullName?.StartsWith("Umbraco") == true)
|
||||
.SelectMany(assembly => assembly.GetTypes())
|
||||
.Where(t => t.IsAssignableTo(type))
|
||||
.Select(t =>
|
||||
{
|
||||
var attribute = System.Attribute.GetCustomAttributes(t)
|
||||
.FirstOrDefault(attribute => attribute is ShortGenericSchemaNameAttribute) as
|
||||
ShortGenericSchemaNameAttribute;
|
||||
return attribute == null
|
||||
? new ShortSchemaNameAttributeInfo(t)
|
||||
: new ShortSchemaNameAttributeInfo(t, attribute.GenericTypes, attribute.SchemaName);
|
||||
})
|
||||
.Where(info => info.GenericTypes != null);
|
||||
|
||||
var matchingType = assignableTypesWithAttributeInfo
|
||||
.SingleOrDefault(t => t.GenericTypes!.Length == type.GenericTypeArguments.Length
|
||||
&& t.GenericTypes.Intersect(type.GenericTypeArguments).Count() ==
|
||||
type.GenericTypeArguments.Length && t.SchemaName.IsNullOrWhiteSpace() == false);
|
||||
|
||||
// use attribute custom name or append the generic type names, ultimately turning i.e. "PagedViewModel<RelationItemViewModel>" into "PagedRelationItem"
|
||||
return matchingType != null
|
||||
? matchingType.SchemaName!
|
||||
: $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}";
|
||||
}
|
||||
|
||||
private class ShortSchemaNameAttributeInfo
|
||||
{
|
||||
public Type Type { get; set; }
|
||||
public Type[]? GenericTypes { get; set; }
|
||||
public string? SchemaName { get; set; }
|
||||
|
||||
public ShortSchemaNameAttributeInfo(Type type, Type[]? genericTypes = null, string? schemaName = null)
|
||||
{
|
||||
Type = type;
|
||||
GenericTypes = genericTypes;
|
||||
SchemaName = schemaName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ public class SwaggerRouteTemplatePipelineFilter : UmbracoPipelineFilter
|
||||
swaggerUiOptions.SwaggerEndpoint($"{name}/swagger.json", $"{apiInfo.Title}");
|
||||
}
|
||||
|
||||
swaggerUiOptions.OAuthClientId(Constants.OAuthClientIds.Swagger);
|
||||
swaggerUiOptions.OAuthUsePkce();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
public class UmbracoOperationIdSettings
|
||||
{
|
||||
private HashSet<string> _nameSpacePrefixes = new HashSet<string>()
|
||||
{
|
||||
"Umbraco.Cms.Api"
|
||||
};
|
||||
|
||||
public IReadOnlySet<string> NameSpacePrefixes
|
||||
{
|
||||
get => _nameSpacePrefixes;
|
||||
}
|
||||
|
||||
public bool AddNameSpacePrefix(string prefix) => _nameSpacePrefixes.Add(prefix);
|
||||
}
|
||||
@@ -1,22 +1,7 @@
|
||||
namespace Umbraco.Cms.Api.Common.Security;
|
||||
namespace Umbraco.Cms.Api.Common.Security;
|
||||
|
||||
public static class Paths
|
||||
{
|
||||
public static class BackOfficeApi
|
||||
{
|
||||
public const string EndpointTemplate = "security/back-office";
|
||||
|
||||
public static readonly string AuthorizationEndpoint = EndpointPath($"{EndpointTemplate}/authorize");
|
||||
|
||||
public static readonly string TokenEndpoint = EndpointPath($"{EndpointTemplate}/token");
|
||||
|
||||
public static readonly string LogoutEndpoint = EndpointPath($"{EndpointTemplate}/signout");
|
||||
|
||||
public static readonly string RevokeEndpoint = EndpointPath($"{EndpointTemplate}/revoke");
|
||||
|
||||
private static string EndpointPath(string relativePath) => $"/umbraco/management/api/v1/{relativePath}";
|
||||
}
|
||||
|
||||
public static class MemberApi
|
||||
{
|
||||
public const string EndpointTemplate = "security/member";
|
||||
@@ -29,6 +14,8 @@ public static class Paths
|
||||
|
||||
public static readonly string RevokeEndpoint = EndpointPath($"{EndpointTemplate}/revoke");
|
||||
|
||||
public static readonly string UserinfoEndpoint = EndpointPath($"{EndpointTemplate}/userinfo");
|
||||
|
||||
// NOTE: we're NOT using /api/v1.0/ here because it will clash with the Delivery API docs
|
||||
private static string EndpointPath(string relativePath) => $"/umbraco/delivery/api/v1/{relativePath}";
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class UmbracoJsonTypeInfoResolver : DefaultJsonTypeInfoResolver, I
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
var result = _typeFinder.FindClassesOfType(type).OrderBy(x => x.Name).ToHashSet();
|
||||
var result = _typeFinder.FindClassesOfType(type).ToHashSet();
|
||||
_subTypesCache.TryAdd(type, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -9,16 +9,19 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asp.Versioning.Mvc "/>
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
<PackageReference Include="OpenIddict.Abstractions" />
|
||||
<PackageReference Include="OpenIddict.AspNetCore" />
|
||||
|
||||
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens"/>
|
||||
|
||||
<!-- Take top-level depedendency on OpenIddict.AspNetCore depends on a vulnerable version -->
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
|
||||
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.OutputCaching;
|
||||
using Microsoft.AspNetCore.OutputCaching;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Caching;
|
||||
@@ -7,9 +8,13 @@ namespace Umbraco.Cms.Api.Delivery.Caching;
|
||||
internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
|
||||
{
|
||||
private readonly TimeSpan _duration;
|
||||
private readonly StringValues _varyByHeaderNames;
|
||||
|
||||
public DeliveryApiOutputCachePolicy(TimeSpan duration)
|
||||
=> _duration = duration;
|
||||
public DeliveryApiOutputCachePolicy(TimeSpan duration, StringValues varyByHeaderNames)
|
||||
{
|
||||
_duration = duration;
|
||||
_varyByHeaderNames = varyByHeaderNames;
|
||||
}
|
||||
|
||||
ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -18,8 +23,14 @@ internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
|
||||
.RequestServices
|
||||
.GetRequiredService<IRequestPreviewService>();
|
||||
|
||||
context.EnableOutputCaching = requestPreviewService.IsPreview() is false;
|
||||
IApiAccessService apiAccessService = context
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<IApiAccessService>();
|
||||
|
||||
context.EnableOutputCaching = requestPreviewService.IsPreview() is false && apiAccessService.HasPublicAccess();
|
||||
context.ResponseExpirationTimeSpan = _duration;
|
||||
context.CacheVaryByRules.HeaderNames = _varyByHeaderNames;
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
|
||||
@@ -16,10 +15,11 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
[ApiVersion("2.0")]
|
||||
public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
{
|
||||
private readonly IRequestRoutingService _requestRoutingService;
|
||||
private readonly IApiContentPathResolver _apiContentPathResolver;
|
||||
private readonly IRequestRedirectService _requestRedirectService;
|
||||
private readonly IRequestPreviewService _requestPreviewService;
|
||||
private readonly IRequestMemberAccessService _requestMemberAccessService;
|
||||
private const string PreviewContentRequestPathPrefix = $"/{Umbraco.Cms.Core.Constants.DeliveryApi.Routing.PreviewContentPathPrefix}";
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
public ByRouteContentApiController(
|
||||
@@ -58,7 +58,7 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete($"Please use the constructor that accepts {nameof(IApiContentPathResolver)}. Will be removed in V15.")]
|
||||
public ByRouteContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
@@ -66,12 +66,50 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
IRequestRedirectService requestRedirectService,
|
||||
IRequestPreviewService requestPreviewService,
|
||||
IRequestMemberAccessService requestMemberAccessService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
requestRedirectService,
|
||||
requestPreviewService,
|
||||
requestMemberAccessService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IApiContentPathResolver>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete($"Please use the non-obsolete constructor. Will be removed in V15.")]
|
||||
public ByRouteContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService,
|
||||
IRequestRoutingService requestRoutingService,
|
||||
IRequestRedirectService requestRedirectService,
|
||||
IRequestPreviewService requestPreviewService,
|
||||
IRequestMemberAccessService requestMemberAccessService,
|
||||
IApiContentPathResolver apiContentPathResolver)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
requestRedirectService,
|
||||
requestPreviewService,
|
||||
requestMemberAccessService,
|
||||
apiContentPathResolver)
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ByRouteContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IRequestRedirectService requestRedirectService,
|
||||
IRequestPreviewService requestPreviewService,
|
||||
IRequestMemberAccessService requestMemberAccessService,
|
||||
IApiContentPathResolver apiContentPathResolver)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
{
|
||||
_requestRoutingService = requestRoutingService;
|
||||
_requestRedirectService = requestRedirectService;
|
||||
_requestPreviewService = requestPreviewService;
|
||||
_requestMemberAccessService = requestMemberAccessService;
|
||||
_apiContentPathResolver = apiContentPathResolver;
|
||||
}
|
||||
|
||||
[HttpGet("item/{*path}")]
|
||||
@@ -105,10 +143,13 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
private async Task<IActionResult> HandleRequest(string path)
|
||||
{
|
||||
path = DecodePath(path);
|
||||
|
||||
path = path.TrimStart("/");
|
||||
path = path.Length == 0 ? "/" : path;
|
||||
|
||||
if (_apiContentPathResolver.IsResolvablePath(path) is false)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
IPublishedContent? contentItem = GetContent(path);
|
||||
if (contentItem is not null)
|
||||
{
|
||||
@@ -128,17 +169,12 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
}
|
||||
|
||||
private IPublishedContent? GetContent(string path)
|
||||
=> path.StartsWith(Constants.DeliveryApi.Routing.PreviewContentPathPrefix)
|
||||
=> path.StartsWith(PreviewContentRequestPathPrefix)
|
||||
? GetPreviewContent(path)
|
||||
: GetPublishedContent(path);
|
||||
|
||||
private IPublishedContent? GetPublishedContent(string path)
|
||||
{
|
||||
var contentRoute = _requestRoutingService.GetContentRoute(path);
|
||||
|
||||
IPublishedContent? contentItem = ApiPublishedContentCache.GetByRoute(contentRoute);
|
||||
return contentItem;
|
||||
}
|
||||
=> _apiContentPathResolver.ResolveContentPath(path);
|
||||
|
||||
private IPublishedContent? GetPreviewContent(string path)
|
||||
{
|
||||
@@ -147,7 +183,7 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Guid.TryParse(path.AsSpan(Constants.DeliveryApi.Routing.PreviewContentPathPrefix.Length).TrimEnd("/"), out Guid contentId) is false)
|
||||
if (Guid.TryParse(path.AsSpan(PreviewContentRequestPathPrefix.Length).TrimEnd("/"), out Guid contentId) is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
[ApiExplorerSettings(GroupName = "Content")]
|
||||
[LocalizeFromAcceptLanguageHeader]
|
||||
[ValidateStartItem]
|
||||
[AddVaryHeader]
|
||||
[OutputCache(PolicyName = Constants.DeliveryApi.OutputCache.ContentCachePolicy)]
|
||||
public abstract class ContentApiControllerBase : DeliveryApiControllerBase
|
||||
{
|
||||
|
||||
@@ -1,18 +1,35 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
|
||||
public abstract class ContentApiItemControllerBase : ContentApiControllerBase
|
||||
{
|
||||
// TODO: Remove this in V14 when the obsolete constructors have been removed
|
||||
private readonly IPublicAccessService _publicAccessService;
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
protected ContentApiItemControllerBase(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService)
|
||||
: this(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
{
|
||||
}
|
||||
|
||||
protected ContentApiItemControllerBase(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
{
|
||||
}
|
||||
=> _publicAccessService = StaticServiceProvider.Instance.GetRequiredService<IPublicAccessService>();
|
||||
|
||||
[Obsolete($"Please use {nameof(IPublicAccessService)} to test for content protection. Will be removed in V14.")]
|
||||
protected bool IsProtected(IPublishedContent content) => _publicAccessService.IsProtected(content.Path);
|
||||
|
||||
protected async Task<IActionResult?> HandleMemberAccessAsync(IPublishedContent contentItem, IRequestMemberAccessService requestMemberAccessService)
|
||||
{
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Attributes;
|
||||
using Umbraco.Cms.Api.Common.Filters;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Features;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[JsonOptionsName(Constants.JsonOptionsNames.DeliveryApi)]
|
||||
[MapToApi(DeliveryApiConfiguration.ApiName)]
|
||||
[Authorize(Policy = AuthorizationPolicies.UmbracoFeatureEnabled)]
|
||||
public abstract class DeliveryApiControllerBase : Controller, IUmbracoFeature
|
||||
public abstract class DeliveryApiControllerBase : Controller
|
||||
{
|
||||
protected string DecodePath(string path)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpenIddict.Server.AspNetCore;
|
||||
using Umbraco.Cms.Api.Delivery.Routing;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Security;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiController]
|
||||
[VersionedDeliveryApiRoute(Common.Security.Paths.MemberApi.EndpointTemplate)]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
|
||||
public class CurrentMemberController : DeliveryApiControllerBase
|
||||
{
|
||||
private readonly ICurrentMemberClaimsProvider _currentMemberClaimsProvider;
|
||||
|
||||
public CurrentMemberController(ICurrentMemberClaimsProvider currentMemberClaimsProvider)
|
||||
=> _currentMemberClaimsProvider = currentMemberClaimsProvider;
|
||||
|
||||
[HttpGet("userinfo")]
|
||||
public async Task<IActionResult> Userinfo()
|
||||
{
|
||||
Dictionary<string, object> claims = await _currentMemberClaimsProvider.GetClaimsAsync();
|
||||
return Ok(claims);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Security.Claims;
|
||||
using System.Security.Claims;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
@@ -15,12 +15,13 @@ using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
using Umbraco.Extensions;
|
||||
using IdentitySignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||||
using SignInResult = Microsoft.AspNetCore.Mvc.SignInResult;
|
||||
using IdentitySignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Security;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiController]
|
||||
[VersionedDeliveryApiRoute(Common.Security.Paths.MemberApi.EndpointTemplate)]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class MemberController : DeliveryApiControllerBase
|
||||
@@ -159,11 +160,12 @@ public class MemberController : DeliveryApiControllerBase
|
||||
claim.SetDestinations(OpenIddictConstants.Destinations.AccessToken);
|
||||
}
|
||||
|
||||
if (request.GetScopes().Contains(OpenIddictConstants.Scopes.OfflineAccess))
|
||||
{
|
||||
// "offline_access" scope is required to use refresh tokens
|
||||
memberPrincipal.SetScopes(OpenIddictConstants.Scopes.OfflineAccess);
|
||||
}
|
||||
// "openid" and "offline_access" are the only scopes allowed for members; explicitly ensure we only add those
|
||||
// NOTE: the "offline_access" scope is required to use refresh tokens
|
||||
IEnumerable<string> allowedScopes = request
|
||||
.GetScopes()
|
||||
.Intersect(new[] { OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess });
|
||||
memberPrincipal.SetScopes(allowedScopes);
|
||||
|
||||
return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, memberPrincipal);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Delivery.Accessors;
|
||||
using Umbraco.Cms.Api.Delivery.Caching;
|
||||
@@ -33,26 +34,33 @@ public static class UmbracoBuilderExtensions
|
||||
builder.Services.AddScoped<IRequestStartItemProvider, RequestStartItemProvider>();
|
||||
builder.Services.AddScoped<RequestContextOutputExpansionStrategy>();
|
||||
builder.Services.AddScoped<RequestContextOutputExpansionStrategyV2>();
|
||||
builder.Services.AddScoped<IOutputExpansionStrategy>(provider =>
|
||||
{
|
||||
HttpContext? httpContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
|
||||
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
|
||||
if (apiVersion is null)
|
||||
{
|
||||
return provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
|
||||
}
|
||||
|
||||
// V1 of the Delivery API uses a different expansion strategy than V2+
|
||||
return apiVersion.MajorVersion == 1
|
||||
? provider.GetRequiredService<RequestContextOutputExpansionStrategy>()
|
||||
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
|
||||
});
|
||||
builder.Services.AddUnique<IOutputExpansionStrategy>(
|
||||
provider =>
|
||||
{
|
||||
HttpContext? httpContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
|
||||
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
|
||||
if (apiVersion is null)
|
||||
{
|
||||
return provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
|
||||
}
|
||||
|
||||
// V1 of the Delivery API uses a different expansion strategy than V2+
|
||||
return apiVersion.MajorVersion == 1
|
||||
? provider.GetRequiredService<RequestContextOutputExpansionStrategy>()
|
||||
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
|
||||
},
|
||||
ServiceLifetime.Scoped);
|
||||
|
||||
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
|
||||
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
|
||||
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
|
||||
builder.Services.AddSingleton<IRequestPreviewService, RequestPreviewService>();
|
||||
builder.Services.AddSingleton<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>();
|
||||
|
||||
// Webooks register a more basic implementation, remove it.
|
||||
builder.Services.AddUnique<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>(ServiceLifetime.Singleton);
|
||||
builder.Services.AddSingleton<IRequestStartItemProviderAccessor, RequestContextRequestStartItemProviderAccessor>();
|
||||
|
||||
builder.Services.AddSingleton<IApiAccessService, ApiAccessService>();
|
||||
builder.Services.AddSingleton<IApiContentQueryService, ApiContentQueryService>();
|
||||
builder.Services.AddSingleton<IApiContentQueryProvider, ApiContentQueryProvider>();
|
||||
@@ -60,6 +68,7 @@ public static class UmbracoBuilderExtensions
|
||||
builder.Services.AddSingleton<IApiMediaQueryService, ApiMediaQueryService>();
|
||||
builder.Services.AddTransient<IMemberApplicationManager, MemberApplicationManager>();
|
||||
builder.Services.AddTransient<IRequestMemberAccessService, RequestMemberAccessService>();
|
||||
builder.Services.AddTransient<ICurrentMemberClaimsProvider, CurrentMemberClaimsProvider>();
|
||||
|
||||
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
|
||||
builder.AddUmbracoApiOpenApiUI();
|
||||
@@ -107,12 +116,20 @@ public static class UmbracoBuilderExtensions
|
||||
|
||||
if (outputCacheSettings.ContentDuration.TotalSeconds > 0)
|
||||
{
|
||||
options.AddPolicy(Constants.DeliveryApi.OutputCache.ContentCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.ContentDuration));
|
||||
options.AddPolicy(
|
||||
Constants.DeliveryApi.OutputCache.ContentCachePolicy,
|
||||
new DeliveryApiOutputCachePolicy(
|
||||
outputCacheSettings.ContentDuration,
|
||||
new StringValues([Constants.DeliveryApi.HeaderNames.AcceptLanguage, Constants.DeliveryApi.HeaderNames.StartItem])));
|
||||
}
|
||||
|
||||
if (outputCacheSettings.MediaDuration.TotalSeconds > 0)
|
||||
{
|
||||
options.AddPolicy(Constants.DeliveryApi.OutputCache.MediaCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.MediaDuration));
|
||||
options.AddPolicy(
|
||||
Constants.DeliveryApi.OutputCache.MediaCachePolicy,
|
||||
new DeliveryApiOutputCachePolicy(
|
||||
outputCacheSettings.MediaDuration,
|
||||
Constants.DeliveryApi.HeaderNames.StartItem));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
public sealed class AddVaryHeaderAttribute : ActionFilterAttribute
|
||||
{
|
||||
private const string Vary = "Accept-Language, Preview, Start-Item";
|
||||
|
||||
public override void OnResultExecuting(ResultExecutingContext context)
|
||||
=> context.HttpContext.Response.Headers.Vary = context.HttpContext.Response.Headers.Vary.Count > 0
|
||||
? $"{context.HttpContext.Response.Headers.Vary}, {Vary}"
|
||||
: Vary;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
@@ -21,7 +21,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Accept-Language",
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.AcceptLanguage,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Defines the language to return. Use this when querying language variant content items.",
|
||||
@@ -37,7 +37,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Preview",
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.Preview,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Whether to request draft content.",
|
||||
@@ -46,7 +46,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Start-Item",
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.StartItem,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "URL segment or GUID of a root content item.",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
@@ -63,7 +63,7 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
|
||||
protected void AddApiKey(OpenApiOperation operation) =>
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Api-Key",
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.ApiKey,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "API key specified through configuration to authorize access to the API.",
|
||||
|
||||
+49
-22
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
@@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
using Umbraco.Cms.Infrastructure.Security;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Handlers;
|
||||
@@ -16,16 +17,21 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
|
||||
private readonly ILogger<InitializeMemberApplicationNotificationHandler> _logger;
|
||||
private readonly DeliveryApiSettings _deliveryApiSettings;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly IServerRoleAccessor _serverRoleAccessor;
|
||||
private static readonly SemaphoreSlim _locker = new(1);
|
||||
private static bool _isInitialized = false;
|
||||
|
||||
public InitializeMemberApplicationNotificationHandler(
|
||||
IRuntimeState runtimeState,
|
||||
IOptions<DeliveryApiSettings> deliveryApiSettings,
|
||||
ILogger<InitializeMemberApplicationNotificationHandler> logger,
|
||||
IServiceScopeFactory serviceScopeFactory)
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
IServerRoleAccessor serverRoleAccessor)
|
||||
{
|
||||
_runtimeState = runtimeState;
|
||||
_logger = logger;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_serverRoleAccessor = serverRoleAccessor;
|
||||
_deliveryApiSettings = deliveryApiSettings.Value;
|
||||
}
|
||||
|
||||
@@ -36,34 +42,55 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
|
||||
return;
|
||||
}
|
||||
|
||||
// we cannot inject the IMemberApplicationManager because it ultimately takes a dependency on the DbContext ... and during
|
||||
// install that is not allowed (no connection string means no DbContext)
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMemberApplicationManager memberApplicationManager = scope.ServiceProvider.GetRequiredService<IMemberApplicationManager>();
|
||||
|
||||
if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true)
|
||||
if (_serverRoleAccessor.CurrentServerRole is ServerRole.Subscriber)
|
||||
{
|
||||
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
|
||||
// subscriber instances should not alter the member application
|
||||
return;
|
||||
}
|
||||
|
||||
if (ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls) is false)
|
||||
try
|
||||
{
|
||||
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
await _locker.WaitAsync(cancellationToken);
|
||||
if (_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls.Any()
|
||||
&& ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls) is false)
|
||||
_isInitialized = true;
|
||||
|
||||
// we cannot inject the IMemberApplicationManager because it ultimately takes a dependency on the DbContext ... and during
|
||||
// install that is not allowed (no connection string means no DbContext)
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMemberApplicationManager memberApplicationManager = scope.ServiceProvider.GetRequiredService<IMemberApplicationManager>();
|
||||
|
||||
if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true)
|
||||
{
|
||||
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls) is false)
|
||||
{
|
||||
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls.Any()
|
||||
&& ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls) is false)
|
||||
{
|
||||
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await memberApplicationManager.EnsureMemberApplicationAsync(
|
||||
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls,
|
||||
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls,
|
||||
cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
|
||||
return;
|
||||
_locker.Release();
|
||||
}
|
||||
|
||||
await memberApplicationManager.EnsureMemberApplicationAsync(
|
||||
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls,
|
||||
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private bool ValidateRedirectUrls(Uri[] redirectUrls)
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Abstractions;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
@@ -73,7 +73,7 @@ internal sealed class RevokeMemberAuthenticationTokensNotificationHandler
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Revoking {count} active tokens for member with ID {id}", tokens.Length, member.Id);
|
||||
_logger.LogInformation("Deleting {count} active tokens for member with ID {id}", tokens.Length, member.Id);
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
await _tokenManager.DeleteAsync(token);
|
||||
|
||||
@@ -12,23 +12,36 @@ public class DeliveryApiJsonTypeResolver : DefaultJsonTypeInfoResolver
|
||||
{
|
||||
JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);
|
||||
|
||||
if (jsonTypeInfo.Type == typeof(IApiContent))
|
||||
Type[] derivedTypes = GetDerivedTypes(jsonTypeInfo);
|
||||
if (derivedTypes.Length > 0)
|
||||
{
|
||||
ConfigureJsonPolymorphismOptions(jsonTypeInfo, typeof(ApiContent));
|
||||
}
|
||||
else if (jsonTypeInfo.Type == typeof(IApiContentResponse))
|
||||
{
|
||||
ConfigureJsonPolymorphismOptions(jsonTypeInfo, typeof(ApiContentResponse));
|
||||
}
|
||||
else if (jsonTypeInfo.Type == typeof(IRichTextElement))
|
||||
{
|
||||
ConfigureJsonPolymorphismOptions(jsonTypeInfo, typeof(RichTextRootElement), typeof(RichTextGenericElement), typeof(RichTextTextElement));
|
||||
ConfigureJsonPolymorphismOptions(jsonTypeInfo, derivedTypes);
|
||||
}
|
||||
|
||||
return jsonTypeInfo;
|
||||
}
|
||||
|
||||
private void ConfigureJsonPolymorphismOptions(JsonTypeInfo jsonTypeInfo, params Type[] derivedTypes)
|
||||
protected virtual Type[] GetDerivedTypes(JsonTypeInfo jsonTypeInfo)
|
||||
{
|
||||
if (jsonTypeInfo.Type == typeof(IApiContent))
|
||||
{
|
||||
return new[] { typeof(ApiContent) };
|
||||
}
|
||||
|
||||
if (jsonTypeInfo.Type == typeof(IApiContentResponse))
|
||||
{
|
||||
return new[] { typeof(ApiContentResponse) };
|
||||
}
|
||||
|
||||
if (jsonTypeInfo.Type == typeof(IRichTextElement))
|
||||
{
|
||||
return new[] { typeof(RichTextRootElement), typeof(RichTextGenericElement), typeof(RichTextTextElement) };
|
||||
}
|
||||
|
||||
return Array.Empty<Type>();
|
||||
}
|
||||
|
||||
protected void ConfigureJsonPolymorphismOptions(JsonTypeInfo jsonTypeInfo, params Type[] derivedTypes)
|
||||
{
|
||||
jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Umbraco.Cms.Api.Delivery.Indexing.Filters;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Querying.Filters;
|
||||
|
||||
@@ -15,15 +14,15 @@ public sealed class ContentTypeFilter : IFilterHandler
|
||||
/// <inheritdoc/>
|
||||
public FilterOption BuildFilterOption(string filter)
|
||||
{
|
||||
var alias = filter.Substring(ContentTypeSpecifier.Length);
|
||||
var filterValue = filter.Substring(ContentTypeSpecifier.Length);
|
||||
var negate = filterValue.StartsWith('!');
|
||||
var aliases = filterValue.TrimStart('!').Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
return new FilterOption
|
||||
{
|
||||
FieldName = ContentTypeFilterIndexer.FieldName,
|
||||
Values = alias.IsNullOrWhiteSpace() == false
|
||||
? new[] { alias.TrimStart('!') }
|
||||
: Array.Empty<string>(),
|
||||
Operator = alias.StartsWith('!')
|
||||
Values = aliases,
|
||||
Operator = negate
|
||||
? FilterOperation.IsNot
|
||||
: FilterOperation.Is
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Delivery.Indexing.Selectors;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Extensions;
|
||||
@@ -10,10 +12,21 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
|
||||
{
|
||||
private const string AncestorsSpecifier = "ancestors:";
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private readonly IRequestPreviewService _requestPreviewService;
|
||||
|
||||
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService)
|
||||
: base(publishedSnapshotAccessor, requestRoutingService) =>
|
||||
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
|
||||
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IRequestRoutingService requestRoutingService)
|
||||
: this(publishedSnapshotAccessor, requestRoutingService, StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>())
|
||||
{
|
||||
}
|
||||
|
||||
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService, IRequestPreviewService requestPreviewService)
|
||||
: base(publishedSnapshotAccessor, requestRoutingService)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_requestPreviewService = requestPreviewService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(string query)
|
||||
@@ -37,10 +50,20 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
|
||||
};
|
||||
}
|
||||
|
||||
IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot();
|
||||
IPublishedContentCache contentCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot()?.Content
|
||||
?? throw new InvalidOperationException("Could not obtain the content cache");
|
||||
|
||||
IPublishedContent contentItem = publishedSnapshot.Content?.GetById((Guid)id)
|
||||
?? throw new InvalidOperationException("Could not obtain the content cache");
|
||||
IPublishedContent? contentItem = contentCache.GetById(_requestPreviewService.IsPreview(), id.Value);
|
||||
|
||||
if (contentItem is null)
|
||||
{
|
||||
// no such content item, make sure the selector does not yield any results
|
||||
return new SelectorOption
|
||||
{
|
||||
FieldName = AncestorsSelectorIndexer.FieldName,
|
||||
Values = Array.Empty<string>()
|
||||
};
|
||||
}
|
||||
|
||||
var ancestorKeys = contentItem.Ancestors().Select(a => a.Key.ToString("D")).ToArray();
|
||||
|
||||
|
||||
@@ -1,62 +1,24 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Cms.Web.Common.Rendering;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Rendering;
|
||||
|
||||
internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansionStrategy
|
||||
internal sealed class RequestContextOutputExpansionStrategyV2 : ElementOnlyOutputExpansionStrategy, IOutputExpansionStrategy
|
||||
{
|
||||
private const string All = "$all";
|
||||
private const string None = "";
|
||||
private const string ExpandParameterName = "expand";
|
||||
private const string FieldsParameterName = "fields";
|
||||
|
||||
private readonly IApiPropertyRenderer _propertyRenderer;
|
||||
private readonly ILogger<RequestContextOutputExpansionStrategyV2> _logger;
|
||||
|
||||
private readonly Stack<Node?> _expandProperties;
|
||||
private readonly Stack<Node?> _includeProperties;
|
||||
|
||||
public RequestContextOutputExpansionStrategyV2(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IApiPropertyRenderer propertyRenderer,
|
||||
ILogger<RequestContextOutputExpansionStrategyV2> logger)
|
||||
: base(propertyRenderer)
|
||||
{
|
||||
_propertyRenderer = propertyRenderer;
|
||||
_logger = logger;
|
||||
_expandProperties = new Stack<Node?>();
|
||||
_includeProperties = new Stack<Node?>();
|
||||
|
||||
InitializeExpandAndInclude(httpContextAccessor);
|
||||
}
|
||||
|
||||
public IDictionary<string, object?> MapContentProperties(IPublishedContent content)
|
||||
=> content.ItemType == PublishedItemType.Content
|
||||
? MapProperties(content.Properties)
|
||||
: throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}");
|
||||
|
||||
public IDictionary<string, object?> MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true)
|
||||
{
|
||||
if (media.ItemType != PublishedItemType.Media)
|
||||
{
|
||||
throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}");
|
||||
}
|
||||
|
||||
IPublishedProperty[] properties = media
|
||||
.Properties
|
||||
.Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false)
|
||||
.ToArray();
|
||||
|
||||
return properties.Any()
|
||||
? MapProperties(properties)
|
||||
: new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
public IDictionary<string, object?> MapElementProperties(IPublishedElement element)
|
||||
=> MapProperties(element.Properties, true);
|
||||
|
||||
private void InitializeExpandAndInclude(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
string? QueryValue(string key) => httpContextAccessor.HttpContext?.Request.Query[key];
|
||||
@@ -66,7 +28,7 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion
|
||||
|
||||
try
|
||||
{
|
||||
_expandProperties.Push(Node.Parse(toExpand));
|
||||
ExpandProperties.Push(Node.Parse(toExpand));
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
@@ -76,7 +38,7 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion
|
||||
|
||||
try
|
||||
{
|
||||
_includeProperties.Push(Node.Parse(toInclude));
|
||||
IncludeProperties.Push(Node.Parse(toInclude));
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
@@ -84,102 +46,4 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion
|
||||
throw new ArgumentException($"Could not parse the '{FieldsParameterName}' parameter: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties, bool forceExpandProperties = false)
|
||||
{
|
||||
Node? currentExpandProperties = _expandProperties.Peek();
|
||||
if (_expandProperties.Count > 1 && currentExpandProperties is null && forceExpandProperties is false)
|
||||
{
|
||||
return new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
Node? currentIncludeProperties = _includeProperties.Peek();
|
||||
var result = new Dictionary<string, object?>();
|
||||
foreach (IPublishedProperty property in properties)
|
||||
{
|
||||
Node? nextIncludeProperties = GetNextProperties(currentIncludeProperties, property.Alias);
|
||||
if (currentIncludeProperties is not null && currentIncludeProperties.Items.Any() && nextIncludeProperties is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Node? nextExpandProperties = GetNextProperties(currentExpandProperties, property.Alias);
|
||||
|
||||
_includeProperties.Push(nextIncludeProperties);
|
||||
_expandProperties.Push(nextExpandProperties);
|
||||
|
||||
result[property.Alias] = GetPropertyValue(property);
|
||||
|
||||
_expandProperties.Pop();
|
||||
_includeProperties.Pop();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Node? GetNextProperties(Node? currentProperties, string propertyAlias)
|
||||
=> currentProperties?.Items.FirstOrDefault(i => i.Key == All)
|
||||
?? currentProperties?.Items.FirstOrDefault(i => i.Key == "properties")?.Items.FirstOrDefault(i => i.Key == All || i.Key == propertyAlias);
|
||||
|
||||
private object? GetPropertyValue(IPublishedProperty property)
|
||||
=> _propertyRenderer.GetPropertyValue(property, _expandProperties.Peek() is not null);
|
||||
|
||||
private class Node
|
||||
{
|
||||
public string Key { get; private set; } = string.Empty;
|
||||
|
||||
public List<Node> Items { get; } = new();
|
||||
|
||||
public static Node Parse(string value)
|
||||
{
|
||||
// verify that there are as many start brackets as there are end brackets
|
||||
if (value.CountOccurrences("[") != value.CountOccurrences("]"))
|
||||
{
|
||||
throw new ArgumentException("Value did not contain an equal number of start and end brackets");
|
||||
}
|
||||
|
||||
// verify that the value does not start with a start bracket
|
||||
if (value.StartsWith("["))
|
||||
{
|
||||
throw new ArgumentException("Value cannot start with a bracket");
|
||||
}
|
||||
|
||||
// verify that there are no empty brackets
|
||||
if (value.Contains("[]"))
|
||||
{
|
||||
throw new ArgumentException("Value cannot contain empty brackets");
|
||||
}
|
||||
|
||||
var stack = new Stack<Node>();
|
||||
var root = new Node { Key = "root" };
|
||||
stack.Push(root);
|
||||
|
||||
var currentNode = new Node();
|
||||
root.Items.Add(currentNode);
|
||||
|
||||
foreach (char c in value)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '[': // Start a new node, child of the current node
|
||||
stack.Push(currentNode);
|
||||
currentNode = new Node();
|
||||
stack.Peek().Items.Add(currentNode);
|
||||
break;
|
||||
case ',': // Start a new node, but at the same level of the current node
|
||||
currentNode = new Node();
|
||||
stack.Peek().Items.Add(currentNode);
|
||||
break;
|
||||
case ']': // Back to parent of the current node
|
||||
currentNode = stack.Pop();
|
||||
break;
|
||||
default: // Add char to current node key
|
||||
currentNode.Key += c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Abstractions;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Security;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
@@ -29,7 +29,7 @@ internal sealed class ApiAccessService : RequestHeaderHandler, IApiAccessService
|
||||
private bool IfEnabled(Func<bool> condition) => _deliveryApiSettings.Enabled && condition();
|
||||
|
||||
private bool HasValidApiKey() => _deliveryApiSettings.ApiKey.IsNullOrWhiteSpace() == false
|
||||
&& _deliveryApiSettings.ApiKey.Equals(GetHeaderValue("Api-Key"));
|
||||
&& _deliveryApiSettings.ApiKey.Equals(GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.ApiKey));
|
||||
|
||||
private bool IfMediaEnabled(Func<bool> condition) => _deliveryApiSettings is { Enabled: true, Media.Enabled: true } && condition();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using OpenIddict.Abstractions;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
// NOTE: this is public and unsealed to allow overriding the default claims with minimal effort.
|
||||
public class CurrentMemberClaimsProvider : ICurrentMemberClaimsProvider
|
||||
{
|
||||
private readonly IMemberManager _memberManager;
|
||||
|
||||
public CurrentMemberClaimsProvider(IMemberManager memberManager)
|
||||
=> _memberManager = memberManager;
|
||||
|
||||
public virtual async Task<Dictionary<string, object>> GetClaimsAsync()
|
||||
{
|
||||
MemberIdentityUser? memberIdentityUser = await _memberManager.GetCurrentMemberAsync();
|
||||
return memberIdentityUser is not null
|
||||
? await GetClaimsForMemberIdentityAsync(memberIdentityUser)
|
||||
: throw new InvalidOperationException("Could not retrieve the current member. This method should only ever be invoked when a member has been authorized.");
|
||||
}
|
||||
|
||||
protected virtual async Task<Dictionary<string, object>> GetClaimsForMemberIdentityAsync(MemberIdentityUser memberIdentityUser)
|
||||
{
|
||||
var claims = new Dictionary<string, object>
|
||||
{
|
||||
[OpenIddictConstants.Claims.Subject] = memberIdentityUser.Key
|
||||
};
|
||||
|
||||
if (memberIdentityUser.Name is not null)
|
||||
{
|
||||
claims[OpenIddictConstants.Claims.Name] = memberIdentityUser.Name;
|
||||
}
|
||||
|
||||
if (memberIdentityUser.Email is not null)
|
||||
{
|
||||
claims[OpenIddictConstants.Claims.Email] = memberIdentityUser.Email;
|
||||
}
|
||||
|
||||
claims[OpenIddictConstants.Claims.Role] = await _memberManager.GetRolesAsync(memberIdentityUser);
|
||||
|
||||
return claims;
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,5 @@ internal abstract class RequestHeaderHandler
|
||||
|
||||
protected RequestHeaderHandler(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor;
|
||||
|
||||
protected string? GetHeaderValue(string headerName)
|
||||
{
|
||||
HttpContext httpContext = _httpContextAccessor.HttpContext ??
|
||||
throw new InvalidOperationException("Could not obtain an HTTP context");
|
||||
|
||||
return httpContext.Request.Headers[headerName];
|
||||
}
|
||||
protected string? GetHeaderValue(string headerName) => _httpContextAccessor.HttpContext?.Request.Headers[headerName];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Security.Claims;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
@@ -11,5 +11,5 @@ internal sealed class RequestPreviewService : RequestHeaderHandler, IRequestPrev
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsPreview() => GetHeaderValue("Preview") == "true";
|
||||
public bool IsPreview() => string.Equals(GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.Preview), "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
@@ -42,31 +42,42 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
|
||||
{
|
||||
requestedPath = requestedPath.EnsureStartsWith("/");
|
||||
|
||||
IPublishedContent? startItem = GetStartItem();
|
||||
|
||||
// must append the root content url segment if it is not hidden by config, because
|
||||
// the URL tracking is based on the actual URL, including the root content url segment
|
||||
if (_globalSettings.HideTopLevelNodeFromPath == false)
|
||||
if (_globalSettings.HideTopLevelNodeFromPath == false && startItem?.UrlSegment != null)
|
||||
{
|
||||
IPublishedContent? startItem = GetStartItem();
|
||||
if (startItem?.UrlSegment != null)
|
||||
{
|
||||
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
|
||||
}
|
||||
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
|
||||
}
|
||||
|
||||
var culture = _requestCultureService.GetRequestedCulture();
|
||||
|
||||
// append the configured domain content ID to the path if we have a domain bound request,
|
||||
// because URL tracking registers the tracked url like "{domain content ID}/{content path}"
|
||||
Uri contentRoute = GetDefaultRequestUri(requestedPath);
|
||||
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
|
||||
if (domainAndUri != null)
|
||||
// important: redirect URLs are always tracked without trailing slashes
|
||||
requestedPath = requestedPath.TrimEnd("/");
|
||||
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
|
||||
|
||||
// if a redirect URL was not found, try by appending the start item ID because URL tracking might have tracked
|
||||
// a redirect with "{root content ID}/{content path}"
|
||||
if (redirectUrl is null && startItem is not null)
|
||||
{
|
||||
requestedPath = GetContentRoute(domainAndUri, contentRoute);
|
||||
culture ??= domainAndUri.Culture;
|
||||
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl($"{startItem.Id}{requestedPath}", culture);
|
||||
}
|
||||
|
||||
// still no redirect URL found - try looking for a configured domain if we have a domain bound request,
|
||||
// because URL tracking might have tracked a redirect with "{domain content ID}/{content path}"
|
||||
if (redirectUrl is null)
|
||||
{
|
||||
Uri contentRoute = GetDefaultRequestUri(requestedPath);
|
||||
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
|
||||
if (domainAndUri is not null)
|
||||
{
|
||||
requestedPath = GetContentRoute(domainAndUri, contentRoute);
|
||||
culture ??= domainAndUri.Culture;
|
||||
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
|
||||
}
|
||||
}
|
||||
|
||||
// important: redirect URLs are always tracked without trailing slashes
|
||||
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath.TrimEnd("/"), culture);
|
||||
IPublishedContent? content = redirectUrl != null
|
||||
? _apiPublishedContentCache.GetById(redirectUrl.ContentKey)
|
||||
: null;
|
||||
|
||||
@@ -58,5 +58,5 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? RequestedStartItem() => GetHeaderValue("Start-Item");
|
||||
public string? RequestedStartItem() => GetHeaderValue(Constants.DeliveryApi.HeaderNames.StartItem);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ internal abstract class RoutingServiceBase
|
||||
}
|
||||
|
||||
protected static string GetContentRoute(DomainAndUri domainAndUri, Uri contentRoute)
|
||||
=> $"{domainAndUri.ContentId}{DomainUtilities.PathRelativeToDomain(domainAndUri.Uri, contentRoute.AbsolutePath)}";
|
||||
=> $"{domainAndUri.ContentId}{DomainUtilities.PathRelativeToDomain(domainAndUri.Uri, contentRoute.LocalPath)}"; // Use LocalPath over AbsolutePath to keep the path decoded.
|
||||
|
||||
protected DomainAndUri? GetDomainAndUriForRoute(Uri contentUrl)
|
||||
{
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Tests.Integration</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>DynamicProxyGenAssembly2</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Management.Security;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Net;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Used to configure <see cref="CookieAuthenticationOptions" /> for the back office authentication type
|
||||
/// </summary>
|
||||
public class ConfigureBackOfficeCookieOptions : IConfigureNamedOptions<CookieAuthenticationOptions>
|
||||
{
|
||||
private readonly IDataProtectionProvider _dataProtection;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IIpResolver _ipResolver;
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
private readonly SecuritySettings _securitySettings;
|
||||
private readonly IUserService _userService;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigureBackOfficeCookieOptions" /> class.
|
||||
/// </summary>
|
||||
/// <param name="securitySettings">The <see cref="SecuritySettings" /> options</param>
|
||||
/// <param name="globalSettings">The <see cref="GlobalSettings" /> options</param>
|
||||
/// <param name="runtimeState">The <see cref="IRuntimeState" /></param>
|
||||
/// <param name="dataProtection">The <see cref="IDataProtectionProvider" /></param>
|
||||
/// <param name="userService">The <see cref="IUserService" /></param>
|
||||
/// <param name="ipResolver">The <see cref="IIpResolver" /></param>
|
||||
/// <param name="timeProvider">The <see cref="TimeProvider" /></param>
|
||||
public ConfigureBackOfficeCookieOptions(
|
||||
IOptions<SecuritySettings> securitySettings,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IRuntimeState runtimeState,
|
||||
IDataProtectionProvider dataProtection,
|
||||
IUserService userService,
|
||||
IIpResolver ipResolver,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_securitySettings = securitySettings.Value;
|
||||
_globalSettings = globalSettings.Value;
|
||||
_runtimeState = runtimeState;
|
||||
_dataProtection = dataProtection;
|
||||
_userService = userService;
|
||||
_ipResolver = ipResolver;
|
||||
_timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(string? name, CookieAuthenticationOptions options)
|
||||
{
|
||||
if (name != Constants.Security.BackOfficeAuthenticationType)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Configure(options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(CookieAuthenticationOptions options)
|
||||
{
|
||||
options.SlidingExpiration = false;
|
||||
options.ExpireTimeSpan = _globalSettings.TimeOut;
|
||||
options.Cookie.Domain = _securitySettings.AuthCookieDomain;
|
||||
options.Cookie.Name = _securitySettings.AuthCookieName;
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.Cookie.SecurePolicy =
|
||||
_globalSettings.UseHttps ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
|
||||
options.Cookie.Path = "/";
|
||||
|
||||
// NOTE: matches route in BackOfficeLoginController
|
||||
const string backOfficeLoginPath = "/umbraco/login";
|
||||
options.LoginPath = backOfficeLoginPath;
|
||||
options.LogoutPath = backOfficeLoginPath;
|
||||
options.AccessDeniedPath = backOfficeLoginPath;
|
||||
|
||||
options.DataProtectionProvider = _dataProtection;
|
||||
|
||||
// NOTE: This is borrowed directly from aspnetcore source
|
||||
// Note: the purpose for the data protector must remain fixed for interop to work.
|
||||
IDataProtector dataProtector = options.DataProtectionProvider.CreateProtector(
|
||||
"Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
|
||||
Constants.Security.BackOfficeAuthenticationType,
|
||||
"v2");
|
||||
var ticketDataFormat = new TicketDataFormat(dataProtector);
|
||||
|
||||
options.TicketDataFormat = new BackOfficeSecureDataFormat(_globalSettings.TimeOut, ticketDataFormat);
|
||||
|
||||
options.Events = new CookieAuthenticationEvents
|
||||
{
|
||||
// IMPORTANT! If you set any of OnRedirectToLogin, OnRedirectToAccessDenied, OnRedirectToLogout, OnRedirectToReturnUrl
|
||||
// you need to be aware that this will bypass the default behavior of returning the correct status codes for ajax requests and
|
||||
// not redirecting for non-ajax requests. This is because the default behavior is baked into this class here:
|
||||
// https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authentication/Cookies/src/CookieAuthenticationEvents.cs#L58
|
||||
// It would be possible to re-use the default behavior if any of these need to be set but that must be taken into account else
|
||||
// our back office requests will not function correctly. For now we don't need to set/configure any of these callbacks because
|
||||
// the defaults work fine with our setup.
|
||||
OnValidatePrincipal = async ctx =>
|
||||
{
|
||||
// We need to resolve the BackOfficeSecurityStampValidator per request as a requirement (even in aspnetcore they do this)
|
||||
BackOfficeSecurityStampValidator securityStampValidator =
|
||||
ctx.HttpContext.RequestServices.GetRequiredService<BackOfficeSecurityStampValidator>();
|
||||
|
||||
// Same goes for the signinmanager
|
||||
IBackOfficeSignInManager signInManager =
|
||||
ctx.HttpContext.RequestServices.GetRequiredService<IBackOfficeSignInManager>();
|
||||
|
||||
ClaimsIdentity? backOfficeIdentity = ctx.Principal?.GetUmbracoIdentity();
|
||||
if (backOfficeIdentity == null)
|
||||
{
|
||||
ctx.RejectPrincipal();
|
||||
await signInManager.SignOutAsync();
|
||||
}
|
||||
|
||||
// ensure the thread culture is set
|
||||
backOfficeIdentity?.EnsureCulture();
|
||||
|
||||
EnsureTicketRenewalIfKeepUserLoggedIn(ctx);
|
||||
|
||||
// add or update a claim to track when the cookie expires, we use this to track time remaining
|
||||
backOfficeIdentity?.AddOrUpdateClaim(new Claim(
|
||||
Constants.Security.TicketExpiresClaimType,
|
||||
ctx.Properties.ExpiresUtc!.Value.ToString("o"),
|
||||
ClaimValueTypes.DateTime,
|
||||
Constants.Security.BackOfficeAuthenticationType,
|
||||
Constants.Security.BackOfficeAuthenticationType,
|
||||
backOfficeIdentity));
|
||||
|
||||
await securityStampValidator.ValidateAsync(ctx);
|
||||
|
||||
// We have to manually specify Issued and Expires,
|
||||
// because the SecurityStampValidator refreshes the principal every 30 minutes,
|
||||
// When the principal is refreshed the Issued is update to time of refresh, however, the Expires remains unchanged
|
||||
// When we then try and renew, the difference of issued and expires effectively becomes the new ExpireTimeSpan
|
||||
// meaning we effectively lose 30 minutes of our ExpireTimeSpan for EVERY principal refresh if we don't
|
||||
// https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/Cookies/src/CookieAuthenticationHandler.cs#L115
|
||||
ctx.Properties.IssuedUtc = _timeProvider.GetUtcNow();
|
||||
ctx.Properties.ExpiresUtc = _timeProvider.GetUtcNow().Add(_globalSettings.TimeOut);
|
||||
ctx.ShouldRenew = true;
|
||||
},
|
||||
OnSigningIn = ctx =>
|
||||
{
|
||||
// occurs when sign in is successful but before the ticket is written to the outbound cookie
|
||||
ClaimsIdentity? backOfficeIdentity = ctx.Principal?.GetUmbracoIdentity();
|
||||
if (backOfficeIdentity != null)
|
||||
{
|
||||
// generate a session id and assign it
|
||||
// create a session token - if we are configured and not in an upgrade state then use the db, otherwise just generate one
|
||||
Guid session = _runtimeState.Level == RuntimeLevel.Run
|
||||
? _userService.CreateLoginSession(
|
||||
backOfficeIdentity.GetId()!.Value,
|
||||
_ipResolver.GetCurrentRequestIpAddress())
|
||||
: Guid.NewGuid();
|
||||
|
||||
// add our session claim
|
||||
backOfficeIdentity.AddClaim(new Claim(
|
||||
Constants.Security.SessionIdClaimType,
|
||||
session.ToString(),
|
||||
ClaimValueTypes.String,
|
||||
Constants.Security.BackOfficeAuthenticationType,
|
||||
Constants.Security.BackOfficeAuthenticationType,
|
||||
backOfficeIdentity));
|
||||
|
||||
// since it is a cookie-based authentication add that claim
|
||||
backOfficeIdentity.AddClaim(new Claim(
|
||||
ClaimTypes.CookiePath,
|
||||
"/",
|
||||
ClaimValueTypes.String,
|
||||
Constants.Security.BackOfficeAuthenticationType,
|
||||
Constants.Security.BackOfficeAuthenticationType,
|
||||
backOfficeIdentity));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnSignedIn = ctx =>
|
||||
{
|
||||
// occurs when sign in is successful and after the ticket is written to the outbound cookie
|
||||
|
||||
// When we are signed in with the cookie, assign the principal to the current HttpContext
|
||||
ctx.HttpContext.SetPrincipalForRequest(ctx.Principal);
|
||||
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnSigningOut = ctx =>
|
||||
{
|
||||
// Clear the user's session on sign out
|
||||
if (ctx.HttpContext?.User?.Identity != null)
|
||||
{
|
||||
var claimsIdentity = ctx.HttpContext.User.Identity as ClaimsIdentity;
|
||||
var sessionId = claimsIdentity?.FindFirstValue(Constants.Security.SessionIdClaimType);
|
||||
if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out Guid guidSession))
|
||||
{
|
||||
_userService.ClearLoginSession(guidSession);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all of our cookies
|
||||
var cookies = new[]
|
||||
{
|
||||
_securitySettings.AuthCookieName,
|
||||
Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalCookieName,
|
||||
Constants.Web.AngularCookieName, Constants.Web.CsrfValidationCookieName
|
||||
};
|
||||
foreach (var cookie in cookies)
|
||||
{
|
||||
ctx.Options.CookieManager.DeleteCookie(ctx.HttpContext!, cookie, new CookieOptions { Path = "/" });
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the ticket is renewed if the <see cref="SecuritySettings.KeepUserLoggedIn" /> is set to true
|
||||
/// and the current request is for the get user seconds endpoint
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="CookieValidatePrincipalContext" /></param>
|
||||
private void EnsureTicketRenewalIfKeepUserLoggedIn(CookieValidatePrincipalContext context)
|
||||
{
|
||||
if (!_securitySettings.KeepUserLoggedIn)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DateTimeOffset currentUtc = _timeProvider.GetUtcNow();
|
||||
DateTimeOffset? issuedUtc = context.Properties.IssuedUtc;
|
||||
DateTimeOffset? expiresUtc = context.Properties.ExpiresUtc;
|
||||
|
||||
if (expiresUtc.HasValue && issuedUtc.HasValue)
|
||||
{
|
||||
TimeSpan timeElapsed = currentUtc.Subtract(issuedUtc.Value);
|
||||
TimeSpan timeRemaining = expiresUtc.Value.Subtract(currentUtc);
|
||||
|
||||
// if it's time to renew, then do it
|
||||
if (timeRemaining < timeElapsed)
|
||||
{
|
||||
context.ShouldRenew = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Management.Security;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the back office security stamp options.
|
||||
/// </summary>
|
||||
public class ConfigureBackOfficeSecurityStampValidatorOptions : IConfigureOptions<BackOfficeSecurityStampValidatorOptions>
|
||||
{
|
||||
private readonly SecuritySettings _securitySettings;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public ConfigureBackOfficeSecurityStampValidatorOptions(IOptions<SecuritySettings> securitySettings, TimeProvider timeProvider)
|
||||
{
|
||||
_timeProvider = timeProvider;
|
||||
_securitySettings = securitySettings.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(BackOfficeSecurityStampValidatorOptions options)
|
||||
{
|
||||
options.TimeProvider = _timeProvider;
|
||||
ConfigureSecurityStampOptions.ConfigureOptions(options, _securitySettings);
|
||||
}
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.Serialization;
|
||||
using Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.OpenApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Configuration;
|
||||
|
||||
public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
private IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
|
||||
|
||||
public ConfigureUmbracoManagementApiSwaggerGenOptions(IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
|
||||
{
|
||||
_umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
|
||||
}
|
||||
|
||||
public void Configure(SwaggerGenOptions swaggerGenOptions)
|
||||
{
|
||||
|
||||
swaggerGenOptions.SwaggerDoc(
|
||||
ManagementApiConfiguration.ApiName,
|
||||
new OpenApiInfo
|
||||
{
|
||||
Title = ManagementApiConfiguration.ApiTitle,
|
||||
Version = "Latest",
|
||||
Description = "This shows all APIs available in this version of Umbraco - including all the legacy apis that are available for backward compatibility",
|
||||
});
|
||||
|
||||
swaggerGenOptions.OperationFilter<ResponseHeaderOperationFilter>();
|
||||
swaggerGenOptions.SelectSubTypesUsing(_umbracoJsonTypeInfoResolver.FindSubTypes);
|
||||
swaggerGenOptions.UseOneOfForPolymorphism();
|
||||
swaggerGenOptions.UseAllOfForInheritance();
|
||||
|
||||
// Ensure all types that implements the IOpenApiDiscriminator have a $type property in the OpenApi schema with the default value (The class name) that is expected by the server
|
||||
swaggerGenOptions.SelectDiscriminatorNameUsing(type => typeof(IOpenApiDiscriminator).IsAssignableFrom(type) ? "$type" : null);
|
||||
swaggerGenOptions.SelectDiscriminatorValueUsing(type => typeof(IOpenApiDiscriminator).IsAssignableFrom(type) ? type.Name : null);
|
||||
|
||||
|
||||
swaggerGenOptions.AddSecurityDefinition(
|
||||
ManagementApiConfiguration.ApiSecurityName,
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
In = ParameterLocation.Header,
|
||||
Name = "Umbraco",
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Description = "Umbraco Authentication",
|
||||
Flows = new OpenApiOAuthFlows
|
||||
{
|
||||
AuthorizationCode = new OpenApiOAuthFlow
|
||||
{
|
||||
AuthorizationUrl =
|
||||
new Uri(Common.Security.Paths.BackOfficeApi.AuthorizationEndpoint, UriKind.Relative),
|
||||
TokenUrl = new Uri(Common.Security.Paths.BackOfficeApi.TokenEndpoint, UriKind.Relative)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sets Security requirement on backoffice apis
|
||||
swaggerGenOptions.OperationFilter<BackOfficeSecurityRequirementsOperationFilter>();
|
||||
swaggerGenOptions.OperationFilter<NotificationHeaderFilter>();
|
||||
swaggerGenOptions.SchemaFilter<RequireNonNullablePropertiesSchemaFilter>();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.AuditLog;
|
||||
|
||||
[VersionedApiBackOfficeRoute("audit-log")]
|
||||
[ApiExplorerSettings(GroupName = "Audit Log")]
|
||||
public class AuditLogControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.AuditLogs;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.AuditLog;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.SectionAccessContentOrMedia)]
|
||||
public class ByKeyAuditLogController : AuditLogControllerBase
|
||||
{
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly IAuditLogPresentationFactory _auditLogPresentationFactory;
|
||||
|
||||
public ByKeyAuditLogController(IAuditService auditService, IAuditLogPresentationFactory auditLogPresentationFactory)
|
||||
{
|
||||
_auditService = auditService;
|
||||
_auditLogPresentationFactory = auditLogPresentationFactory;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<AuditLogResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> ByKey(Guid id, Direction orderDirection = Direction.Descending, DateTime? sinceDate = null, int skip = 0, int take = 100)
|
||||
{
|
||||
PagedModel<IAuditItem> result = await _auditService.GetItemsByKeyAsync(id, skip, take, orderDirection, sinceDate);
|
||||
IEnumerable<AuditLogResponseModel> mapped = _auditLogPresentationFactory.CreateAuditLogViewModel(result.Items);
|
||||
var viewModel = new PagedViewModel<AuditLogResponseModel>
|
||||
{
|
||||
Total = result.Total,
|
||||
Items = mapped,
|
||||
};
|
||||
|
||||
return Ok(viewModel);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.AuditLogs;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.AuditLog;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ByTypeAuditLogController : AuditLogControllerBase
|
||||
{
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly IAuditLogPresentationFactory _auditLogPresentationFactory;
|
||||
|
||||
public ByTypeAuditLogController(IAuditService auditService, IAuditLogPresentationFactory auditLogPresentationFactory)
|
||||
{
|
||||
_auditService = auditService;
|
||||
_auditLogPresentationFactory = auditLogPresentationFactory;
|
||||
}
|
||||
|
||||
[HttpGet("type/{logType}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<AuditLogResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> ByType(AuditType logType, DateTime? sinceDate = null, int skip = 0, int take = 100)
|
||||
{
|
||||
IAuditItem[] result = _auditService.GetLogs(logType, sinceDate).ToArray();
|
||||
IEnumerable<AuditLogResponseModel> mapped = _auditLogPresentationFactory.CreateAuditLogViewModel(result.Skip(skip).Take(take));
|
||||
var viewModel = new PagedViewModel<AuditLogResponseModel>
|
||||
{
|
||||
Total = result.Length,
|
||||
Items = mapped,
|
||||
};
|
||||
|
||||
return await Task.FromResult(Ok(viewModel));
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.AuditLogs;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Exceptions;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.AuditLog;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class CurrentUserAuditLogController : AuditLogControllerBase
|
||||
{
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly IAuditLogPresentationFactory _auditLogPresentationFactory;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public CurrentUserAuditLogController(
|
||||
IAuditService auditService,
|
||||
IAuditLogPresentationFactory auditLogPresentationFactory,
|
||||
IUserService userService)
|
||||
{
|
||||
_auditService = auditService;
|
||||
_auditLogPresentationFactory = auditLogPresentationFactory;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<AuditLogWithUsernameResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> CurrentUser(Direction orderDirection = Direction.Descending, DateTime? sinceDate = null, int skip = 0, int take = 100)
|
||||
{
|
||||
// FIXME: Pull out current backoffice user when its implemented.
|
||||
// var userId = _backOfficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1;
|
||||
var userId = Constants.Security.SuperUserId;
|
||||
|
||||
IUser? user = _userService.GetUserById(userId);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
throw new PanicException("Could not find current user");
|
||||
}
|
||||
|
||||
PagedModel<IAuditItem> result = await _auditService.GetPagedItemsByUserAsync(
|
||||
user.Key,
|
||||
skip,
|
||||
take,
|
||||
orderDirection,
|
||||
null,
|
||||
sinceDate);
|
||||
|
||||
IEnumerable<AuditLogWithUsernameResponseModel> mapped = _auditLogPresentationFactory.CreateAuditLogWithUsernameViewModels(result.Items);
|
||||
var viewModel = new PagedViewModel<AuditLogWithUsernameResponseModel>
|
||||
{
|
||||
Total = result.Total,
|
||||
Items = mapped,
|
||||
};
|
||||
|
||||
return Ok(viewModel);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management;
|
||||
|
||||
[BindProperties]
|
||||
public class
|
||||
BackOfficeLoginModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the "ReturnUrl" query parameter or defaults to the configured Umbraco directory.
|
||||
/// </summary>
|
||||
[FromQuery(Name = "ReturnUrl")]
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The configured Umbraco directory.
|
||||
/// </summary>
|
||||
public string? UmbracoUrl { get; set; }
|
||||
}
|
||||
|
||||
[ApiExplorerSettings(IgnoreApi=true)]
|
||||
[Route("/umbraco/login")]
|
||||
public class BackOfficeLoginController : Controller
|
||||
{
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public BackOfficeLoginController(
|
||||
IOptionsSnapshot<GlobalSettings> globalSettings,
|
||||
IHostingEnvironment hostingEnvironment)
|
||||
{
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings));
|
||||
}
|
||||
|
||||
// GET
|
||||
public IActionResult Index(BackOfficeLoginModel model)
|
||||
{
|
||||
if (string.IsNullOrEmpty(model.UmbracoUrl))
|
||||
{
|
||||
model.UmbracoUrl = _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoPath);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model.ReturnUrl))
|
||||
{
|
||||
model.ReturnUrl = model.UmbracoUrl;
|
||||
}
|
||||
|
||||
return View("/umbraco/UmbracoLogin/Index.cshtml", model);
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Content;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Content;
|
||||
|
||||
public abstract class ContentCollectionControllerBase<TContent, TCollectionResponseModel, TValueResponseModelBase, TVariantResponseModel> : ManagementApiControllerBase
|
||||
where TContent : class, IContentBase
|
||||
where TCollectionResponseModel : ContentResponseModelBase<TValueResponseModelBase, TVariantResponseModel>
|
||||
where TValueResponseModelBase : ValueModelBase
|
||||
where TVariantResponseModel : VariantResponseModelBase
|
||||
{
|
||||
private readonly IUmbracoMapper _mapper;
|
||||
|
||||
protected ContentCollectionControllerBase(IUmbracoMapper mapper) => _mapper = mapper;
|
||||
|
||||
protected IActionResult CollectionResult(ListViewPagedModel<TContent> result)
|
||||
{
|
||||
PagedModel<TContent> collectionItemsResult = result.Items;
|
||||
ListViewConfiguration collectionConfiguration = result.ListViewConfiguration;
|
||||
|
||||
var collectionPropertyAliases = collectionConfiguration
|
||||
.IncludeProperties
|
||||
.Select(p => p.Alias)
|
||||
.WhereNotNull()
|
||||
.ToArray();
|
||||
|
||||
List<TCollectionResponseModel> collectionResponseModels =
|
||||
_mapper.MapEnumerable<TContent, TCollectionResponseModel>(collectionItemsResult.Items, context =>
|
||||
{
|
||||
context.SetIncludedProperties(collectionPropertyAliases);
|
||||
});
|
||||
|
||||
var pageViewModel = new PagedViewModel<TCollectionResponseModel>
|
||||
{
|
||||
Items = collectionResponseModels,
|
||||
Total = collectionItemsResult.Total,
|
||||
};
|
||||
|
||||
return Ok(pageViewModel);
|
||||
}
|
||||
|
||||
protected IActionResult ContentCollectionOperationStatusResult(ContentCollectionOperationStatus status, string type) =>
|
||||
OperationStatusResult(status, problemDetailsBuilder => status switch
|
||||
{
|
||||
ContentCollectionOperationStatus.CollectionNotFound => new NotFoundObjectResult(problemDetailsBuilder
|
||||
.WithTitle("Collection data type could not be found")
|
||||
.WithDetail($"No collection data type was found for the corresponding {type} type. Ensure that the default and/or a custom collection data types exists")
|
||||
.Build()),
|
||||
ContentCollectionOperationStatus.ContentNotCollection => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle($"The {type} item is not configured as a collection")
|
||||
.WithDetail($"The specified {type} is not configured as a collection")
|
||||
.Build()),
|
||||
ContentCollectionOperationStatus.ContentNotFound => new NotFoundObjectResult(problemDetailsBuilder
|
||||
.WithTitle($"The specified {type} could not be found")
|
||||
.Build()),
|
||||
ContentCollectionOperationStatus.ContentTypeNotFound => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle($"The related {type} type could not be found")
|
||||
.Build()),
|
||||
ContentCollectionOperationStatus.DataTypeNotCollection => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle("Data type id does not belong to a collection")
|
||||
.WithDetail("The specified data type does not represent a collection")
|
||||
.Build()),
|
||||
ContentCollectionOperationStatus.DataTypeNotContentCollection => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle("Data type id does not represent the configured collection")
|
||||
.WithDetail($"The specified data type is not the configured collection for the given {type} item")
|
||||
.Build()),
|
||||
ContentCollectionOperationStatus.DataTypeNotContentProperty => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle($"Data type id is not a {type} property")
|
||||
.WithDetail($"The specified data type is not part of the {type} properties")
|
||||
.Build()),
|
||||
ContentCollectionOperationStatus.DataTypeNotFound => new NotFoundObjectResult(problemDetailsBuilder
|
||||
.WithTitle("The specified collection data type could not be found")
|
||||
.Build()),
|
||||
ContentCollectionOperationStatus.DataTypeWithoutContentType => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle($"Missing {type} when specifying a collection data type")
|
||||
.WithDetail($"The specified collection data type needs to be used in conjunction with a {type} item.")
|
||||
.Build()),
|
||||
ContentCollectionOperationStatus.MissingPropertiesInCollectionConfiguration => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle("Missing properties in collection configuration")
|
||||
.WithDetail("No properties are configured to display in the collection configuration")
|
||||
.Build()),
|
||||
ContentCollectionOperationStatus.OrderByNotPartOfCollectionConfiguration => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle("Order by value is not a property on the configured collection")
|
||||
.WithDetail("The specified orderBy property is not part of the collection configuration")
|
||||
.Build()),
|
||||
_ => new ObjectResult("Unknown content collection operation status")
|
||||
{
|
||||
StatusCode = StatusCodes.Status500InternalServerError,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing.Validation;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Content;
|
||||
|
||||
public class ContentControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
protected IActionResult ContentEditingOperationStatusResult(ContentEditingOperationStatus status)
|
||||
=> OperationStatusResult(status, problemDetailsBuilder => status switch
|
||||
{
|
||||
ContentEditingOperationStatus.CancelledByNotification => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Cancelled by notification")
|
||||
.WithDetail("A notification handler prevented the content operation.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.ContentTypeNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("The requested content could not be found")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.ContentTypeCultureVarianceMismatch => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Content type culture variance mismatch")
|
||||
.WithDetail("The content type variance did not match that of the passed content data.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.NotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("The content could not be found")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.ParentNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("The targeted content parent could not be found")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.ParentInvalid => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid parent")
|
||||
.WithDetail("The targeted parent was not valid for the operation.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.NotAllowed => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Operation not permitted")
|
||||
.WithDetail(
|
||||
"The attempted operation was not permitted, likely due to a permission/configuration mismatch with the operation.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.TemplateNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("The template could not be found")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.TemplateNotAllowed => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Template not allowed")
|
||||
.WithDetail("The selected template was not allowed for the operation.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.PropertyTypeNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("One or more property types could not be found")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.InTrash => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Content is in the recycle bin")
|
||||
.WithDetail("Could not perform the operation because the targeted content was in the recycle bin.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.NotInTrash => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Content is not in the recycle bin")
|
||||
.WithDetail("The attempted operation requires the targeted content to be in the recycle bin.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.SortingInvalid => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid sorting options")
|
||||
.WithDetail("The supplied sorting operations were invalid. Additional details can be found in the log.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.InvalidCulture => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid culture")
|
||||
.WithDetail("One or more of the supplied culture codes did not match the configured languages.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.DuplicateKey => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid Id")
|
||||
.WithDetail("The supplied id is already in use.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.Unknown => StatusCode(
|
||||
StatusCodes.Status500InternalServerError,
|
||||
problemDetailsBuilder
|
||||
.WithTitle("Unknown error. Please see the log for more details.")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
|
||||
.WithTitle("Unknown content operation status.")
|
||||
.Build()),
|
||||
});
|
||||
|
||||
protected IActionResult ContentEditingOperationStatusResult<TContentModelBase, TValueModel, TVariantModel>(
|
||||
ContentEditingOperationStatus status,
|
||||
TContentModelBase requestModel,
|
||||
ContentValidationResult validationResult)
|
||||
where TContentModelBase : ContentModelBase<TValueModel, TVariantModel>
|
||||
where TValueModel : ValueModelBase
|
||||
where TVariantModel : VariantModelBase
|
||||
{
|
||||
if (status is not ContentEditingOperationStatus.PropertyValidationError)
|
||||
{
|
||||
return ContentEditingOperationStatusResult(status);
|
||||
}
|
||||
|
||||
var errors = new SortedDictionary<string, string[]>();
|
||||
var missingPropertyAliases = new List<string>();
|
||||
foreach (PropertyValidationError validationError in validationResult.ValidationErrors)
|
||||
{
|
||||
TValueModel? requestValue = requestModel.Values.FirstOrDefault(value =>
|
||||
value.Alias == validationError.Alias
|
||||
&& value.Culture == validationError.Culture
|
||||
&& value.Segment == validationError.Segment);
|
||||
if (requestValue is null)
|
||||
{
|
||||
missingPropertyAliases.Add(validationError.Alias);
|
||||
continue;
|
||||
}
|
||||
|
||||
var index = requestModel.Values.IndexOf(requestValue);
|
||||
var key = $"$.{nameof(ContentModelBase<TValueModel, TVariantModel>.Values).ToFirstLowerInvariant()}[{index}].{nameof(ValueModelBase.Value).ToFirstLowerInvariant()}{validationError.JsonPath}";
|
||||
errors.Add(key, validationError.ErrorMessages);
|
||||
}
|
||||
|
||||
return OperationStatusResult(status, problemDetailsBuilder
|
||||
=> BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Validation failed")
|
||||
.WithDetail("One or more properties did not pass validation")
|
||||
.WithRequestModelErrors(errors)
|
||||
.WithExtension("missingProperties", missingPropertyAliases.ToArray())
|
||||
.Build()));
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using System.Globalization;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Culture;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Culture;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class AllCultureController : CultureControllerBase
|
||||
{
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
|
||||
public AllCultureController(IUmbracoMapper umbracoMapper) => _umbracoMapper = umbracoMapper;
|
||||
|
||||
/// <summary>
|
||||
/// Returns all cultures available for creating languages.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<CultureReponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<PagedViewModel<CultureReponseModel>> GetAll(int skip = 0, int take = 100)
|
||||
{
|
||||
CultureInfo[] all = CultureInfo.GetCultures(CultureTypes.AllCultures)
|
||||
.DistinctBy(x => x.Name)
|
||||
.OrderBy(x => x.EnglishName)
|
||||
.ToArray();
|
||||
|
||||
var viewModel = new PagedViewModel<CultureReponseModel>
|
||||
{
|
||||
Items = _umbracoMapper.MapEnumerable<CultureInfo, CultureReponseModel>(all.Skip(skip).Take(take)),
|
||||
Total = all.Length
|
||||
};
|
||||
|
||||
return await Task.FromResult(viewModel);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Culture;
|
||||
|
||||
[VersionedApiBackOfficeRoute("culture")]
|
||||
[ApiExplorerSettings(GroupName = "Culture")]
|
||||
public abstract class CultureControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ByKeyDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
|
||||
public ByKeyDataTypeController(IDataTypeService dataTypeService, IUmbracoMapper umbracoMapper)
|
||||
{
|
||||
_dataTypeService = dataTypeService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(DataTypeResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByKey(Guid id)
|
||||
{
|
||||
IDataType? dataType = await _dataTypeService.GetAsync(id);
|
||||
if (dataType == null)
|
||||
{
|
||||
return DataTypeNotFound();
|
||||
}
|
||||
|
||||
return Ok(_umbracoMapper.Map<DataTypeResponseModel>(dataType));
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ConfigurationDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly DataTypesSettings _dataTypesSettings;
|
||||
|
||||
public ConfigurationDataTypeController(IOptionsSnapshot<DataTypesSettings> dataTypesSettings) => _dataTypesSettings = dataTypesSettings.Value;
|
||||
|
||||
[HttpGet("configuration")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(DatatypeConfigurationResponseModel), StatusCodes.Status200OK)]
|
||||
public Task<IActionResult> Configuration()
|
||||
{
|
||||
var responseModel = new DatatypeConfigurationResponseModel
|
||||
{
|
||||
CanBeChanged = _dataTypesSettings.CanBeChanged,
|
||||
DocumentListViewId = Constants.DataTypes.Guids.ListViewContentGuid,
|
||||
MediaListViewId = Constants.DataTypes.Guids.ListViewMediaGuid,
|
||||
MemberListViewId = Constants.DataTypes.Guids.ListViewMembersGuid,
|
||||
};
|
||||
return Task.FromResult<IActionResult>(Ok(responseModel));
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class CopyDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public CopyDataTypeController(IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_dataTypeService = dataTypeService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/copy")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Copy(Guid id, CopyDataTypeRequestModel copyDataTypeRequestModel)
|
||||
{
|
||||
IDataType? source = await _dataTypeService.GetAsync(id);
|
||||
if (source is null)
|
||||
{
|
||||
return DataTypeNotFound();
|
||||
}
|
||||
|
||||
Attempt<IDataType, DataTypeOperationStatus> result = await _dataTypeService.CopyAsync(source, copyDataTypeRequestModel.Target?.Id, CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Success
|
||||
? CreatedAtId<ByKeyDataTypeController>(controller => nameof(controller.ByKey), result.Result.Key)
|
||||
: DataTypeOperationStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class CreateDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IDataTypePresentationFactory _dataTypePresentationFactory;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public CreateDataTypeController(IDataTypeService dataTypeService, IDataTypePresentationFactory dataTypePresentationFactory, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_dataTypeService = dataTypeService;
|
||||
_dataTypePresentationFactory = dataTypePresentationFactory;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Create(CreateDataTypeRequestModel createDataTypeRequestModel)
|
||||
{
|
||||
var attempt = await _dataTypePresentationFactory.CreateAsync(createDataTypeRequestModel);
|
||||
if (!attempt.Success)
|
||||
{
|
||||
return DataTypeOperationStatusResult(attempt.Status);
|
||||
}
|
||||
|
||||
Attempt<IDataType, DataTypeOperationStatus> result = await _dataTypeService.CreateAsync(attempt.Result, CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Success
|
||||
? CreatedAtId<ByKeyDataTypeController>(controller => nameof(controller.ByKey), result.Result.Key)
|
||||
: DataTypeOperationStatusResult(result.Status);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[VersionedApiBackOfficeRoute(Constants.UdiEntityType.DataType)]
|
||||
[ApiExplorerSettings(GroupName = "Data Type")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes)]
|
||||
public abstract class DataTypeControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
protected IActionResult DataTypeOperationStatusResult(DataTypeOperationStatus status) =>
|
||||
OperationStatusResult(status, problemDetailsBuilder => status switch
|
||||
{
|
||||
DataTypeOperationStatus.InvalidConfiguration => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid data type configuration")
|
||||
.WithDetail("The supplied data type configuration was not valid. Please see the log for more details.")
|
||||
.Build()),
|
||||
DataTypeOperationStatus.NotFound => DataTypeNotFound(problemDetailsBuilder),
|
||||
DataTypeOperationStatus.InvalidName => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid data type name")
|
||||
.WithDetail("The data type name must be non-empty and no longer than 255 characters.")
|
||||
.Build()),
|
||||
DataTypeOperationStatus.ParentNotContainer => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("parent id is not a container")
|
||||
.WithDetail("The parent id does not represent a container.")
|
||||
.Build()),
|
||||
DataTypeOperationStatus.DuplicateKey => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("The id is already used")
|
||||
.WithDetail("The data type id must be unique.")
|
||||
.Build()),
|
||||
DataTypeOperationStatus.CancelledByNotification => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Cancelled by notification")
|
||||
.WithDetail("A notification handler prevented the data type operation.")
|
||||
.Build()),
|
||||
DataTypeOperationStatus.PropertyEditorNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("The targeted property editor was not found.")
|
||||
.Build()),
|
||||
DataTypeOperationStatus.ParentNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("The targeted parent for the data type operation was not found.")
|
||||
.Build()),
|
||||
DataTypeOperationStatus.NonDeletable => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("The data type is non-deletable")
|
||||
.WithDetail("The specified data type is required by the system and cannot be deleted.")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
|
||||
.WithTitle("Unknown data type operation status.")
|
||||
.Build()),
|
||||
});
|
||||
|
||||
protected IActionResult DataTypeNotFound() => OperationStatusResult(DataTypeOperationStatus.NotFound, DataTypeNotFound);
|
||||
|
||||
private IActionResult DataTypeNotFound(ProblemDetailsBuilder problemDetailsBuilder)
|
||||
=> NotFound(problemDetailsBuilder
|
||||
.WithTitle("The data type could not be found")
|
||||
.Build());
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class DeleteDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public DeleteDataTypeController(IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_dataTypeService = dataTypeService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
Attempt<IDataType?, DataTypeOperationStatus> result = await _dataTypeService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
: DataTypeOperationStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Filter;
|
||||
|
||||
[ApiExplorerSettings(GroupName = "Data Type")]
|
||||
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Filter}/{Constants.UdiEntityType.DataType}")]
|
||||
// This auth policy might become problematic, as when getting DataTypes on Media types, you don't need access to the document tree.
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes)]
|
||||
public abstract class DataTypeFilterControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType.Item;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Language;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Filter;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class FilterDataTypeFilterController : DataTypeFilterControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IUmbracoMapper _mapper;
|
||||
|
||||
public FilterDataTypeFilterController(IDataTypeService dataTypeService, IUmbracoMapper mapper)
|
||||
{
|
||||
_dataTypeService = dataTypeService;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<DataTypeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> Filter(
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
string name = "",
|
||||
string? editorUiAlias = null,
|
||||
string? editorAlias = null)
|
||||
{
|
||||
PagedModel<IDataType> dataTypes = await _dataTypeService.FilterAsync(name, editorUiAlias, editorAlias, skip, take);
|
||||
List<DataTypeItemResponseModel> responseModels = _mapper.MapEnumerable<IDataType, DataTypeItemResponseModel>(dataTypes.Items);
|
||||
var viewModel = new PagedViewModel<DataTypeItemResponseModel>
|
||||
{
|
||||
Total = dataTypes.Total,
|
||||
Items = responseModels,
|
||||
};
|
||||
return Ok(viewModel);
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Folder;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ByKeyDataTypeFolderController : DataTypeFolderControllerBase
|
||||
{
|
||||
public ByKeyDataTypeFolderController(
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IDataTypeContainerService dataTypeContainerService)
|
||||
: base(backOfficeSecurityAccessor, dataTypeContainerService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(FolderResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByKey(Guid id) => await GetFolderAsync(id);
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Folder;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class CreateDataTypeFolderController : DataTypeFolderControllerBase
|
||||
{
|
||||
public CreateDataTypeFolderController(
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IDataTypeContainerService dataTypeContainerService)
|
||||
: base(backOfficeSecurityAccessor, dataTypeContainerService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Create(CreateFolderRequestModel createFolderRequestModel)
|
||||
=> await CreateFolderAsync<ByKeyDataTypeFolderController>(
|
||||
createFolderRequestModel,
|
||||
controller => nameof(controller.ByKey));
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
|
||||
|
||||
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DataType}/folder")]
|
||||
[ApiExplorerSettings(GroupName = "Data Type")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
public abstract class DataTypeFolderControllerBase : FolderManagementControllerBase<IDataType>
|
||||
{
|
||||
protected DataTypeFolderControllerBase(
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IDataTypeContainerService dataTypeContainerService)
|
||||
: base(backOfficeSecurityAccessor, dataTypeContainerService)
|
||||
{
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class DeleteDataTypeFolderController : DataTypeFolderControllerBase
|
||||
{
|
||||
public DeleteDataTypeFolderController(
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IDataTypeContainerService dataTypeContainerService)
|
||||
: base(backOfficeSecurityAccessor, dataTypeContainerService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(Guid id) => await DeleteFolderAsync(id);
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Folder;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class UpdateDataTypeFolderController : DataTypeFolderControllerBase
|
||||
{
|
||||
public UpdateDataTypeFolderController(
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IDataTypeContainerService dataTypeContainerService)
|
||||
: base(backOfficeSecurityAccessor, dataTypeContainerService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Update(Guid id, UpdateFolderResponseModel updateFolderResponseModel)
|
||||
=> await UpdateFolderAsync(id, updateFolderResponseModel);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class IsUsedDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeUsageService _dataTypeUsageService;
|
||||
|
||||
public IsUsedDataTypeController(IDataTypeUsageService dataTypeUsageService)
|
||||
{
|
||||
_dataTypeUsageService = dataTypeUsageService;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/is-used")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> IsUsed(Guid id)
|
||||
{
|
||||
Attempt<bool, DataTypeOperationStatus> result = await _dataTypeUsageService.HasSavedValuesAsync(id);
|
||||
|
||||
return result.Success
|
||||
? Ok(result.Result)
|
||||
: DataTypeOperationStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Item;
|
||||
|
||||
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.DataType}")]
|
||||
[ApiExplorerSettings(GroupName = "Data Type")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes)]
|
||||
public class DatatypeItemControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType.Item;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Item;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ItemDatatypeItemController : DatatypeItemControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IUmbracoMapper _mapper;
|
||||
|
||||
public ItemDatatypeItemController(IDataTypeService dataTypeService, IUmbracoMapper mapper)
|
||||
{
|
||||
_dataTypeService = dataTypeService;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<DataTypeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult> Item([FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
{
|
||||
var dataTypes = new List<IDataType>();
|
||||
foreach (Guid id in ids)
|
||||
{
|
||||
IDataType? dataType = await _dataTypeService.GetAsync(id);
|
||||
if (dataType is not null)
|
||||
{
|
||||
dataTypes.Add(dataType);
|
||||
}
|
||||
}
|
||||
|
||||
List<DataTypeItemResponseModel> responseModels = _mapper.MapEnumerable<IDataType, DataTypeItemResponseModel>(dataTypes);
|
||||
return Ok(responseModels);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class MoveDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public MoveDataTypeController(IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_dataTypeService = dataTypeService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/move")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Move(Guid id, MoveDataTypeRequestModel moveDataTypeRequestModel)
|
||||
{
|
||||
IDataType? source = await _dataTypeService.GetAsync(id);
|
||||
if (source is null)
|
||||
{
|
||||
return DataTypeNotFound();
|
||||
}
|
||||
|
||||
Attempt<IDataType, DataTypeOperationStatus> result = await _dataTypeService.MoveAsync(source, moveDataTypeRequestModel.Target?.Id, CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
: DataTypeOperationStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ReferencesDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IDataTypeReferencePresentationFactory _dataTypeReferencePresentationFactory;
|
||||
|
||||
public ReferencesDataTypeController(IDataTypeService dataTypeService, IDataTypeReferencePresentationFactory dataTypeReferencePresentationFactory)
|
||||
{
|
||||
_dataTypeService = dataTypeService;
|
||||
_dataTypeReferencePresentationFactory = dataTypeReferencePresentationFactory;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/references")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(DataTypeReferenceResponseModel[]), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> References(Guid id)
|
||||
{
|
||||
Attempt<IReadOnlyDictionary<Udi, IEnumerable<string>>, DataTypeOperationStatus> result = await _dataTypeService.GetReferencesAsync(id);
|
||||
if (result.Success == false)
|
||||
{
|
||||
return DataTypeOperationStatusResult(result.Status);
|
||||
}
|
||||
|
||||
DataTypeReferenceResponseModel[] viewModels = _dataTypeReferencePresentationFactory.CreateDataTypeReferenceViewModels(result.Result).ToArray();
|
||||
return Ok(viewModels);
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ChildrenDataTypeTreeController : DataTypeTreeControllerBase
|
||||
{
|
||||
public ChildrenDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
|
||||
: base(entityService, dataTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("children")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<DataTypeTreeItemResponseModel>>> Children(Guid parentId, int skip = 0, int take = 100, bool foldersOnly = false)
|
||||
{
|
||||
RenderFoldersOnly(foldersOnly);
|
||||
return await GetChildren(parentId, skip, take);
|
||||
}
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Controllers.Tree;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
|
||||
|
||||
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Tree}/{Constants.UdiEntityType.DataType}")]
|
||||
[ApiExplorerSettings(GroupName = "Data Type")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
|
||||
public class DataTypeTreeControllerBase : FolderTreeControllerBase<DataTypeTreeItemResponseModel>
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
public DataTypeTreeControllerBase(IEntityService entityService, IDataTypeService dataTypeService)
|
||||
: base(entityService) =>
|
||||
_dataTypeService = dataTypeService;
|
||||
|
||||
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DataType;
|
||||
|
||||
protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.DataTypeContainer;
|
||||
|
||||
protected override DataTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentId, IEntitySlim[] entities)
|
||||
{
|
||||
var dataTypes = _dataTypeService
|
||||
.GetAllAsync(entities.Select(entity => entity.Key).ToArray()).GetAwaiter().GetResult()
|
||||
.ToDictionary(contentType => contentType.Id);
|
||||
|
||||
return entities.Select(entity =>
|
||||
{
|
||||
DataTypeTreeItemResponseModel responseModel = MapTreeItemViewModel(parentId, entity);
|
||||
if (dataTypes.TryGetValue(entity.Id, out IDataType? dataType))
|
||||
{
|
||||
responseModel.EditorUiAlias = dataType.EditorUiAlias;
|
||||
responseModel.IsDeletable = dataType.IsDeletableDataType();
|
||||
}
|
||||
|
||||
return responseModel;
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType.Item;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class RootDataTypeTreeController : DataTypeTreeControllerBase
|
||||
{
|
||||
public RootDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
|
||||
: base(entityService, dataTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("root")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<DataTypeTreeItemResponseModel>>> Root(int skip = 0, int take = 100, bool foldersOnly = false)
|
||||
{
|
||||
RenderFoldersOnly(foldersOnly);
|
||||
return await GetRoot(skip, take);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class UpdateDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private IDataTypePresentationFactory _dataTypePresentationFactory;
|
||||
|
||||
public UpdateDataTypeController(IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IDataTypePresentationFactory dataTypePresentationFactory)
|
||||
{
|
||||
_dataTypeService = dataTypeService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_dataTypePresentationFactory = dataTypePresentationFactory;
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Update(Guid id, UpdateDataTypeRequestModel updateDataTypeViewModel)
|
||||
{
|
||||
IDataType? current = await _dataTypeService.GetAsync(id);
|
||||
if (current == null)
|
||||
{
|
||||
return DataTypeNotFound();
|
||||
}
|
||||
|
||||
Attempt<IDataType, DataTypeOperationStatus> attempt = await _dataTypePresentationFactory.CreateAsync(updateDataTypeViewModel, current);
|
||||
if (!attempt.Success)
|
||||
{
|
||||
return DataTypeOperationStatusResult(attempt.Status);
|
||||
}
|
||||
|
||||
Attempt<IDataType, DataTypeOperationStatus> result = await _dataTypeService.UpdateAsync(attempt.Result, CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
return result.Success
|
||||
? Ok()
|
||||
: DataTypeOperationStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Dictionary;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class AllDictionaryController : DictionaryControllerBase
|
||||
{
|
||||
private readonly IDictionaryItemService _dictionaryItemService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
|
||||
public AllDictionaryController(IDictionaryItemService dictionaryItemService, IUmbracoMapper umbracoMapper)
|
||||
{
|
||||
_dictionaryItemService = dictionaryItemService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<DictionaryOverviewResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<DictionaryOverviewResponseModel>>> All(string? filter = null, int skip = 0, int take = 100)
|
||||
{
|
||||
// unfortunately we can't paginate here...we'll have to get all and paginate in memory
|
||||
IDictionaryItem[] items = (await _dictionaryItemService.GetDescendantsAsync(Constants.System.RootKey, filter)).ToArray();
|
||||
var model = new PagedViewModel<DictionaryOverviewResponseModel>
|
||||
{
|
||||
Total = items.Length,
|
||||
Items = _umbracoMapper.MapEnumerable<IDictionaryItem, DictionaryOverviewResponseModel>(items.Skip(skip).Take(take))
|
||||
};
|
||||
return await Task.FromResult(model);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Dictionary;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ByKeyDictionaryController : DictionaryControllerBase
|
||||
{
|
||||
private readonly IDictionaryItemService _dictionaryItemService;
|
||||
private readonly IDictionaryPresentationFactory _dictionaryPresentationFactory;
|
||||
|
||||
public ByKeyDictionaryController(IDictionaryItemService dictionaryItemService, IDictionaryPresentationFactory dictionaryPresentationFactory)
|
||||
{
|
||||
_dictionaryItemService = dictionaryItemService;
|
||||
_dictionaryPresentationFactory = dictionaryPresentationFactory;
|
||||
}
|
||||
|
||||
[HttpGet($"{{{nameof(id)}:guid}}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(DictionaryItemResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByKey(Guid id)
|
||||
{
|
||||
IDictionaryItem? dictionary = await _dictionaryItemService.GetAsync(id);
|
||||
if (dictionary == null)
|
||||
{
|
||||
return DictionaryItemNotFound();
|
||||
}
|
||||
|
||||
return Ok(await _dictionaryPresentationFactory.CreateDictionaryItemViewModelAsync(dictionary));
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Dictionary;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class CreateDictionaryController : DictionaryControllerBase
|
||||
{
|
||||
private readonly IDictionaryItemService _dictionaryItemService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly IDictionaryPresentationFactory _dictionaryPresentationFactory;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public CreateDictionaryController(
|
||||
IDictionaryItemService dictionaryItemService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IDictionaryPresentationFactory dictionaryPresentationFactory,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_dictionaryItemService = dictionaryItemService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_dictionaryPresentationFactory = dictionaryPresentationFactory;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Create(CreateDictionaryItemRequestModel createDictionaryItemRequestModel)
|
||||
{
|
||||
IDictionaryItem created = await _dictionaryPresentationFactory.MapCreateModelToDictionaryItemAsync(createDictionaryItemRequestModel);
|
||||
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
new DictionaryPermissionResource(createDictionaryItemRequestModel.Translations.Select(t => t.IsoCode)),
|
||||
AuthorizationPolicies.DictionaryPermissionByResource);
|
||||
|
||||
if (authorizationResult.Succeeded is false)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
Attempt<IDictionaryItem, DictionaryItemOperationStatus> result =
|
||||
await _dictionaryItemService.CreateAsync(created, CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Success
|
||||
? CreatedAtId<ByKeyDictionaryController>(controller => nameof(controller.ByKey), result.Result!.Key)
|
||||
: DictionaryItemOperationStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class DeleteDictionaryController : DictionaryControllerBase
|
||||
{
|
||||
private readonly IDictionaryItemService _dictionaryItemService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public DeleteDictionaryController(IDictionaryItemService dictionaryItemService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_dictionaryItemService = dictionaryItemService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpDelete($"{{{nameof(id)}:guid}}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
Attempt<IDictionaryItem?, DictionaryItemOperationStatus> result = await _dictionaryItemService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
: DictionaryItemOperationStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
|
||||
|
||||
[VersionedApiBackOfficeRoute("dictionary")]
|
||||
[ApiExplorerSettings(GroupName = "Dictionary")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDictionary)]
|
||||
public abstract class DictionaryControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
protected IActionResult DictionaryItemOperationStatusResult(DictionaryItemOperationStatus status) =>
|
||||
OperationStatusResult(status, problemDetailsBuilder => status switch
|
||||
{
|
||||
DictionaryItemOperationStatus.DuplicateItemKey => Conflict(problemDetailsBuilder
|
||||
.WithTitle("Duplicate dictionary item name detected")
|
||||
.WithDetail("Another dictionary item exists with the same name. Dictionary item names must be unique.")
|
||||
.Build()),
|
||||
DictionaryItemOperationStatus.ItemNotFound => DictionaryItemNotFound(problemDetailsBuilder),
|
||||
DictionaryItemOperationStatus.ParentNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("The dictionary item parent could not be found")
|
||||
.Build()),
|
||||
DictionaryItemOperationStatus.CancelledByNotification => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Cancelled by notification")
|
||||
.WithDetail("A notification handler prevented the dictionary item operation.")
|
||||
.Build()),
|
||||
DictionaryItemOperationStatus.InvalidParent => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid parent")
|
||||
.WithDetail("The targeted parent dictionary item is not valid for this dictionary item operation.")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
|
||||
.WithTitle("Unknown dictionary operation status.")
|
||||
.Build()),
|
||||
});
|
||||
|
||||
protected IActionResult DictionaryItemNotFound() => OperationStatusResult(DictionaryItemOperationStatus.ItemNotFound, DictionaryItemNotFound);
|
||||
|
||||
private IActionResult DictionaryItemNotFound(ProblemDetailsBuilder problemDetailsBuilder)
|
||||
=> NotFound(problemDetailsBuilder
|
||||
.WithTitle("The dictionary item could not be found")
|
||||
.Build());
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ExportDictionaryController : DictionaryControllerBase
|
||||
{
|
||||
private readonly IDictionaryItemService _dictionaryItemService;
|
||||
private readonly IEntityXmlSerializer _entityXmlSerializer;
|
||||
|
||||
public ExportDictionaryController(IDictionaryItemService dictionaryItemService, IEntityXmlSerializer entityXmlSerializer)
|
||||
{
|
||||
_dictionaryItemService = dictionaryItemService;
|
||||
_entityXmlSerializer = entityXmlSerializer;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/export")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Export(Guid id, bool includeChildren = false)
|
||||
{
|
||||
IDictionaryItem? dictionaryItem = await _dictionaryItemService.GetAsync(id);
|
||||
if (dictionaryItem is null)
|
||||
{
|
||||
return DictionaryItemNotFound();
|
||||
}
|
||||
|
||||
XElement xml = _entityXmlSerializer.Serialize(dictionaryItem, includeChildren);
|
||||
|
||||
return File(Encoding.UTF8.GetBytes(xml.ToDataString()), MediaTypeNames.Application.Octet, $"{dictionaryItem.ItemKey}.udt");
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Api.Management.Services;
|
||||
using Umbraco.Cms.Api.Management.Services.OperationStatus;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Dictionary;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ImportDictionaryController : DictionaryControllerBase
|
||||
{
|
||||
private readonly IDictionaryItemImportService _dictionaryItemImportService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public ImportDictionaryController(
|
||||
IDictionaryItemImportService dictionaryItemImportService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_dictionaryItemImportService = dictionaryItemImportService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpPost("import")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Import(ImportDictionaryRequestModel importDictionaryRequestModel)
|
||||
{
|
||||
Attempt<IDictionaryItem?, DictionaryImportOperationStatus> result = await _dictionaryItemImportService
|
||||
.ImportDictionaryItemFromUdtFileAsync(
|
||||
importDictionaryRequestModel.TemporaryFile.Id,
|
||||
importDictionaryRequestModel.Parent?.Id,
|
||||
CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Success
|
||||
? CreatedAtId<ByKeyDictionaryController>(controller => nameof(controller.ByKey), result.Result!.Key)
|
||||
: OperationStatusResult(result.Status, problemDetailsBuilder => result.Status switch
|
||||
{
|
||||
DictionaryImportOperationStatus.ParentNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("The parent dictionary item could not be found.")
|
||||
.Build()),
|
||||
DictionaryImportOperationStatus.TemporaryFileNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("The temporary file with specified id could not be found.")
|
||||
.Build()),
|
||||
DictionaryImportOperationStatus.InvalidFileType => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid file type")
|
||||
.WithDetail("The dictionary import only supports UDT files.")
|
||||
.Build()),
|
||||
DictionaryImportOperationStatus.InvalidFileContent => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid file content")
|
||||
.WithDetail("The uploaded file could not be read as a valid UDT file.")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
|
||||
.WithTitle("Unknown dictionary import operation status.")
|
||||
.Build()),
|
||||
});
|
||||
}
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Item;
|
||||
|
||||
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/dictionary")]
|
||||
[ApiExplorerSettings(GroupName = "Dictionary")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDictionary)]
|
||||
public class DictionaryItemControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Dictionary.Item;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Item;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ItemDictionaryItemController : DictionaryItemControllerBase
|
||||
{
|
||||
private readonly IDictionaryItemService _dictionaryItemService;
|
||||
private readonly IUmbracoMapper _mapper;
|
||||
|
||||
public ItemDictionaryItemController(IDictionaryItemService dictionaryItemService, IUmbracoMapper mapper)
|
||||
{
|
||||
_dictionaryItemService = dictionaryItemService;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<DictionaryItemItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult> Item([FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
{
|
||||
IEnumerable<IDictionaryItem> dictionaryItems = await _dictionaryItemService.GetManyAsync(ids.ToArray());
|
||||
|
||||
List<DictionaryItemItemResponseModel> responseModels = _mapper.MapEnumerable<IDictionaryItem, DictionaryItemItemResponseModel>(dictionaryItems);
|
||||
return Ok(responseModels);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Dictionary;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class MoveDictionaryController : DictionaryControllerBase
|
||||
{
|
||||
private readonly IDictionaryItemService _dictionaryItemService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public MoveDictionaryController(IDictionaryItemService dictionaryItemService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_dictionaryItemService = dictionaryItemService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/move")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Move(Guid id, MoveDictionaryRequestModel moveDictionaryRequestModel)
|
||||
{
|
||||
IDictionaryItem? source = await _dictionaryItemService.GetAsync(id);
|
||||
if (source == null)
|
||||
{
|
||||
return DictionaryItemNotFound();
|
||||
}
|
||||
|
||||
Attempt<IDictionaryItem, DictionaryItemOperationStatus> result = await _dictionaryItemService.MoveAsync(
|
||||
source,
|
||||
moveDictionaryRequestModel.Target?.Id,
|
||||
CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
: DictionaryItemOperationStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ChildrenDictionaryTreeController : DictionaryTreeControllerBase
|
||||
{
|
||||
public ChildrenDictionaryTreeController(IEntityService entityService, IDictionaryItemService dictionaryItemService)
|
||||
: base(entityService, dictionaryItemService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("children")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<NamedEntityTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<NamedEntityTreeItemResponseModel>>> Children(Guid parentId, int skip = 0, int take = 100)
|
||||
{
|
||||
PagedModel<IDictionaryItem> paginatedItems = await DictionaryItemService.GetPagedAsync(parentId, skip, take);
|
||||
|
||||
return Ok(PagedViewModel(await MapTreeItemViewModels(parentId, paginatedItems.Items), paginatedItems.Total));
|
||||
}
|
||||
}
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Controllers.Tree;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
|
||||
|
||||
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Tree}/dictionary")]
|
||||
[ApiExplorerSettings(GroupName = "Dictionary")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDictionaryOrTemplates)]
|
||||
// NOTE: at the moment dictionary items (renamed to dictionary tree) aren't supported by EntityService, so we have little use of the
|
||||
// tree controller base. We'll keep it though, in the hope that we can mend EntityService.
|
||||
public class DictionaryTreeControllerBase : NamedEntityTreeControllerBase<NamedEntityTreeItemResponseModel>
|
||||
{
|
||||
public DictionaryTreeControllerBase(IEntityService entityService, IDictionaryItemService dictionaryItemService)
|
||||
: base(entityService) =>
|
||||
DictionaryItemService = dictionaryItemService;
|
||||
|
||||
// dictionary items do not currently have a known UmbracoObjectType, so we'll settle with Unknown for now
|
||||
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Unknown;
|
||||
|
||||
protected IDictionaryItemService DictionaryItemService { get; }
|
||||
|
||||
protected async Task<IEnumerable<NamedEntityTreeItemResponseModel>> MapTreeItemViewModels(Guid? parentKey, IEnumerable<IDictionaryItem> dictionaryItems)
|
||||
{
|
||||
async Task<NamedEntityTreeItemResponseModel> CreateEntityTreeItemViewModelAsync(IDictionaryItem dictionaryItem)
|
||||
{
|
||||
var hasChildren = await DictionaryItemService.CountChildrenAsync(dictionaryItem.Key) > 0;
|
||||
return new NamedEntityTreeItemResponseModel
|
||||
{
|
||||
Name = dictionaryItem.ItemKey,
|
||||
Id = dictionaryItem.Key,
|
||||
HasChildren = hasChildren,
|
||||
Parent = parentKey.HasValue
|
||||
? new ReferenceByIdModel
|
||||
{
|
||||
Id = parentKey.Value
|
||||
}
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
return await Task.WhenAll(dictionaryItems.Select(CreateEntityTreeItemViewModelAsync));
|
||||
}
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class RootDictionaryTreeController : DictionaryTreeControllerBase
|
||||
{
|
||||
public RootDictionaryTreeController(IEntityService entityService, IDictionaryItemService dictionaryItemService)
|
||||
: base(entityService, dictionaryItemService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("root")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<NamedEntityTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<NamedEntityTreeItemResponseModel>>> Root(int skip = 0, int take = 100)
|
||||
{
|
||||
PagedModel<IDictionaryItem> paginatedItems = await DictionaryItemService.GetPagedAsync(null, skip, take);
|
||||
|
||||
return Ok(PagedViewModel(await MapTreeItemViewModels(null, paginatedItems.Items), paginatedItems.Total));
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.Dictionary;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Dictionary;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class UpdateDictionaryController : DictionaryControllerBase
|
||||
{
|
||||
private readonly IDictionaryItemService _dictionaryItemService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly IDictionaryPresentationFactory _dictionaryPresentationFactory;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public UpdateDictionaryController(
|
||||
IDictionaryItemService dictionaryItemService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IDictionaryPresentationFactory dictionaryPresentationFactory,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_dictionaryItemService = dictionaryItemService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_dictionaryPresentationFactory = dictionaryPresentationFactory;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
[HttpPut($"{{{nameof(id)}:guid}}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Update(Guid id, UpdateDictionaryItemRequestModel updateDictionaryItemRequestModel)
|
||||
{
|
||||
IDictionaryItem? current = await _dictionaryItemService.GetAsync(id);
|
||||
if (current == null)
|
||||
{
|
||||
return DictionaryItemNotFound();
|
||||
}
|
||||
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
new DictionaryPermissionResource(updateDictionaryItemRequestModel.Translations.Select(t => t.IsoCode)),
|
||||
AuthorizationPolicies.DictionaryPermissionByResource);
|
||||
|
||||
if (authorizationResult.Succeeded is false)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
IDictionaryItem updated = await _dictionaryPresentationFactory.MapUpdateModelToDictionaryItemAsync(current, updateDictionaryItemRequestModel);
|
||||
|
||||
Attempt<IDictionaryItem, DictionaryItemOperationStatus> result =
|
||||
await _dictionaryItemService.UpdateAsync(updated, CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
: DictionaryItemOperationStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.Content;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ByKeyDocumentController : DocumentControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IContentEditingService _contentEditingService;
|
||||
private readonly IDocumentPresentationFactory _documentPresentationFactory;
|
||||
|
||||
public ByKeyDocumentController(
|
||||
IAuthorizationService authorizationService,
|
||||
IContentEditingService contentEditingService,
|
||||
IDocumentPresentationFactory documentPresentationFactory)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_contentEditingService = contentEditingService;
|
||||
_documentPresentationFactory = documentPresentationFactory;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(DocumentResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByKey(Guid id)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
IContent? content = await _contentEditingService.GetAsync(id);
|
||||
if (content == null)
|
||||
{
|
||||
return DocumentNotFound();
|
||||
}
|
||||
|
||||
DocumentResponseModel model = await _documentPresentationFactory.CreateResponseModelAsync(content);
|
||||
return Ok(model);
|
||||
}
|
||||
}
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document.Collection;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document.Collection;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ByKeyDocumentCollectionController : DocumentCollectionControllerBase
|
||||
{
|
||||
private readonly IContentListViewService _contentListViewService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public ByKeyDocumentCollectionController(
|
||||
IContentListViewService contentListViewService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IUmbracoMapper mapper)
|
||||
: base(mapper)
|
||||
{
|
||||
_contentListViewService = contentListViewService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<DocumentCollectionResponseModel>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByKey(
|
||||
Guid id,
|
||||
Guid? dataTypeId = null,
|
||||
string orderBy = "updateDate",
|
||||
string? orderCulture = null,
|
||||
Direction orderDirection = Direction.Ascending,
|
||||
string? filter = null,
|
||||
int skip = 0,
|
||||
int take = 100)
|
||||
{
|
||||
Attempt<ListViewPagedModel<IContent>?, ContentCollectionOperationStatus> collectionAttempt = await _contentListViewService.GetListViewItemsByKeyAsync(
|
||||
CurrentUser(_backOfficeSecurityAccessor),
|
||||
id,
|
||||
dataTypeId,
|
||||
orderBy,
|
||||
orderCulture,
|
||||
orderDirection,
|
||||
filter,
|
||||
skip,
|
||||
take);
|
||||
|
||||
return collectionAttempt.Success
|
||||
? CollectionResult(collectionAttempt.Result!)
|
||||
: CollectionOperationStatusResult(collectionAttempt.Status);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user