Compare commits
474
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 | ||
|
|
a6a76d1815 | ||
|
|
6379f2fd35 | ||
|
|
52c21b0fca |
@@ -35,7 +35,7 @@
|
||||
<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>
|
||||
|
||||
+44
-33
@@ -5,35 +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.NewtonsoftJson" Version="8.0.1" />
|
||||
<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.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.0" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
<!-- Umbraco packages -->
|
||||
<ItemGroup>
|
||||
@@ -45,49 +46,59 @@
|
||||
<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="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.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="Smidge.InMemory" Version="4.3.0" />
|
||||
<PackageVersion Include="Smidge.Nuglify" Version="4.3.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>
|
||||
|
||||
+181
-50
@@ -25,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,8 +75,12 @@ stages:
|
||||
- job: A
|
||||
displayName: Build Umbraco CMS
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
vmImage: 'windows-latest'
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 500
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js $(nodeVersion)
|
||||
retryCountOnTaskFailure: 3
|
||||
@@ -89,6 +97,11 @@ stages:
|
||||
- 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:
|
||||
@@ -102,6 +115,10 @@ stages:
|
||||
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
|
||||
- task: npm@1
|
||||
displayName: Run npm build (Login)
|
||||
inputs:
|
||||
@@ -125,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:
|
||||
@@ -135,6 +156,35 @@ stages:
|
||||
inputs:
|
||||
targetPath: $(Build.SourcesDirectory)
|
||||
artifactName: build_output
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish Backend BOM
|
||||
inputs:
|
||||
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}}))
|
||||
@@ -198,6 +248,11 @@ stages:
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 1
|
||||
fetchFilter: tree:0
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js 10.15.x
|
||||
retryCountOnTaskFailure: 3
|
||||
@@ -249,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:
|
||||
@@ -288,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
|
||||
@@ -319,7 +384,9 @@ stages:
|
||||
# Integration Tests (SQL Server)
|
||||
- job:
|
||||
timeoutInMinutes: 120
|
||||
condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerIntegrationTests}})
|
||||
# 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:
|
||||
matrix:
|
||||
@@ -415,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:
|
||||
@@ -429,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: |
|
||||
@@ -496,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
|
||||
@@ -522,18 +584,28 @@ 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)
|
||||
@@ -544,12 +616,30 @@ stages:
|
||||
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:
|
||||
@@ -635,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
|
||||
@@ -670,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
|
||||
@@ -695,6 +822,8 @@ stages:
|
||||
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
|
||||
@@ -721,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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,9 @@ public static class UmbracoBuilderAuthExtensions
|
||||
.SetLogoutEndpointUris(
|
||||
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetRevocationEndpointUris(
|
||||
Paths.MemberApi.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
|
||||
@@ -52,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
|
||||
|
||||
@@ -14,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}";
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
<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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
private readonly IRequestRedirectService _requestRedirectService;
|
||||
private readonly IRequestPreviewService _requestPreviewService;
|
||||
private readonly IRequestMemberAccessService _requestMemberAccessService;
|
||||
private const string PreviewContentRequestPathPrefix = $"/{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}";
|
||||
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(
|
||||
@@ -145,6 +145,11 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
path = DecodePath(path);
|
||||
path = path.Length == 0 ? "/" : path;
|
||||
|
||||
if (_apiContentPathResolver.IsResolvablePath(path) is false)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
IPublishedContent? contentItem = GetContent(path);
|
||||
if (contentItem is not 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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -160,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.",
|
||||
|
||||
+48
-21
@@ -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)
|
||||
|
||||
@@ -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 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 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,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Headers;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -48,16 +49,26 @@ public sealed class ConfigureImageSharpMiddlewareOptions : IConfigureOptions<Ima
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
int width = context.Parser.ParseValue<int>(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), context.Culture);
|
||||
if (width <= 0 || width > _imagingSettings.Resize.MaxWidth)
|
||||
if (context.Commands.Contains(ResizeWebProcessor.Width))
|
||||
{
|
||||
context.Commands.Remove(ResizeWebProcessor.Width);
|
||||
if (!int.TryParse(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), NumberStyles.Integer,
|
||||
CultureInfo.InvariantCulture, out var width)
|
||||
|| width < 0
|
||||
|| width >= _imagingSettings.Resize.MaxWidth)
|
||||
{
|
||||
context.Commands.Remove(ResizeWebProcessor.Width);
|
||||
}
|
||||
}
|
||||
|
||||
int height = context.Parser.ParseValue<int>(context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), context.Culture);
|
||||
if (height <= 0 || height > _imagingSettings.Resize.MaxHeight)
|
||||
if (context.Commands.Contains(ResizeWebProcessor.Height))
|
||||
{
|
||||
context.Commands.Remove(ResizeWebProcessor.Height);
|
||||
if (!int.TryParse(context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), NumberStyles.Integer,
|
||||
CultureInfo.InvariantCulture, out var height)
|
||||
|| height < 0
|
||||
|| height >= _imagingSettings.Resize.MaxHeight)
|
||||
{
|
||||
context.Commands.Remove(ResizeWebProcessor.Height);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SixLabors.ImageSharp" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Web" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Web" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.7, 3)" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.11, 3)" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Web" VersionOverride="[2.0.2, 3)" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.7" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Web" Version="2.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+8
@@ -7,7 +7,15 @@
|
||||
<ItemGroup>
|
||||
<!-- Take top-level depedendency on Azure.Identity, because Microsoft.EntityFrameworkCore.SqlServer depends on a vulnerable version -->
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<!-- Both Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer,NPoco.SqlServer, and more bring in a vulnerable version of System.Text.Json -->
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Microsoft.Data.SqlClient -->
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" />
|
||||
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
|
||||
|
||||
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+13
-11
@@ -16,8 +16,8 @@ namespace Umbraco.Cms.Persistence.EFCore.Locking;
|
||||
internal class SqlServerEFCoreDistributedLockingMechanism<T> : IDistributedLockingMechanism
|
||||
where T : DbContext
|
||||
{
|
||||
private readonly IOptionsMonitor<ConnectionStrings> _connectionStrings;
|
||||
private readonly IOptionsMonitor<GlobalSettings> _globalSettings;
|
||||
private ConnectionStrings _connectionStrings;
|
||||
private GlobalSettings _globalSettings;
|
||||
private readonly ILogger<SqlServerEFCoreDistributedLockingMechanism<T>> _logger;
|
||||
private readonly Lazy<IEFCoreScopeAccessor<T>> _scopeAccessor; // Hooray it's a circular dependency.
|
||||
|
||||
@@ -32,27 +32,29 @@ internal class SqlServerEFCoreDistributedLockingMechanism<T> : IDistributedLocki
|
||||
{
|
||||
_logger = logger;
|
||||
_scopeAccessor = scopeAccessor;
|
||||
_globalSettings = globalSettings;
|
||||
_connectionStrings = connectionStrings;
|
||||
_globalSettings = globalSettings.CurrentValue;
|
||||
_connectionStrings = connectionStrings.CurrentValue;
|
||||
globalSettings.OnChange(x=>_globalSettings = x);
|
||||
connectionStrings.OnChange(x=>_connectionStrings = x);
|
||||
}
|
||||
|
||||
public bool HasActiveRelatedScope => _scopeAccessor.Value.AmbientScope is not null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() &&
|
||||
string.Equals(_connectionStrings.CurrentValue.ProviderName, "Microsoft.Data.SqlClient", StringComparison.InvariantCultureIgnoreCase) && _scopeAccessor.Value.AmbientScope is not null;
|
||||
public bool Enabled => _connectionStrings.IsConnectionStringConfigured() &&
|
||||
string.Equals(_connectionStrings.ProviderName, "Microsoft.Data.SqlClient", StringComparison.InvariantCultureIgnoreCase) && _scopeAccessor.Value.AmbientScope is not null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null)
|
||||
{
|
||||
obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout;
|
||||
obtainLockTimeout ??= _globalSettings.DistributedLockingReadLockDefaultTimeout;
|
||||
return new SqlServerDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null)
|
||||
{
|
||||
obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout;
|
||||
obtainLockTimeout ??= _globalSettings.DistributedLockingWriteLockDefaultTimeout;
|
||||
return new SqlServerDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value);
|
||||
}
|
||||
|
||||
@@ -168,9 +170,9 @@ internal class SqlServerEFCoreDistributedLockingMechanism<T> : IDistributedLocki
|
||||
"A transaction with minimum ReadCommitted isolation level is required.");
|
||||
}
|
||||
|
||||
await dbContext.Database.ExecuteSqlRawAsync($"SET LOCK_TIMEOUT {(int)_timeout.TotalMilliseconds};");
|
||||
|
||||
var rowsAffected = await dbContext.Database.ExecuteSqlAsync(@$"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id={LockId}");
|
||||
#pragma warning disable EF1002
|
||||
var rowsAffected = await dbContext.Database.ExecuteSqlRawAsync(@$"SET LOCK_TIMEOUT {(int)_timeout.TotalMilliseconds};UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id={LockId}");
|
||||
#pragma warning restore EF1002
|
||||
|
||||
if (rowsAffected == 0)
|
||||
{
|
||||
|
||||
+10
-8
@@ -16,8 +16,8 @@ namespace Umbraco.Cms.Persistence.EFCore.Locking;
|
||||
internal class SqliteEFCoreDistributedLockingMechanism<T> : IDistributedLockingMechanism
|
||||
where T : DbContext
|
||||
{
|
||||
private readonly IOptionsMonitor<ConnectionStrings> _connectionStrings;
|
||||
private readonly IOptionsMonitor<GlobalSettings> _globalSettings;
|
||||
private ConnectionStrings _connectionStrings;
|
||||
private GlobalSettings _globalSettings;
|
||||
private readonly ILogger<SqliteEFCoreDistributedLockingMechanism<T>> _logger;
|
||||
private readonly Lazy<IEFCoreScopeAccessor<T>> _efCoreScopeAccessor;
|
||||
|
||||
@@ -29,27 +29,29 @@ internal class SqliteEFCoreDistributedLockingMechanism<T> : IDistributedLockingM
|
||||
{
|
||||
_logger = logger;
|
||||
_efCoreScopeAccessor = efCoreScopeAccessor;
|
||||
_connectionStrings = connectionStrings;
|
||||
_globalSettings = globalSettings;
|
||||
_globalSettings = globalSettings.CurrentValue;
|
||||
_connectionStrings = connectionStrings.CurrentValue;
|
||||
globalSettings.OnChange(x=>_globalSettings = x);
|
||||
connectionStrings.OnChange(x=>_connectionStrings = x);
|
||||
}
|
||||
|
||||
public bool HasActiveRelatedScope => _efCoreScopeAccessor.Value.AmbientScope is not null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() &&
|
||||
string.Equals(_connectionStrings.CurrentValue.ProviderName, "Microsoft.Data.Sqlite", StringComparison.InvariantCultureIgnoreCase) && _efCoreScopeAccessor.Value.AmbientScope is not null;
|
||||
public bool Enabled => _connectionStrings.IsConnectionStringConfigured() &&
|
||||
string.Equals(_connectionStrings.ProviderName, "Microsoft.Data.Sqlite", StringComparison.InvariantCultureIgnoreCase) && _efCoreScopeAccessor.Value.AmbientScope is not null;
|
||||
|
||||
// With journal_mode=wal we can always read a snapshot.
|
||||
public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null)
|
||||
{
|
||||
obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout;
|
||||
obtainLockTimeout ??= _globalSettings.DistributedLockingReadLockDefaultTimeout;
|
||||
return new SqliteDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value);
|
||||
}
|
||||
|
||||
// With journal_mode=wal only a single write transaction can exist at a time.
|
||||
public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null)
|
||||
{
|
||||
obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout;
|
||||
obtainLockTimeout ??= _globalSettings.DistributedLockingWriteLockDefaultTimeout;
|
||||
return new SqliteDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value);
|
||||
}
|
||||
|
||||
|
||||
@@ -127,10 +127,16 @@ internal class EFCoreScope<TDbContext> : CoreScope, IEfCoreScope<TDbContext>
|
||||
|
||||
Locks.ClearLocks(InstanceId);
|
||||
|
||||
if (ParentScope is null)
|
||||
// Since we can nest EFCoreScopes in other scopes derived from CoreScope, we should check whether our ParentScope OR the base ParentScope exists.
|
||||
// Only if neither do do we take responsibility for ensuring the locks are cleared.
|
||||
// Eventually the highest parent will clear the locks.
|
||||
// Further, these locks are a reference to the locks of the highest parent anyway (see the constructor of CoreScope).
|
||||
#pragma warning disable SA1100 // Do not prefix calls with base unless local implementation exists (justification: provides additional clarify here that this is defined on the base class).
|
||||
if (ParentScope is null && base.HasParentScope is false)
|
||||
{
|
||||
Locks.EnsureLocksCleared(InstanceId);
|
||||
}
|
||||
#pragma warning restore SA1100 // Do not prefix calls with base unless local implementation exists
|
||||
|
||||
_efCoreScopeProvider.PopAmbientScope();
|
||||
|
||||
|
||||
@@ -7,6 +7,14 @@
|
||||
<ItemGroup>
|
||||
<!-- Take top-level depedendency on Azure.Identity, because Microsoft.EntityFrameworkCore.SqlServer depends on a vulnerable version -->
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
|
||||
<!-- Take top-level depedendency on Microsoft.Extensions.Caching.Memory, because Microsoft.EntityFrameworkCore.* depends on a vulnerable version -->
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||
<!-- Both Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer,NPoco.SqlServer, and more bring in a vulnerable version of System.Text.Json -->
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Microsoft.Data.SqlClient -->
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" />
|
||||
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||
<PackageReference Include="OpenIddict.EntityFrameworkCore" />
|
||||
|
||||
+11
-8
@@ -17,8 +17,8 @@ namespace Umbraco.Cms.Persistence.SqlServer.Services;
|
||||
/// </summary>
|
||||
public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism
|
||||
{
|
||||
private readonly IOptionsMonitor<ConnectionStrings> _connectionStrings;
|
||||
private readonly IOptionsMonitor<GlobalSettings> _globalSettings;
|
||||
private ConnectionStrings _connectionStrings;
|
||||
private GlobalSettings _globalSettings;
|
||||
private readonly ILogger<SqlServerDistributedLockingMechanism> _logger;
|
||||
private readonly Lazy<IScopeAccessor> _scopeAccessor; // Hooray it's a circular dependency.
|
||||
|
||||
@@ -33,25 +33,28 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism
|
||||
{
|
||||
_logger = logger;
|
||||
_scopeAccessor = scopeAccessor;
|
||||
_globalSettings = globalSettings;
|
||||
_connectionStrings = connectionStrings;
|
||||
_globalSettings = globalSettings.CurrentValue;
|
||||
_connectionStrings = connectionStrings.CurrentValue;
|
||||
globalSettings.OnChange(x => _globalSettings = x);
|
||||
connectionStrings.OnChange(x => _connectionStrings = x);
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() &&
|
||||
string.Equals(_connectionStrings.CurrentValue.ProviderName,Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase);
|
||||
public bool Enabled => _connectionStrings.IsConnectionStringConfigured() &&
|
||||
string.Equals(_connectionStrings.ProviderName,Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null)
|
||||
{
|
||||
obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout;
|
||||
obtainLockTimeout ??= _globalSettings.DistributedLockingReadLockDefaultTimeout;
|
||||
return new SqlServerDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null)
|
||||
{
|
||||
obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout;
|
||||
obtainLockTimeout ??= _globalSettings.DistributedLockingWriteLockDefaultTimeout;
|
||||
return new SqlServerDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,15 @@
|
||||
<ItemGroup>
|
||||
<!-- Take top-level depedendency on Azure.Identity, because NPoco.SqlServer depends on a vulnerable version -->
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<!-- Both Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer,NPoco.SqlServer, and more bring in a vulnerable version of System.Text.Json -->
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Microsoft.Data.SqlClient -->
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" />
|
||||
|
||||
<PackageReference Include="NPoco.SqlServer" />
|
||||
|
||||
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -16,8 +16,8 @@ namespace Umbraco.Cms.Persistence.Sqlite.Services;
|
||||
|
||||
public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism
|
||||
{
|
||||
private readonly IOptionsMonitor<ConnectionStrings> _connectionStrings;
|
||||
private readonly IOptionsMonitor<GlobalSettings> _globalSettings;
|
||||
private ConnectionStrings _connectionStrings;
|
||||
private GlobalSettings _globalSettings;
|
||||
private readonly ILogger<SqliteDistributedLockingMechanism> _logger;
|
||||
private readonly Lazy<IScopeAccessor> _scopeAccessor;
|
||||
|
||||
@@ -29,25 +29,27 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism
|
||||
{
|
||||
_logger = logger;
|
||||
_scopeAccessor = scopeAccessor;
|
||||
_connectionStrings = connectionStrings;
|
||||
_globalSettings = globalSettings;
|
||||
_connectionStrings = connectionStrings.CurrentValue;
|
||||
_globalSettings = globalSettings.CurrentValue;
|
||||
globalSettings.OnChange(x=>_globalSettings = x);
|
||||
connectionStrings.OnChange(x=>_connectionStrings = x);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() &&
|
||||
string.Equals(_connectionStrings.CurrentValue.ProviderName, Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase);
|
||||
public bool Enabled => _connectionStrings.IsConnectionStringConfigured() &&
|
||||
string.Equals(_connectionStrings.ProviderName, Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
// With journal_mode=wal we can always read a snapshot.
|
||||
public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null)
|
||||
{
|
||||
obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout;
|
||||
obtainLockTimeout ??= _globalSettings.DistributedLockingReadLockDefaultTimeout;
|
||||
return new SqliteDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value);
|
||||
}
|
||||
|
||||
// With journal_mode=wal only a single write transaction can exist at a time.
|
||||
public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null)
|
||||
{
|
||||
obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout;
|
||||
obtainLockTimeout ??= _globalSettings.DistributedLockingWriteLockDefaultTimeout;
|
||||
return new SqliteDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,13 +30,13 @@
|
||||
</Target>
|
||||
|
||||
<Target Name="BuildBelle">
|
||||
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Client\" Command="npm ci --no-fund --no-audit --prefer-offline" />
|
||||
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Client\" Command="npm run build:skip-tests" />
|
||||
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Client\" Command="npm ci --no-fund --no-audit --prefer-offline" Timeout="600000" />
|
||||
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Client\" Command="npm run build:skip-tests" Timeout="600000" />
|
||||
</Target>
|
||||
|
||||
<Target Name="BuildLogin">
|
||||
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Login\" Command="npm ci --no-fund --no-audit --prefer-offline" />
|
||||
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Login\" Command="npm run build" />
|
||||
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Login\" Command="npm ci --no-fund --no-audit --prefer-offline" Timeout="600000" />
|
||||
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Login\" Command="npm run build" Timeout="600000" />
|
||||
</Target>
|
||||
|
||||
<Target Name="CleanStaticAssetsPreconditions" AfterTargets="Clean" Condition="'$(UmbracoBuild)' == ''">
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<!-- Generate JSON schema on build (and before copying to project) -->
|
||||
<Target Name="GenerateAppsettingsSchema" BeforeTargets="Build;CopyUmbracoJsonSchemaFiles" Condition="!Exists('$(_UmbracoCmsJsonSchemaReference)')">
|
||||
<Message Text="Generating $(_UmbracoCmsJsonSchemaReference) because it doesn't exist" Importance="high" />
|
||||
<Exec WorkingDirectory="$(MSBuildThisFileDirectory)..\..\tools\Umbraco.JsonSchema" Command="dotnet run --configuration $(Configuration) -- --outputFile "$(MSBuildThisFileDirectory)$(_UmbracoCmsJsonSchemaReference)"" />
|
||||
<Exec WorkingDirectory="$(MSBuildThisFileDirectory)..\..\tools\Umbraco.JsonSchema" Command="dotnet run --configuration $(Configuration) -- --outputFile "$(MSBuildThisFileDirectory)$(_UmbracoCmsJsonSchemaReference)"" Timeout="600000" />
|
||||
</Target>
|
||||
|
||||
<!-- Remove generated JSON schema on clean -->
|
||||
|
||||
@@ -48,6 +48,13 @@ public static class AppCacheExtensions
|
||||
return default;
|
||||
}
|
||||
|
||||
// If we've retrieved the specific string that represents null in the cache, return it only if we are requesting it (via a typed request for a string).
|
||||
// Otherwise consider it a null value.
|
||||
if (RetrievedNullRepresentationInCache(result))
|
||||
{
|
||||
return RequestedNullRepresentationInCache<T>() ? (T)result : default;
|
||||
}
|
||||
|
||||
return result.TryConvertTo<T>().Result;
|
||||
}
|
||||
|
||||
@@ -59,6 +66,18 @@ public static class AppCacheExtensions
|
||||
return default;
|
||||
}
|
||||
|
||||
// If we've retrieved the specific string that represents null in the cache, return it only if we are requesting it (via a typed request for a string).
|
||||
// Otherwise consider it a null value.
|
||||
if (RetrievedNullRepresentationInCache(result))
|
||||
{
|
||||
return RequestedNullRepresentationInCache<T>() ? (T)result : default;
|
||||
}
|
||||
|
||||
return result.TryConvertTo<T>().Result;
|
||||
}
|
||||
|
||||
private static bool RetrievedNullRepresentationInCache(object result) => result == (object)Cms.Core.Constants.Cache.NullRepresentationInCache;
|
||||
|
||||
private static bool RequestedNullRepresentationInCache<T>() => typeof(T) == typeof(string);
|
||||
|
||||
}
|
||||
|
||||
@@ -22,4 +22,6 @@ public static class CacheKeys
|
||||
|
||||
public const string ContentRecycleBinCacheKey = "recycleBin_content";
|
||||
public const string MediaRecycleBinCacheKey = "recycleBin_media";
|
||||
|
||||
public const string MemberUserNameCachePrefix = "uRepo_userNameKey+";
|
||||
}
|
||||
|
||||
@@ -142,7 +142,9 @@ public static class DistributedCacheExtensions
|
||||
Id = x.Item.Id,
|
||||
Key = x.Item.Key,
|
||||
ChangeTypes = x.ChangeTypes,
|
||||
Blueprint = x.Item.Blueprint
|
||||
Blueprint = x.Item.Blueprint,
|
||||
PublishedCultures = x.PublishedCultures?.ToArray(),
|
||||
UnpublishedCultures = x.UnpublishedCultures?.ToArray()
|
||||
});
|
||||
|
||||
dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads);
|
||||
@@ -157,15 +159,30 @@ public static class DistributedCacheExtensions
|
||||
=> dc.RefreshMemberCache(members.AsEnumerable());
|
||||
|
||||
public static void RefreshMemberCache(this DistributedCache dc, IEnumerable<IMember> members)
|
||||
=> dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.DistinctBy(x => (x.Id, x.Username)).Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, false)));
|
||||
|
||||
=> dc.RefreshByPayload(
|
||||
MemberCacheRefresher.UniqueId,
|
||||
GetPayloads(members, false));
|
||||
|
||||
[Obsolete("Use the overload accepting IEnumerable instead to avoid allocating arrays. This overload will be removed in Umbraco 13.")]
|
||||
public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members)
|
||||
=> dc.RemoveMemberCache(members.AsEnumerable());
|
||||
|
||||
public static void RemoveMemberCache(this DistributedCache dc, IEnumerable<IMember> members)
|
||||
=> dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.DistinctBy(x => (x.Id, x.Username)).Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, true)));
|
||||
=> dc.RefreshByPayload(
|
||||
MemberCacheRefresher.UniqueId,
|
||||
GetPayloads(members, true));
|
||||
|
||||
// Internal for unit test.
|
||||
internal static IEnumerable<MemberCacheRefresher.JsonPayload> GetPayloads(IEnumerable<IMember> members, bool removed)
|
||||
=> members
|
||||
.DistinctBy(x => (x.Id, x.Username))
|
||||
.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, removed)
|
||||
{
|
||||
PreviousUsername = x.HasAdditionalData &&
|
||||
x.AdditionalData!.TryGetValue(Cms.Core.Constants.Entities.AdditionalDataKeys.MemberPreviousUserName, out var previousUsername)
|
||||
? previousUsername?.ToString()
|
||||
: null,
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
namespace Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
|
||||
|
||||
/// <summary>
|
||||
/// Defines behaviours for clearing of cached partials views that are configured to be cached individually by member.
|
||||
/// </summary>
|
||||
public interface IMemberPartialViewCacheInvalidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Clears the partial view cache items for the specified member ids.
|
||||
/// </summary>
|
||||
/// <param name="memberIds">The member Ids to clear the cache for.</param>
|
||||
/// <remarks>
|
||||
/// Called from the <see cref="MemberCacheRefresher"/> when a member is saved or deleted.
|
||||
/// </remarks>
|
||||
void ClearPartialViewCacheItems(IEnumerable<int> memberIds);
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
namespace Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
|
||||
|
||||
internal class NoopMemberPartialViewCacheInvalidator : IMemberPartialViewCacheInvalidator
|
||||
{
|
||||
public void ClearPartialViewCacheItems(IEnumerable<int> memberIds)
|
||||
{
|
||||
// No operation performed, this is a no-op implementation.
|
||||
}
|
||||
}
|
||||
@@ -182,6 +182,10 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
public TreeChangeTypes ChangeTypes { get; init; }
|
||||
|
||||
public bool Blueprint { get; init; }
|
||||
|
||||
public string[]? PublishedCultures { get; init; }
|
||||
|
||||
public string[]? UnpublishedCultures { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// using Newtonsoft.Json;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
@@ -15,10 +18,37 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
|
||||
public static readonly Guid UniqueId = Guid.Parse("E285DF34-ACDC-4226-AE32-C0CB5CF388DA");
|
||||
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
private readonly IMemberPartialViewCacheInvalidator _memberPartialViewCacheInvalidator;
|
||||
|
||||
public MemberCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IIdKeyMap idKeyMap, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
|
||||
: base(appCaches, serializer, eventAggregator, factory) =>
|
||||
[Obsolete("Use the non obsoleted constructor instead. Scheduled for removal in v17")]
|
||||
public MemberCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
IIdKeyMap idKeyMap,
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheRefresherNotificationFactory factory)
|
||||
: this(
|
||||
appCaches,
|
||||
serializer,
|
||||
idKeyMap,
|
||||
eventAggregator,
|
||||
factory,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IMemberPartialViewCacheInvalidator>())
|
||||
{
|
||||
}
|
||||
|
||||
public MemberCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
IIdKeyMap idKeyMap,
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheRefresherNotificationFactory factory,
|
||||
IMemberPartialViewCacheInvalidator memberPartialViewCacheInvalidator)
|
||||
: base(appCaches, serializer, eventAggregator, factory)
|
||||
{
|
||||
_idKeyMap = idKeyMap;
|
||||
_memberPartialViewCacheInvalidator = memberPartialViewCacheInvalidator;
|
||||
}
|
||||
|
||||
#region Indirect
|
||||
|
||||
@@ -40,6 +70,8 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
|
||||
|
||||
public string? Username { get; }
|
||||
|
||||
public string? PreviousUsername { get; set; }
|
||||
|
||||
public bool Removed { get; }
|
||||
}
|
||||
|
||||
@@ -67,16 +99,36 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
|
||||
|
||||
private void ClearCache(params JsonPayload[] payloads)
|
||||
{
|
||||
AppCaches.ClearPartialViewCache();
|
||||
// Clear the partial views cache for all partials that are cached by member, for the updates members.
|
||||
_memberPartialViewCacheInvalidator.ClearPartialViewCacheItems(payloads.Select(p => p.Id));
|
||||
|
||||
Attempt<IAppPolicyCache?> memberCache = AppCaches.IsolatedCaches.Get<IMember>();
|
||||
|
||||
foreach (JsonPayload p in payloads)
|
||||
{
|
||||
_idKeyMap.ClearCache(p.Id);
|
||||
if (memberCache.Success)
|
||||
if (memberCache.Success is false)
|
||||
{
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, int>(p.Id));
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(p.Username));
|
||||
continue;
|
||||
}
|
||||
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, int>(p.Id));
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(p.Username));
|
||||
|
||||
// This specific cache key was introduced to fix an issue where the member username could not be the same as the member id, because the cache keys collided.
|
||||
// This is done in a bit of a hacky way, because the cache key is created internally in the repository, but we need to clear it here.
|
||||
// Ideally, we want to use a shared way of generating the key between this and the repository.
|
||||
// Additionally, the RepositoryCacheKeys actually caches the string to avoid re-allocating memory; we would like to also use this in the repository
|
||||
// See:
|
||||
// https://github.com/umbraco/Umbraco-CMS/pull/17350
|
||||
// https://github.com/umbraco/Umbraco-CMS/pull/17815
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(CacheKeys.MemberUserNameCachePrefix + p.Username));
|
||||
|
||||
// If provided, clear the cache by the previous user name too.
|
||||
if (string.IsNullOrEmpty(p.PreviousUsername) is false)
|
||||
{
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(p.PreviousUsername));
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(CacheKeys.MemberUserNameCachePrefix + p.PreviousUsername));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ public class RepositoryCachePolicyOptions
|
||||
public RepositoryCachePolicyOptions(Func<int> performCount)
|
||||
{
|
||||
PerformCount = performCount;
|
||||
CacheNullValues = false;
|
||||
GetAllCacheValidateCount = true;
|
||||
GetAllCacheAllowZeroCount = false;
|
||||
}
|
||||
@@ -21,6 +22,7 @@ public class RepositoryCachePolicyOptions
|
||||
public RepositoryCachePolicyOptions()
|
||||
{
|
||||
PerformCount = null;
|
||||
CacheNullValues = false;
|
||||
GetAllCacheValidateCount = false;
|
||||
GetAllCacheAllowZeroCount = false;
|
||||
}
|
||||
@@ -30,6 +32,11 @@ public class RepositoryCachePolicyOptions
|
||||
/// </summary>
|
||||
public Func<int>? PerformCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the Get method will cache null results so that the db is not hit for repeated lookups
|
||||
/// </summary>
|
||||
public bool CacheNullValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True/false as to validate the total item count when all items are returned from cache, the default is true but this
|
||||
/// means that a db lookup will occur - though that lookup will probably be significantly less expensive than the
|
||||
|
||||
@@ -51,7 +51,15 @@ public class ValueEditorCache : IValueEditorCache
|
||||
{
|
||||
foreach (Dictionary<int, IDataValueEditor> editors in _valueEditorCache.Values)
|
||||
{
|
||||
editors.Remove(id);
|
||||
if (editors.TryGetValue(id, out IDataValueEditor? editor))
|
||||
{
|
||||
if (editor is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
editors.Remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class TypeFinder : ITypeFinder
|
||||
"ServiceStack.", "SqlCE4Umbraco,", "Superpower,", // used by Serilog
|
||||
"System.", "TidyNet,", "TidyNet.", "WebDriver,", "itextsharp,", "mscorlib,", "NUnit,", "NUnit.", "NUnit3.",
|
||||
"Selenium.", "ImageProcessor", "MiniProfiler.", "Owin,", "SQLite",
|
||||
"ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
|
||||
"ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", "ReSharperTestRunnerArm32", "ReSharperTestRunnerArm64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
|
||||
};
|
||||
|
||||
private static readonly ConcurrentDictionary<string, Type?> TypeNamesCache = new();
|
||||
|
||||
@@ -160,6 +160,7 @@ public class ContentSettings
|
||||
internal const bool StaticDisableUnpublishWhenReferenced = false;
|
||||
internal const bool StaticAllowEditInvariantFromNonDefault = false;
|
||||
internal const bool StaticShowDomainWarnings = true;
|
||||
internal const bool StaticShowUnroutableContentWarnings = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the content notification settings.
|
||||
@@ -285,4 +286,10 @@ public class ContentSettings
|
||||
/// </summary>
|
||||
[DefaultValue(StaticShowDomainWarnings)]
|
||||
public bool ShowDomainWarnings { get; set; } = StaticShowDomainWarnings;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to show unroutable content warnings.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticShowUnroutableContentWarnings)]
|
||||
public bool ShowUnroutableContentWarnings { get; set; } = StaticShowUnroutableContentWarnings;
|
||||
}
|
||||
|
||||
@@ -129,6 +129,7 @@ public class GlobalSettings
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to install the database when it is missing.
|
||||
/// </summary>
|
||||
[Obsolete("This option will be removed in V16.")]
|
||||
[DefaultValue(StaticInstallMissingDatabase)]
|
||||
public bool InstallMissingDatabase { get; set; } = StaticInstallMissingDatabase;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ public class ModelsBuilderSettings
|
||||
internal const string StaticModelsDirectory = "~/umbraco/models";
|
||||
internal const bool StaticAcceptUnsafeModelsDirectory = false;
|
||||
internal const int StaticDebugLevel = 0;
|
||||
internal const bool StaticIncludeVersionNumberInGeneratedModels = true;
|
||||
private bool _flagOutOfDateModels = true;
|
||||
|
||||
/// <summary>
|
||||
@@ -78,4 +79,16 @@ public class ModelsBuilderSettings
|
||||
/// <remarks>0 means minimal (safe on live site), anything else means more and more details (maybe not safe).</remarks>
|
||||
[DefaultValue(StaticDebugLevel)]
|
||||
public int DebugLevel { get; set; } = StaticDebugLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the version number should be included in generated models.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default this is written to the <see cref="System.CodeDom.Compiler.GeneratedCodeAttribute"/> output in
|
||||
/// generated code for each property of the model. This can be useful for debugging purposes but isn't essential,
|
||||
/// and it has the causes the generated code to change every time Umbraco is upgraded. In turn, this leads
|
||||
/// to unnecessary code file changes that need to be checked into source control. Default is <c>true</c>.
|
||||
/// </remarks>
|
||||
[DefaultValue(StaticIncludeVersionNumberInGeneratedModels)]
|
||||
public bool IncludeVersionNumberInGeneratedModels { get; set; } = StaticIncludeVersionNumberInGeneratedModels;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ public class RequestHandlerSettings
|
||||
{
|
||||
internal const bool StaticAddTrailingSlash = true;
|
||||
internal const string StaticConvertUrlsToAscii = "try";
|
||||
internal const string StaticConvertFileNamesToAscii = "false";
|
||||
internal const bool StaticEnableDefaultCharReplacements = true;
|
||||
|
||||
internal static readonly CharItem[] DefaultCharCollection =
|
||||
@@ -73,6 +74,22 @@ public class RequestHandlerSettings
|
||||
/// </summary>
|
||||
public bool ShouldTryConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("try");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to convert file names to ASCII (valid values: "true", "try" or "false").
|
||||
/// </summary>
|
||||
[DefaultValue(StaticConvertFileNamesToAscii)]
|
||||
public string ConvertFileNamesToAscii { get; set; } = StaticConvertFileNamesToAscii;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether URLs should be converted to ASCII.
|
||||
/// </summary>
|
||||
public bool ShouldConvertFileNamesToAscii => ConvertFileNamesToAscii.InvariantEquals("true");
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether URLs should be tried to be converted to ASCII.
|
||||
/// </summary>
|
||||
public bool ShouldTryConvertFileNamesToAscii => ConvertFileNamesToAscii.InvariantEquals("try");
|
||||
|
||||
/// <summary>
|
||||
/// Disable all default character replacements
|
||||
/// </summary>
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
public class RichTextEditorSettings
|
||||
{
|
||||
internal const string StaticValidElements =
|
||||
"+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-s[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption,cite,video[*],audio[*],picture[*],source[*],canvas[*]";
|
||||
"+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-s[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption,cite,video[*],audio[*],picture[*],source[*],canvas[*],code";
|
||||
|
||||
internal const string StaticInvalidElements = "font";
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
@@ -19,12 +20,16 @@ public class SecuritySettings
|
||||
internal const bool StaticAllowEditInvariantFromNonDefault = false;
|
||||
internal const bool StaticAllowConcurrentLogins = false;
|
||||
internal const string StaticAuthCookieName = "UMB_UCONTEXT";
|
||||
internal const bool StaticUsernameIsEmail = true;
|
||||
internal const bool StaticMemberRequireUniqueEmail = true;
|
||||
|
||||
internal const string StaticAllowedUserNameCharacters =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\";
|
||||
|
||||
internal const int StaticMemberDefaultLockoutTimeInMinutes = 30 * 24 * 60;
|
||||
internal const int StaticUserDefaultLockoutTimeInMinutes = 30 * 24 * 60;
|
||||
internal const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000;
|
||||
internal const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to keep the user logged in.
|
||||
@@ -58,7 +63,14 @@ public class SecuritySettings
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the user's email address is to be considered as their username.
|
||||
/// </summary>
|
||||
public bool UsernameIsEmail { get; set; } = true;
|
||||
[DefaultValue(StaticUsernameIsEmail)]
|
||||
public bool UsernameIsEmail { get; set; } = StaticUsernameIsEmail;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the member's email address must be unique.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticMemberRequireUniqueEmail)]
|
||||
public bool MemberRequireUniqueEmail { get; set; } = StaticMemberRequireUniqueEmail;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the set of allowed characters for a username
|
||||
@@ -116,4 +128,28 @@ public class SecuritySettings
|
||||
/// </summary>
|
||||
[DefaultValue(StaticAllowConcurrentLogins)]
|
||||
public bool AllowConcurrentLogins { get; set; } = StaticAllowConcurrentLogins;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default duration (in milliseconds) of failed login attempts.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The default duration (in milliseconds) of failed login attempts.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// The user login endpoint ensures that failed login attempts take at least as long as the average successful login.
|
||||
/// However, if no successful logins have occurred, this value is used as the default duration.
|
||||
/// </remarks>
|
||||
[Range(0, int.MaxValue)] // TODO (V17): Change property type to short and update maximum range to short.MaxValue
|
||||
[DefaultValue(StaticUserDefaultFailedLoginDurationInMilliseconds)]
|
||||
public long UserDefaultFailedLoginDurationInMilliseconds { get; set; } = StaticUserDefaultFailedLoginDurationInMilliseconds;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum duration (in milliseconds) of failed login attempts.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The minimum duration (in milliseconds) of failed login attempts.
|
||||
/// </value>
|
||||
[Range(0, int.MaxValue)] // TODO (V17): Change property type to short and update maximum range to short.MaxValue
|
||||
[DefaultValue(StaticUserMinimumFailedLoginDurationInMilliseconds)]
|
||||
public long UserMinimumFailedLoginDurationInMilliseconds { get; set; } = StaticUserMinimumFailedLoginDurationInMilliseconds;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Umbraco.Cms.Core;
|
||||
|
||||
public static partial class Constants
|
||||
{
|
||||
public static class Cache
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the string used to represent a null value in the cache.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used in conjunction with the option to cache null values on the repository caches, so we
|
||||
/// can distinguish a true null "not found" value and a cached null value.</remarks>
|
||||
public const string NullRepresentationInCache = "*NULL*";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Umbraco.Cms.Core;
|
||||
namespace Umbraco.Cms.Core;
|
||||
|
||||
public static partial class Constants
|
||||
{
|
||||
@@ -24,14 +24,42 @@ public static partial class Constants
|
||||
public static class OutputCache
|
||||
{
|
||||
/// <summary>
|
||||
/// Output cache policy name for content
|
||||
/// Output cache policy name for content.
|
||||
/// </summary>
|
||||
public const string ContentCachePolicy = "DeliveryApiContent";
|
||||
|
||||
/// <summary>
|
||||
/// Output cache policy name for media
|
||||
/// Output cache policy name for media.
|
||||
/// </summary>
|
||||
public const string MediaCachePolicy = "DeliveryApiMedia";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for Delivery API header names.
|
||||
/// </summary>
|
||||
public static class HeaderNames
|
||||
{
|
||||
/// <summary>
|
||||
/// Header name for accept language.
|
||||
/// </summary>
|
||||
public const string AcceptLanguage = "Accept-Language";
|
||||
|
||||
/// <summary>
|
||||
/// Header name for API key.
|
||||
/// </summary>
|
||||
public const string ApiKey = "Api-Key";
|
||||
|
||||
/// <summary>
|
||||
/// Header name for preview.
|
||||
/// </summary>
|
||||
public const string Preview = "Preview";
|
||||
|
||||
/// <summary>
|
||||
/// Header name for start item.
|
||||
/// </summary>
|
||||
public const string StartItem = "Start-Item";
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Umbraco.Cms.Core;
|
||||
|
||||
public static partial class Constants
|
||||
{
|
||||
public static class Entities
|
||||
{
|
||||
public static class AdditionalDataKeys
|
||||
{
|
||||
public const string MemberPreviousUserName = "previousUsername";
|
||||
|
||||
public const string MemberGroupPreviousName = "previousName";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,5 +36,10 @@ public static partial class Constants
|
||||
public static string WebhookTotal = $"{WebhookPrefix}Total";
|
||||
public static string WebhookCustomHeaders = $"{WebhookPrefix}CustomHeaders";
|
||||
public static string WebhookCustomEvent = $"{WebhookPrefix}CustomEvent";
|
||||
public static string RichTextEditorCount = "RichTextEditorCount";
|
||||
public static string RichTextBlockCount = "RichTextBlockCount";
|
||||
public static string TotalPropertyCount = "TotalPropertyCount";
|
||||
public static string HighestPropertyCount = "HighestPropertyCount";
|
||||
public static string TotalCompositions = "TotalCompositions";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ public static partial class Constants
|
||||
public const string ControllerToken = "controller";
|
||||
public const string ActionToken = "action";
|
||||
public const string AreaToken = "area";
|
||||
public const string DynamicRoutePattern = "/{**umbracoSlug}";
|
||||
}
|
||||
|
||||
public static class RoutePath
|
||||
|
||||
@@ -15,6 +15,27 @@ public class ApiContentPathResolver : IApiContentPathResolver
|
||||
_apiPublishedContentCache = apiPublishedContentCache;
|
||||
}
|
||||
|
||||
[Obsolete("No longer used in V15. Scheduled for removal in V15.")]
|
||||
public virtual bool IsResolvablePath(string path)
|
||||
{
|
||||
// File requests will blow up with an downstream exception in GetRequiredPublishedSnapshot, which fails due to an UmbracoContext
|
||||
// not being available for what's considered a static file request.
|
||||
// See: https://github.com/umbraco/Umbraco-CMS/issues/19051
|
||||
// Given a URL segment and hence route can't contain a period, we can safely assume that if the last segment of the path contains
|
||||
// a period, it's a file request and should return null here.
|
||||
if (IsFileRequest(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsFileRequest(string path) => path
|
||||
.Split('/', StringSplitOptions.RemoveEmptyEntries)
|
||||
.LastOrDefault()?
|
||||
.Contains('.') is true;
|
||||
|
||||
public virtual IPublishedContent? ResolveContentPath(string path)
|
||||
{
|
||||
path = path.EnsureStartsWith("/");
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public sealed class ApiContentResponseBuilder : ApiContentBuilderBase<IApiContentResponse>, IApiContentResponseBuilder
|
||||
public class ApiContentResponseBuilder : ApiContentBuilderBase<IApiContentResponse>, IApiContentResponseBuilder
|
||||
{
|
||||
private readonly IApiContentRouteBuilder _apiContentRouteBuilder;
|
||||
|
||||
@@ -14,6 +13,12 @@ public sealed class ApiContentResponseBuilder : ApiContentBuilderBase<IApiConten
|
||||
=> _apiContentRouteBuilder = apiContentRouteBuilder;
|
||||
|
||||
protected override IApiContentResponse Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties)
|
||||
{
|
||||
IDictionary<string, IApiContentRoute> cultures = GetCultures(content);
|
||||
return new ApiContentResponse(content.Key, name, content.ContentType.Alias, content.CreateDate, content.UpdateDate, route, properties, cultures);
|
||||
}
|
||||
|
||||
protected virtual IDictionary<string, IApiContentRoute> GetCultures(IPublishedContent content)
|
||||
{
|
||||
var routesByCulture = new Dictionary<string, IApiContentRoute>();
|
||||
|
||||
@@ -35,6 +40,6 @@ public sealed class ApiContentResponseBuilder : ApiContentBuilderBase<IApiConten
|
||||
routesByCulture[publishedCultureInfo.Culture] = cultureRoute;
|
||||
}
|
||||
|
||||
return new ApiContentResponse(content.Key, name, content.ContentType.Alias, content.CreateDate, content.UpdateDate, route, properties, routesByCulture);
|
||||
return routesByCulture;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,6 @@ public sealed class ApiMediaUrlProvider : IApiMediaUrlProvider
|
||||
throw new ArgumentException("Media URLs can only be generated from Media items.", nameof(media));
|
||||
}
|
||||
|
||||
return _publishedUrlProvider.GetMediaUrl(media, UrlMode.Relative);
|
||||
return _publishedUrlProvider.GetMediaUrl(media);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,8 @@ namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IApiContentPathResolver
|
||||
{
|
||||
[Obsolete("No longer used in V15. Scheduled for removal in V15.")]
|
||||
bool IsResolvablePath(string path) => true;
|
||||
|
||||
IPublishedContent? ResolveContentPath(string path);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface ICurrentMemberClaimsProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the claims for the currently logged in member.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used by the OIDC user info endpoint to supply "current user" info.
|
||||
/// </remarks>
|
||||
Task<Dictionary<string, object>> GetClaimsAsync();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class NoopCurrentMemberClaimsProvider : ICurrentMemberClaimsProvider
|
||||
{
|
||||
public Task<Dictionary<string, object>> GetClaimsAsync() => Task.FromResult(new Dictionary<string, object>());
|
||||
}
|
||||
@@ -126,7 +126,6 @@ public static partial class UmbracoBuilderExtensions
|
||||
// register OEmbed providers - no type scanning - all explicit opt-in of adding types, IEmbedProvider is not IDiscoverable
|
||||
builder.EmbedProviders()
|
||||
.Append<YouTube>()
|
||||
.Append<Twitter>()
|
||||
.Append<Vimeo>()
|
||||
.Append<DailyMotion>()
|
||||
.Append<Flickr>()
|
||||
@@ -138,7 +137,8 @@ public static partial class UmbracoBuilderExtensions
|
||||
.Append<Issuu>()
|
||||
.Append<Hulu>()
|
||||
.Append<Giphy>()
|
||||
.Append<LottieFiles>();
|
||||
.Append<LottieFiles>()
|
||||
.Append<X>();
|
||||
builder.SearchableTrees().Add(() => builder.TypeLoader.GetTypes<ISearchableTree>());
|
||||
builder.BackOfficeAssets();
|
||||
builder.SelectorHandlers().Add(() => builder.TypeLoader.GetTypes<ISelectorHandler>());
|
||||
|
||||
@@ -8,12 +8,14 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
using Umbraco.Cms.Core.Configuration;
|
||||
using Umbraco.Cms.Core.Configuration.Grid;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Diagnostics;
|
||||
using Umbraco.Cms.Core.Dictionary;
|
||||
using Umbraco.Cms.Core.DynamicRoot;
|
||||
using Umbraco.Cms.Core.Editors;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Features;
|
||||
@@ -35,7 +37,6 @@ using Umbraco.Cms.Core.Runtime;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.DynamicRoot;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
using Umbraco.Cms.Core.Telemetry;
|
||||
using Umbraco.Cms.Core.Templates;
|
||||
@@ -341,6 +342,11 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
// Data type configuration cache
|
||||
Services.AddUnique<IDataTypeConfigurationCache, DataTypeConfigurationCache>();
|
||||
Services.AddNotificationHandler<DataTypeCacheRefresherNotification, DataTypeConfigurationCacheRefresher>();
|
||||
|
||||
// Partial view cache invalidators (no-op, shipped implementation is added in Umbraco.Web.Website, but we
|
||||
// need this to ensure we have a service registered for this interface even in headless setups).
|
||||
// See: https://github.com/umbraco/Umbraco-CMS/issues/19661
|
||||
Services.AddUnique<IMemberPartialViewCacheInvalidator, NoopMemberPartialViewCacheInvalidator>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ public class RootDynamicRootOriginFinder : IDynamicRootOriginFinder
|
||||
return null;
|
||||
}
|
||||
|
||||
var entity = _entityService.Get(query.Context.ParentKey);
|
||||
// when creating new content, CurrentKey will be null - fallback to using ParentKey
|
||||
Guid entityKey = query.Context.CurrentKey ?? query.Context.ParentKey;
|
||||
var entity = _entityService.Get(entityKey);
|
||||
|
||||
if (entity is null || _allowedObjectTypes.Contains(entity.NodeObjectType) is false)
|
||||
{
|
||||
|
||||
@@ -20,12 +20,14 @@ public class SiteDynamicRootOriginFinder : RootDynamicRootOriginFinder
|
||||
|
||||
public override Guid? FindOriginKey(DynamicRootNodeQuery query)
|
||||
{
|
||||
if (query.OriginAlias != SupportedOriginType || query.Context.CurrentKey.HasValue is false)
|
||||
if (query.OriginAlias != SupportedOriginType)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IEntitySlim? entity = _entityService.Get(query.Context.CurrentKey.Value);
|
||||
// when creating new content, CurrentKey will be null - fallback to using ParentKey
|
||||
Guid entityKey = query.Context.CurrentKey ?? query.Context.ParentKey;
|
||||
IEntitySlim? entity = _entityService.Get(entityKey);
|
||||
if (entity is null || entity.NodeObjectType != Constants.ObjectTypes.Document)
|
||||
{
|
||||
return null;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1469,6 +1469,7 @@ Mange hilsner fra Umbraco robotten
|
||||
<key alias="publishWithMissingDomain">Der er ikke noget domæne konfigureret for %0%, kontakt vensligst en
|
||||
administrator, se loggen for mere information
|
||||
</key>
|
||||
<key alias="publishWithNoUrl">Dokumentet har ikke nogen URL, muligvis grundet en kollision med et andet dokuments navn. Flere detaljer kan ses under Info.</key>
|
||||
<key alias="copySuccessMessage">Dit systems information er blevet kopieret til udklipsholderen</key>
|
||||
<key alias="cannotCopyInformation">Kunne desværre ikke kopiere dit systems information til udklipsholderen</key>
|
||||
<key alias="webhookSaved">Webhook gemt</key>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -167,6 +167,7 @@
|
||||
<key alias="move">Media moved</key>
|
||||
<key alias="copy">Media copied</key>
|
||||
<key alias="save">Media saved</key>
|
||||
<key alias="new">Media created</key>
|
||||
</area>
|
||||
<area alias="auditTrails">
|
||||
<key alias="atViewingFor">Viewing for</key>
|
||||
@@ -190,6 +191,7 @@
|
||||
<key alias="smallPublishVariant">Publish</key>
|
||||
<key alias="smallMove">Move</key>
|
||||
<key alias="smallSave">Save</key>
|
||||
<key alias="smallNew">New</key>
|
||||
<key alias="smallSaveVariant">Save</key>
|
||||
<key alias="smallDelete">Delete</key>
|
||||
<key alias="smallUnpublish">Unpublish</key>
|
||||
@@ -292,6 +294,7 @@
|
||||
Content.
|
||||
</key>
|
||||
<key alias="nestedContentDeleteAllItems">Are you sure you want to delete all items?</key>
|
||||
<key alias="nestedContentDeleteItem">Are you sure you want to delete this item?</key>
|
||||
<key alias="nestedContentNoContentTypes">No Content Types are configured for this property.</key>
|
||||
<key alias="nestedContentAddElementType">Add Element Type</key>
|
||||
<key alias="nestedContentSelectElementTypeModalTitle">Select Element Type</key>
|
||||
@@ -329,6 +332,7 @@
|
||||
<key alias="schedulePublishHelp">Select the date and time to publish and/or unpublish the content item.</key>
|
||||
<key alias="createEmpty">Create new</key>
|
||||
<key alias="createFromClipboard">Paste from clipboard</key>
|
||||
<key alias="removeItem">Remove item</key>
|
||||
<key alias="nodeIsInTrash">This item is in the Recycle Bin</key>
|
||||
<key alias="noProperties">No content can be added for this item</key>
|
||||
<key alias="variantSaveNotAllowed">Save is not allowed</key>
|
||||
@@ -1700,6 +1704,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
<key alias="publishWithMissingDomain">There is no domain configured for %0%, please contact an administrator, see
|
||||
log for more information
|
||||
</key>
|
||||
<key alias="publishWithNoUrl">The document does not have a URL, possibly due to a naming collision with another document. More details can be found under Info.</key>
|
||||
<key alias="copySuccessMessage">Your system information has successfully been copied to the clipboard</key>
|
||||
<key alias="cannotCopyInformation">Could not copy your system information to the clipboard</key>
|
||||
<key alias="webhookSaved">Webhook saved</key>
|
||||
@@ -3077,7 +3082,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
<key alias="detailedLevelDescription"><![CDATA[We will send:
|
||||
<ul>
|
||||
<li>Anonymized site ID, Umbraco version, and packages installed.</li>
|
||||
<li>Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, Backoffice external login providers, Webhooks, and Property Editors in use.</li>
|
||||
<li>Number of: Root nodes, Content nodes, Macros, Media, Document Types, Property Types, Compositions, Templates, Languages, Domains, User Group, Users, Members, Backoffice external login providers, Webhooks, rich text datatypes, blocks used in rich text datatypes, and Property Editors in use.</li>
|
||||
<li>System information: Webserver, server OS, server framework, server OS language, and database provider.</li>
|
||||
<li>Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, whether the delivery API is enabled, and allows public access, and if you are in debug mode.</li>
|
||||
</ul>
|
||||
|
||||
@@ -165,6 +165,7 @@
|
||||
<key alias="move">Media moved</key>
|
||||
<key alias="copy">Media copied</key>
|
||||
<key alias="save">Media saved</key>
|
||||
<key alias="new">Media created</key>
|
||||
</area>
|
||||
<area alias="auditTrails">
|
||||
<key alias="atViewingFor">Viewing for</key>
|
||||
@@ -189,6 +190,7 @@
|
||||
<key alias="smallPublishVariant">Publish</key>
|
||||
<key alias="smallMove">Move</key>
|
||||
<key alias="smallSave">Save</key>
|
||||
<key alias="smallNew">New</key>
|
||||
<key alias="smallSaveVariant">Save</key>
|
||||
<key alias="smallDelete">Delete</key>
|
||||
<key alias="smallUnpublish">Unpublish</key>
|
||||
@@ -288,6 +290,7 @@
|
||||
<![CDATA[<a href="https://docs.umbraco.com/umbraco-cms/fundamentals/data/scheduled-publishing#timezones" target="_blank" rel="noopener">What does this mean?</a>]]></key>
|
||||
<key alias="nestedContentDeleteItem">Are you sure you want to delete this item?</key>
|
||||
<key alias="nestedContentDeleteAllItems">Are you sure you want to delete all items?</key>
|
||||
<key alias="nestedContentDeleteItem">Are you sure you want to delete this item?</key>
|
||||
<key alias="nestedContentEditorNotSupported">Property %0% uses editor %1% which is not supported by Nested
|
||||
Content.
|
||||
</key>
|
||||
@@ -328,6 +331,7 @@
|
||||
<key alias="schedulePublishHelp">Select the date and time to publish and/or unpublish the content item.</key>
|
||||
<key alias="createEmpty">Create new</key>
|
||||
<key alias="createFromClipboard">Paste from clipboard</key>
|
||||
<key alias="removeItem">Remove item</key>
|
||||
<key alias="nodeIsInTrash">This item is in the Recycle Bin</key>
|
||||
<key alias="variantSaveNotAllowed">Save is not allowed</key>
|
||||
<key alias="variantPublishNotAllowed">Publish is not allowed</key>
|
||||
@@ -1732,6 +1736,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
<key alias="publishWithMissingDomain">There is no domain configured for %0%, please contact an administrator, see
|
||||
log for more information
|
||||
</key>
|
||||
<key alias="publishWithNoUrl">The document does not have a URL, possibly due to a naming collision with another document. More details can be found under Info.</key>
|
||||
<key alias="preventCleanupEnableError">An error occurred while enabling version cleanup for %0%</key>
|
||||
<key alias="preventCleanupDisableError">An error occurred while disabling version cleanup for %0%</key>
|
||||
<key alias="copySuccessMessage">Your system information has successfully been copied to the clipboard</key>
|
||||
@@ -3096,7 +3101,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
We will send:
|
||||
<ul>
|
||||
<li>Anonymized site ID, Umbraco version, and packages installed.</li>
|
||||
<li>Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, Backoffice external login providers, Webhooks, and Property Editors in use.</li>
|
||||
<li>Number of: Root nodes, Content nodes, Macros, Media, Document Types, Property Types, Compositions, Templates, Languages, Domains, User Group, Users, Members, Backoffice external login providers, Webhooks, rich text datatypes, blocks used in rich text datatypes, and Property Editors in use.</li>
|
||||
<li>System information: Webserver, server OS, server framework, server OS language, and database provider.</li>
|
||||
<li>Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, whether the delivery API is enabled, and allows public access, and if you are in debug mode.</li>
|
||||
</ul>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,6 @@
|
||||
<name>The Umbraco community</name>
|
||||
<link>https://docs.umbraco.com/umbraco-cms/extending/language-files</link>
|
||||
</creator>
|
||||
<area alias="apps">
|
||||
<key alias="umbContent">Innehåll</key>
|
||||
</area>
|
||||
<area alias="actions">
|
||||
<key alias="assigndomain">Hantera domännamn</key>
|
||||
<key alias="auditTrail">Hantera versioner</key>
|
||||
@@ -14,13 +11,16 @@
|
||||
<key alias="changeDocType">Ändra dokumenttyp</key>
|
||||
<key alias="copy">Kopiera</key>
|
||||
<key alias="create">Skapa</key>
|
||||
<key alias="export">Exportera</key>
|
||||
<key alias="createGroup">Skapa grupp</key>
|
||||
<key alias="createPackage">Skapa paket</key>
|
||||
<key alias="createblueprint">Skapa innehållsmall</key>
|
||||
<key alias="defaultValue">Standardvärde</key>
|
||||
<key alias="delete">Ta bort</key>
|
||||
<key alias="disable">Avaktivera</key>
|
||||
<key alias="editSettings">Ändra inställningar</key>
|
||||
<key alias="emptyrecyclebin">Töm papperskorgen</key>
|
||||
<key alias="enable">Aktivera</key>
|
||||
<key alias="exportDocumentType">Exportera dokumenttyp</key>
|
||||
<key alias="importdocumenttype">Importera dokumenttyp</key>
|
||||
<key alias="importPackage">Importera paket</key>
|
||||
@@ -40,6 +40,13 @@
|
||||
<key alias="translate">Översätt</key>
|
||||
<key alias="unpublish">Avpublicera</key>
|
||||
<key alias="update">Uppdatera</key>
|
||||
<key alias="setPermissions">Ändra rättigheter</key>
|
||||
<key alias="unlock">Lås upp</key>
|
||||
<key alias="createblueprint">Skapa innehållsmall</key>
|
||||
<key alias="resendInvite">Skicka inbjudan igen</key>
|
||||
<key alias="toggleHideUnavailable">Dölj otillgängliga alternativ</key>
|
||||
<key alias="changeDataType">Ändra datatyp</key>
|
||||
<key alias="editContent">Redigera innehåll</key>
|
||||
</area>
|
||||
<area alias="actionCategories">
|
||||
<key alias="administration">Administration</key>
|
||||
@@ -47,6 +54,10 @@
|
||||
<key alias="other">Övrigt</key>
|
||||
<key alias="content">Innehåll</key>
|
||||
</area>
|
||||
<area alias="apps">
|
||||
<key alias="umbContent">Innehåll</key>
|
||||
<key alias="umbInfo">Info</key>
|
||||
</area>
|
||||
<area alias="assignDomain">
|
||||
<key alias="addNew">Lägg till nytt domännamn</key>
|
||||
<key alias="domain">Domännamn</key>
|
||||
@@ -113,8 +124,21 @@
|
||||
<key alias="areaValidationEntriesExceed"><![CDATA[<strong>%0%</strong> måste maximalt finnas <strong>%3%</strong> time(s).]]></key>
|
||||
</area>
|
||||
<area alias="validation">
|
||||
<key alias="invalidEmail">Ogiltig e-postadress</key>
|
||||
<key alias="invalidNull">Värde får inte vara 'null'</key>
|
||||
<key alias="invalidEmpty">Värde får inte vara tomt</key>
|
||||
<key alias="invalidPattern">Värdet är ogiltigt och matchar inte det korrekta formatet</key>
|
||||
<key alias="entriesShort"><![CDATA[Minsta %0% poster, kräver <strong>%1%</strong> mer.]]></key>
|
||||
<key alias="entriesExceed"><![CDATA[Max %0% poster, <strong>%1%</strong> för många.]]></key>
|
||||
<key alias="entriesAreasMismatch">Innehållskraven har ej godkänts på ett eller flera områden.</key>
|
||||
<key alias="invalidMemberGroupName">Ogiltigt namn för medlemsgrupp</key>
|
||||
<key alias="invalidUserGroupName">Ogiltigt namn för användargrupp</key>
|
||||
<key alias="invalidToken">Ogiltig token</key>
|
||||
<key alias="invalidUsername">Ogiltigt användarnamn</key>
|
||||
<key alias="duplicateEmail">E-postadressen '%0%' är redan tagen</key>
|
||||
<key alias="duplicateUserGroupName">Namnet '%0%' är redan taget</key>
|
||||
<key alias="duplicateMemberGroupName">Namnet '%0%' är redan taget</key>
|
||||
<key alias="duplicateUsername">Användarnamnet '%0%' är redan taget</key>
|
||||
</area>
|
||||
<area alias="blueprints">
|
||||
<key alias="createBlueprintFrom"><![CDATA[Skapa en ny innehållsmall för <em>%0%</em>]]></key>
|
||||
@@ -176,6 +200,7 @@
|
||||
<key alias="clickToEdit">Klicka för att redigera detta objekt</key>
|
||||
<key alias="createBy">Skapad av</key>
|
||||
<key alias="createByDesc">Ursprunglig författare</key>
|
||||
<key alias="updatedBy" version="7.0">Uppdaterad av</key>
|
||||
<key alias="createDate">Skapad</key>
|
||||
<key alias="createDateDesc">Datum/tid som dokumentet skapades</key>
|
||||
<key alias="documentType">Dokumenttyp</key>
|
||||
@@ -559,6 +584,29 @@
|
||||
<key alias="retrieve">Hämta</key>
|
||||
<key alias="selected">valda</key>
|
||||
</area>
|
||||
<area alias="colors">
|
||||
<key alias="blue">Blå</key>
|
||||
</area>
|
||||
<area alias="shortcuts">
|
||||
<key alias="addGroup">Lägg till grupp</key>
|
||||
<key alias="addProperty">Lägg till egenskap</key>
|
||||
<key alias="addEditor">Lägg till redigerare</key>
|
||||
<key alias="addTemplate">Lägg till mall</key>
|
||||
<key alias="editDataType">Ändra datatyp</key>
|
||||
<key alias="shortcut">Genvägar</key>
|
||||
<key alias="showShortcuts">visa genvägar</key>
|
||||
<key alias="toggleListView">Toggle list view</key>
|
||||
<key alias="toggleAllowAsRoot">Toggle allow as root</key>
|
||||
<key alias="commentLine">Kommentera bort/återställ rader</key>
|
||||
<key alias="removeLine">Radera rad</key>
|
||||
<key alias="copyLineUp">Kopiera upp rader</key>
|
||||
<key alias="copyLineDown">Kopiera ned rader</key>
|
||||
<key alias="moveLineUp">Flytta upp rader</key>
|
||||
<key alias="moveLineDown">Flytta ned rader</key>
|
||||
<key alias="generalHeader">Allmänt</key>
|
||||
<key alias="editorHeader">Redigerare</key>
|
||||
<key alias="addTab">Lägg till tabb</key>
|
||||
</area>
|
||||
<area alias="graphicheadline">
|
||||
<key alias="backgroundcolor">Bakgrundsfärg</key>
|
||||
<key alias="bold">Fetstil</key>
|
||||
@@ -661,6 +709,11 @@
|
||||
<key alias="createNewMember">Skapa en ny medlem</key>
|
||||
<key alias="allMembers">Alla medlemmar</key>
|
||||
<key alias="memberGroupNoProperties">Medlemsgrupper har inga extra egenskaper för redigering.</key>
|
||||
<key alias="duplicateMemberLogin">En medlem med detta inlogg existerar redan</key>
|
||||
<key alias="memberHasGroup">Medlem finns redan i gruppen '%0%'</key>
|
||||
<key alias="memberHasPassword">Medlem har redan ett lösenord</key>
|
||||
<key alias="memberLockoutNotEnabled">Avstängning är inte aktiverat för denna medlem</key>
|
||||
<key alias="memberNotInGroup">Medlem är inte med i gruppen '%0%'</key>
|
||||
</area>
|
||||
<area alias="moveOrCopy">
|
||||
<key alias="choose">Välj sida ovan...</key>
|
||||
@@ -1087,9 +1140,45 @@
|
||||
<key alias="sortCreateDateAscending">Äldst</key>
|
||||
<key alias="sortCreateDateDescending">Nyast</key>
|
||||
<key alias="sortLastLoginDateDescending">Senaste login</key>
|
||||
<key alias="duplicateLogin">En användare med detta inlogg existerar redan</key>
|
||||
<key alias="passwordRequiresDigit">Lösenordet måste innehålla minst en siffra ('0'-'9')</key>
|
||||
<key alias="passwordRequiresLower">Lösenordet måste innehålla minst en liten bokstav ('a'-'z')</key>
|
||||
<key alias="passwordRequiresNonAlphanumeric">Lösenordet måste innehålla minst ett specialtecken</key>
|
||||
<key alias="passwordRequiresUniqueChars">Lösenordet måste innehålla minst %0% unika tecken</key>
|
||||
<key alias="passwordRequiresUpper">Lösenordet måste innehålla minst en stor bokstav ('A'-'Z')</key>
|
||||
<key alias="passwordTooShort">Lösenordet måste vara minst %0% tecken</key>
|
||||
<key alias="userHasPassword">Användaren har redan ett lösenord</key>
|
||||
<key alias="userHasGroup">Användaren finns redan i gruppen '%0%'</key>
|
||||
<key alias="userLockoutNotEnabled">Avstängning är inte aktiverat för denna användaren</key>
|
||||
<key alias="userNotInGroup">Användaren finns inte i gruppen '%0%'</key>
|
||||
</area>
|
||||
<area alias="logViewer">
|
||||
<key alias="selectAllLogLevelFilters">Välj alla</key>
|
||||
<key alias="deselectAllLogLevelFilters">Avmarkera alla</key>
|
||||
</area>
|
||||
<area alias="preview">
|
||||
<key alias="endLabel">Avsluta</key>
|
||||
<key alias="endTitle">Avsluta förhandsvisning</key>
|
||||
<key alias="openWebsiteLabel">Förhandsvisa webbplats</key>
|
||||
<key alias="openWebsiteTitle">Öppna webbplats i förhandsvisningsläge</key>
|
||||
<key alias="returnToPreviewHeadline">Förhandsvisa webbplats?</key>
|
||||
<key alias="returnToPreviewDescription">Du har avslutat förhandsvisningsläge, vill du aktivera det igen för att se senast sparade version av webbplatsen?</key>
|
||||
<key alias="returnToPreviewAcceptButton">Förhandsvisa senaste version</key>
|
||||
<key alias="returnToPreviewDeclineButton">Visa publicerad version</key>
|
||||
<key alias="viewPublishedContentHeadline">Visa publicerad version?</key>
|
||||
<key alias="viewPublishedContentDescription">Du visar webbplats i förhandsvisningsläge, vill du avsluta förhandsvisning och visa senaste publicerad version av webbplatsen?
|
||||
</key>
|
||||
<key alias="viewPublishedContentAcceptButton">Visa publicerad version</key>
|
||||
<key alias="viewPublishedContentDeclineButton">Fortsätt förhandsvisning</key>
|
||||
</area>
|
||||
<area alias="permissions">
|
||||
<key alias="FolderCreation">Skapa kataloger</key>
|
||||
<key alias="FileWritingForPackages">Skriva filer till paket</key>
|
||||
<key alias="FileWriting">Skriva filer</key>
|
||||
<key alias="MediaFolderCreation">Skapa media-kataloger</key>
|
||||
</area>
|
||||
<area alias="analytics">
|
||||
<key alias="minimalLevelDescription">Vi kommer endast att skicka ett anonymt Id så att vi informeras om att webbplatsens finns.</key>
|
||||
<key alias="basicLevelDescription">Vi kommer att skicka ett anonymt Id, Umbraco-version och installerade paket</key>
|
||||
</area>
|
||||
</language>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
@using Umbraco.Extensions
|
||||
|
||||
@{
|
||||
var isLoggedIn = Context.User?.Identity?.IsAuthenticated ?? false;
|
||||
var isLoggedIn = Context.User.GetMemberIdentity()?.IsAuthenticated ?? false;
|
||||
var logoutModel = new PostRedirectModel();
|
||||
// You can modify this to redirect to a different URL instead of the current one
|
||||
logoutModel.RedirectUrl = null;
|
||||
@@ -15,7 +15,7 @@
|
||||
{
|
||||
<div class="login-status">
|
||||
|
||||
<p>Welcome back <strong>@Context?.User?.Identity?.Name</strong>!</p>
|
||||
<p>Welcome back <strong>@Context.User?.GetMemberIdentity()?.Name</strong>!</p>
|
||||
|
||||
@using (Html.BeginUmbracoForm<UmbLoginStatusController>("HandleLogout", new { RedirectUrl = logoutModel.RedirectUrl }))
|
||||
{
|
||||
|
||||
@@ -330,7 +330,10 @@ public static class ClaimsIdentityExtensions
|
||||
var firstValue = identity.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (firstValue is not null)
|
||||
{
|
||||
return int.Parse(firstValue, CultureInfo.InvariantCulture);
|
||||
if (int.TryParse(firstValue, CultureInfo.InvariantCulture, out var id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
public static class IntExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Does something 'x' amount of times
|
||||
/// Does something 'x' amount of times.
|
||||
/// </summary>
|
||||
/// <param name="n"></param>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="n">Number of times to execute the action.</param>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
public static void Times(this int n, Action<int> action)
|
||||
{
|
||||
for (var i = 0; i < n; i++)
|
||||
@@ -19,11 +21,11 @@ public static class IntExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Guid based on an integer value
|
||||
/// Creates a Guid based on an integer value.
|
||||
/// </summary>
|
||||
/// <param name="value"><see cref="int" /> value to convert</param>
|
||||
/// <param name="value">The <see cref="int" /> value to convert.</param>
|
||||
/// <returns>
|
||||
/// <see cref="Guid" />
|
||||
/// The converted <see cref="Guid" />.
|
||||
/// </returns>
|
||||
public static Guid ToGuid(this int value)
|
||||
{
|
||||
@@ -31,4 +33,28 @@ public static class IntExtensions
|
||||
BitConverter.GetBytes(value).CopyTo(bytes, 0);
|
||||
return new Guid(bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores a GUID previously created from an integer value using <see cref="ToGuid" />.
|
||||
/// </summary>
|
||||
/// <param name="value">The <see cref="Guid" /> value to convert.</param>
|
||||
/// <param name="result">The converted <see cref="int" />.</param>
|
||||
/// <returns>
|
||||
/// True if the <see cref="int" /> value could be created, otherwise false.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This is used with Umbraco entities that only have integer references in the database (e.g. users).
|
||||
/// </remarks>
|
||||
public static bool TryParseFromGuid(Guid value, [NotNullWhen(true)] out int? result)
|
||||
{
|
||||
if (value.ToString().EndsWith("-0000-0000-0000-000000000000") is false)
|
||||
{
|
||||
// We have a proper GUID, not one converted from an integer.
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = BitConverter.ToInt32(value.ToByteArray());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,7 +826,7 @@ public static class PublishedContentExtensions
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot
|
||||
/// This can be useful in order to return all nodes in an entire site by a type when combined with ContentAtRoot
|
||||
/// </remarks>
|
||||
public static IEnumerable<IPublishedContent> DescendantsOrSelfOfType(
|
||||
this IEnumerable<IPublishedContent> parentNodes, IVariationContextAccessor variationContextAccessor, string docTypeAlias, string? culture = null) => parentNodes.SelectMany(x =>
|
||||
@@ -843,7 +843,7 @@ public static class PublishedContentExtensions
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot
|
||||
/// This can be useful in order to return all nodes in an entire site by a type when combined with ContentAtRoot
|
||||
/// </remarks>
|
||||
public static IEnumerable<T> DescendantsOrSelf<T>(this IEnumerable<IPublishedContent> parentNodes, IVariationContextAccessor variationContextAccessor, string? culture = null)
|
||||
where T : class, IPublishedContent =>
|
||||
|
||||
@@ -21,16 +21,17 @@ public sealed class PublicAccessHandler :
|
||||
|
||||
private void Handle(IEnumerable<IMemberGroup> affectedEntities)
|
||||
{
|
||||
var keyName = Constants.Entities.AdditionalDataKeys.MemberGroupPreviousName;
|
||||
foreach (IMemberGroup grp in affectedEntities)
|
||||
{
|
||||
// check if the name has changed
|
||||
if ((grp.AdditionalData?.ContainsKey("previousName") ?? false)
|
||||
&& grp.AdditionalData["previousName"] != null
|
||||
&& grp.AdditionalData["previousName"]?.ToString().IsNullOrWhiteSpace() == false
|
||||
&& grp.AdditionalData["previousName"]?.ToString() != grp.Name)
|
||||
if ((grp.AdditionalData?.ContainsKey(keyName) ?? false)
|
||||
&& grp.AdditionalData[keyName] != null
|
||||
&& grp.AdditionalData[keyName]?.ToString().IsNullOrWhiteSpace() == false
|
||||
&& grp.AdditionalData[keyName]?.ToString() != grp.Name)
|
||||
{
|
||||
_publicAccessService.RenameMemberGroupRoleRules(
|
||||
grp.AdditionalData["previousName"]?.ToString(),
|
||||
grp.AdditionalData[keyName]?.ToString(),
|
||||
grp.Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,19 +346,12 @@ namespace Umbraco.Cms.Core.IO
|
||||
// our root path, due to relative segments, so better check
|
||||
if (_ioHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar))
|
||||
{
|
||||
// this says that 4.7.2 supports long paths - but Windows does not
|
||||
// https://docs.microsoft.com/en-us/dotnet/api/system.io.pathtoolongexception?view=netframework-4.7.2
|
||||
if (path.Length > 260)
|
||||
{
|
||||
throw new PathTooLongException($"Path {path} is too long.");
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
// nothing prevents us to reach the file, security-wise, yet it is outside
|
||||
// this filesystem's root - throw
|
||||
throw new UnauthorizedAccessException($"File original: [{originalPath}] full: [{path}] is outside this filesystem's root.");
|
||||
throw new UnauthorizedAccessException($"Requested path {originalPath} is outside this filesystem's root.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Umbraco.Cms.Core.Media.EmbedProviders;
|
||||
/// <summary>
|
||||
/// Embed Provider for Twitter the popular online service for microblogging and social networking.
|
||||
/// </summary>
|
||||
[Obsolete("Please use X instead, scheduled for removal in v16")]
|
||||
public class Twitter : OEmbedProviderBase
|
||||
{
|
||||
public Twitter(IJsonSerializer jsonSerializer)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Core.Media.EmbedProviders;
|
||||
|
||||
/// <summary>
|
||||
/// Embed Provider for X the popular online service for microblogging and social networking.
|
||||
/// </summary>
|
||||
public class X : OEmbedProviderBase
|
||||
{
|
||||
public X(IJsonSerializer jsonSerializer)
|
||||
: base(jsonSerializer)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ApiEndpoint => "http://publish.twitter.com/oembed";
|
||||
|
||||
public override string[] UrlSchemeRegex => new[] { @"(https?:\/\/(www\.)?)(twitter|x)\.com\/.*\/status\/.*" };
|
||||
|
||||
public override Dictionary<string, string> RequestParams => new();
|
||||
|
||||
public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0)
|
||||
{
|
||||
var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight);
|
||||
OEmbedResponse? oembed = base.GetJsonResponse<OEmbedResponse>(requestUrl);
|
||||
|
||||
return oembed?.GetHtml();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,21 @@ public sealed class AuditItem : EntityBase, IAuditItem
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuditItem" /> class.
|
||||
/// </summary>
|
||||
public AuditItem(int objectId, AuditType type, int userId, string? entityType, DateTime createDate, string? comment = null, string? parameters = null)
|
||||
{
|
||||
DisableChangeTracking();
|
||||
|
||||
Id = objectId;
|
||||
Comment = comment;
|
||||
AuditType = type;
|
||||
UserId = userId;
|
||||
EntityType = entityType;
|
||||
Parameters = parameters;
|
||||
CreateDate = createDate;
|
||||
|
||||
EnableChangeTracking();
|
||||
}
|
||||
|
||||
public AuditItem(int objectId, AuditType type, int userId, string? entityType, string? comment = null, string? parameters = null)
|
||||
{
|
||||
DisableChangeTracking();
|
||||
|
||||
@@ -194,6 +194,7 @@ public class ContentItemDisplay<TVariant> :
|
||||
/// This is not used for outgoing model information.
|
||||
/// </remarks>
|
||||
[IgnoreDataMember]
|
||||
[Obsolete("No longer used. Will be removed in V15.")]
|
||||
public ContentPropertyCollectionDto? ContentDto { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,69 +1,69 @@
|
||||
namespace Umbraco.Cms.Core.Models.ContentEditing;
|
||||
|
||||
/// <summary>
|
||||
/// The action associated with saving a content item
|
||||
/// The action associated with saving a content item.
|
||||
/// </summary>
|
||||
public enum ContentSaveAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves the content item, no publish
|
||||
/// Saves the content item, no publish.
|
||||
/// </summary>
|
||||
Save = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new content item
|
||||
/// Creates a new content item.
|
||||
/// </summary>
|
||||
SaveNew = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Saves and publishes the content item
|
||||
/// Saves and publishes the content item.
|
||||
/// </summary>
|
||||
Publish = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Creates and publishes a new content item
|
||||
/// Creates and publishes a new content item.
|
||||
/// </summary>
|
||||
PublishNew = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Saves and sends publish notification
|
||||
/// Saves and sends publish notification.
|
||||
/// </summary>
|
||||
SendPublish = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Creates and sends publish notification
|
||||
/// Creates and sends publish notification.
|
||||
/// </summary>
|
||||
SendPublishNew = 5,
|
||||
|
||||
/// <summary>
|
||||
/// Saves and schedules publishing
|
||||
/// Saves and schedules publishing.
|
||||
/// </summary>
|
||||
Schedule = 6,
|
||||
|
||||
/// <summary>
|
||||
/// Creates and schedules publishing
|
||||
/// Creates and schedules publishing.
|
||||
/// </summary>
|
||||
ScheduleNew = 7,
|
||||
|
||||
/// <summary>
|
||||
/// Saves and publishes the content item including all descendants that have a published version
|
||||
/// Saves and publishes the content item including all descendants that have a published version.
|
||||
/// </summary>
|
||||
PublishWithDescendants = 8,
|
||||
|
||||
/// <summary>
|
||||
/// Creates and publishes the content item including all descendants that have a published version
|
||||
/// Creates and publishes the new content item including all descendants that have a published version.
|
||||
/// </summary>
|
||||
PublishWithDescendantsNew = 9,
|
||||
|
||||
/// <summary>
|
||||
/// Saves and publishes the content item including all descendants regardless of whether they have a published version
|
||||
/// or not
|
||||
/// or not.
|
||||
/// </summary>
|
||||
PublishWithDescendantsForce = 10,
|
||||
|
||||
/// <summary>
|
||||
/// Creates and publishes the content item including all descendants regardless of whether they have a published
|
||||
/// version or not
|
||||
/// Creates and publishes the new content item including all descendants regardless of whether they have a published
|
||||
/// version or not.
|
||||
/// </summary>
|
||||
PublishWithDescendantsForceNew = 11,
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ public class PropertyTypeBasic
|
||||
|
||||
[Required]
|
||||
[RegularExpression(@"^([a-zA-Z]\w.*)$", ErrorMessage = "Invalid alias")]
|
||||
[MaxLength(255, ErrorMessage = "Alias is too long")]
|
||||
[DataMember(Name = "alias")]
|
||||
public string Alias { get; set; } = null!;
|
||||
|
||||
|
||||
@@ -132,6 +132,30 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects if any persisted property types have been removed by comparing old and new collections,
|
||||
/// and sets <see cref="HasPropertyTypeBeenRemoved"/> accordingly.
|
||||
/// </summary>
|
||||
private void DetectPropertyTypeRemovals(IEnumerable<IPropertyType> oldPropertyTypes, IEnumerable<IPropertyType> newPropertyTypes)
|
||||
{
|
||||
if (HasPropertyTypeBeenRemoved)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var oldIds = new HashSet<int>(oldPropertyTypes.Select(pt => pt.Id).Where(id => id > 0));
|
||||
if (oldIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newIds = new HashSet<int>(newPropertyTypes.Select(pt => pt.Id).Where(id => id > 0));
|
||||
if (oldIds.Any(id => !newIds.Contains(id)))
|
||||
{
|
||||
HasPropertyTypeBeenRemoved = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PropertyTypes that are not part of a PropertyGroup
|
||||
/// </summary>
|
||||
@@ -273,6 +297,7 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
|
||||
{
|
||||
if (PropertyTypeCollection != null)
|
||||
{
|
||||
DetectPropertyTypeRemovals(PropertyTypeCollection, value);
|
||||
PropertyTypeCollection.ClearCollectionChangedEvents();
|
||||
}
|
||||
|
||||
@@ -443,21 +468,21 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
|
||||
/// Please note that resetting the dirty properties could potentially
|
||||
/// obstruct the saving of a new or updated entity.
|
||||
/// </remarks>
|
||||
public override void ResetDirtyProperties()
|
||||
public override void ResetDirtyProperties(bool rememberDirty)
|
||||
{
|
||||
base.ResetDirtyProperties();
|
||||
base.ResetDirtyProperties(rememberDirty);
|
||||
|
||||
// loop through each property group to reset the property types
|
||||
var propertiesReset = new List<int>();
|
||||
|
||||
foreach (PropertyGroup propertyGroup in PropertyGroups)
|
||||
{
|
||||
propertyGroup.ResetDirtyProperties();
|
||||
propertyGroup.ResetDirtyProperties(rememberDirty);
|
||||
if (propertyGroup.PropertyTypes is not null)
|
||||
{
|
||||
foreach (IPropertyType propertyType in propertyGroup.PropertyTypes)
|
||||
{
|
||||
propertyType.ResetDirtyProperties();
|
||||
propertyType.ResetDirtyProperties(rememberDirty);
|
||||
propertiesReset.Add(propertyType.Id);
|
||||
}
|
||||
}
|
||||
@@ -467,7 +492,7 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
|
||||
// but don't re-reset ones we've already done.
|
||||
foreach (IPropertyType propertyType in PropertyTypes.Where(x => propertiesReset.Contains(x.Id) == false))
|
||||
{
|
||||
propertyType.ResetDirtyProperties();
|
||||
propertyType.ResetDirtyProperties(rememberDirty);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType
|
||||
{
|
||||
private List<IContentTypeComposition> _contentTypeComposition = new();
|
||||
private List<int> _removedContentTypeKeyTracker = new();
|
||||
private bool _hasCompositionBeenRemoved;
|
||||
|
||||
protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, int parentId)
|
||||
: base(shortStringHelper, parentId)
|
||||
@@ -104,6 +105,24 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A boolean flag indicating if a composition has been removed from this instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is currently (specifically) used in order to know that we need to refresh the content cache which
|
||||
/// needs to occur when a composition has been removed from a content type
|
||||
/// </remarks>
|
||||
[IgnoreDataMember]
|
||||
internal bool HasCompositionTypeBeenRemoved
|
||||
{
|
||||
get => _hasCompositionBeenRemoved;
|
||||
private set
|
||||
{
|
||||
_hasCompositionBeenRemoved = value;
|
||||
OnPropertyChanged(nameof(HasCompositionTypeBeenRemoved));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IPropertyType> GetOriginalComposedPropertyTypes() => GetRawComposedPropertyTypes();
|
||||
|
||||
@@ -179,6 +198,8 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType
|
||||
_removedContentTypeKeyTracker.AddRange(compositionIdsToRemove);
|
||||
}
|
||||
|
||||
HasCompositionTypeBeenRemoved = true;
|
||||
|
||||
OnPropertyChanged(nameof(ContentTypeComposition));
|
||||
|
||||
return _contentTypeComposition.Remove(contentTypeComposition);
|
||||
|
||||
@@ -10,5 +10,7 @@ public sealed class ApiContentRoute : IApiContentRoute
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public string? QueryString { get; set; }
|
||||
|
||||
public IApiContentStartItem StartItem { get; }
|
||||
}
|
||||
|
||||
@@ -4,5 +4,10 @@ public interface IApiContentRoute
|
||||
{
|
||||
string Path { get; }
|
||||
|
||||
public string? QueryString
|
||||
{
|
||||
get => null; set { }
|
||||
}
|
||||
|
||||
IApiContentStartItem StartItem { get; }
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ _additionalData ??= new Dictionary<string, object?>();
|
||||
// if the name has changed, add the value to the additional data,
|
||||
// this is required purely for event handlers to know the previous name of the group
|
||||
// so we can keep the public access up to date.
|
||||
AdditionalData["previousName"] = _name;
|
||||
AdditionalData[Constants.Entities.AdditionalDataKeys.MemberGroupPreviousName] = _name;
|
||||
}
|
||||
|
||||
SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user