Compare commits
358
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 | ||
|
|
35c51a029a | ||
|
|
8c1128c85b | ||
|
|
c9021ab2d2 | ||
|
|
67a71f8f82 | ||
|
|
edd0a4a4a9 | ||
|
|
8a22672c7f | ||
|
|
11270eaaf5 | ||
|
|
1e32d59ecb | ||
|
|
eab27123dd | ||
|
|
a6253957c8 | ||
|
|
a8f5631144 | ||
|
|
ee37ad0f4b | ||
|
|
2d71b5a63b | ||
|
|
c3db3457e7 | ||
|
|
7787af2df1 | ||
|
|
3730109cce | ||
|
|
9bab74d30e | ||
|
|
63a487bc0b | ||
|
|
a40eadcfce | ||
|
|
9b19d63a6a | ||
|
|
6939472f37 | ||
|
|
0387307301 | ||
|
|
25a5dde8e9 | ||
|
|
ff9903b14d | ||
|
|
d64bf5de22 | ||
|
|
9300d867fb | ||
|
|
b237285703 | ||
|
|
a74d963cfa | ||
|
|
5fe18bb78c | ||
|
|
b16dfa9ca1 | ||
|
|
3e6116fcba | ||
|
|
ef3bf496e9 | ||
|
|
bff293213d | ||
|
|
a47a1775f2 | ||
|
|
b2a2df7a8a | ||
|
|
34179f5b7d |
@@ -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>
|
||||
|
||||
+35
-29
@@ -12,28 +12,29 @@
|
||||
</ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.8" />
|
||||
<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.Data.Sqlite" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8" />
|
||||
<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.8" />
|
||||
<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.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<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,54 +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.3.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.3.0" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.64" />
|
||||
<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.7.1.1" />
|
||||
<PackageVersion Include="MailKit" Version="4.8.0" />
|
||||
<PackageVersion Include="Markdown" Version="2.2.1" />
|
||||
<PackageVersion Include="MessagePack" Version="2.5.187" />
|
||||
<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.2" />
|
||||
<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.2" />
|
||||
<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.5" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.3" />
|
||||
<PackageVersion Include="Smidge.InMemory" Version="4.4.0" />
|
||||
<PackageVersion Include="Smidge.Nuglify" Version="4.4.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.7.1" />
|
||||
<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.12.0" />
|
||||
<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.1" />
|
||||
<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}";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.9, 3)" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.11, 3)" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Web" VersionOverride="[2.0.2, 3)" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
+5
@@ -7,6 +7,11 @@
|
||||
<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 -->
|
||||
|
||||
+2
@@ -170,7 +170,9 @@ internal class SqlServerEFCoreDistributedLockingMechanism<T> : IDistributedLocki
|
||||
"A transaction with minimum ReadCommitted isolation level is required.");
|
||||
}
|
||||
|
||||
#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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
|
||||
<!-- 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" />
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
<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 -->
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("/");
|
||||
|
||||
@@ -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>());
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -20,6 +20,7 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
|
||||
|
||||
private string _alias;
|
||||
private string? _name;
|
||||
private bool _hasPropertyTypeBeenRemoved;
|
||||
private PropertyTypeCollection? _propertyTypes;
|
||||
private int _sortOrder;
|
||||
|
||||
@@ -94,6 +95,20 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A boolean flag indicating if a property type has been removed from this group.
|
||||
/// </summary>
|
||||
[IgnoreDataMember]
|
||||
internal bool HasPropertyTypeBeenRemoved
|
||||
{
|
||||
get => _hasPropertyTypeBeenRemoved;
|
||||
private set
|
||||
{
|
||||
_hasPropertyTypeBeenRemoved = value;
|
||||
OnPropertyChanged(nameof(HasPropertyTypeBeenRemoved));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of property types for the group.
|
||||
/// </summary>
|
||||
@@ -112,6 +127,7 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
|
||||
{
|
||||
if (_propertyTypes != null)
|
||||
{
|
||||
DetectPropertyTypeRemovals(_propertyTypes, value);
|
||||
_propertyTypes.ClearCollectionChangedEvents();
|
||||
}
|
||||
|
||||
@@ -155,4 +171,27 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
|
||||
|
||||
private void PropertyTypesChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
|
||||
OnPropertyChanged(nameof(PropertyTypes));
|
||||
|
||||
private void DetectPropertyTypeRemovals(PropertyTypeCollection oldPropertyTypes, PropertyTypeCollection? 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 = newPropertyTypes != null
|
||||
? new HashSet<int>(newPropertyTypes.Select(pt => pt.Id).Where(id => id > 0))
|
||||
: new HashSet<int>();
|
||||
|
||||
if (oldIds.Any(id => !newIds.Contains(id)))
|
||||
{
|
||||
HasPropertyTypeBeenRemoved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the options available with publishing a content branch for force publishing.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum PublishBranchFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// The default behavior is to publish only the published content that has changed.
|
||||
/// </summary>
|
||||
Default = 0,
|
||||
|
||||
/// <summary>
|
||||
/// For publishing a branch, publish all changed content, including content that is not published.
|
||||
/// </summary>
|
||||
IncludeUnpublished = 1,
|
||||
|
||||
/// <summary>
|
||||
/// For publishing a branch, force republishing of all published content, including content that has not changed.
|
||||
/// </summary>
|
||||
ForceRepublish = 2,
|
||||
|
||||
/// <summary>
|
||||
/// For publishing a branch, publish all content, including content that is not published and content that has not changed.
|
||||
/// </summary>
|
||||
All = IncludeUnpublished | ForceRepublish,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies options for publishing notifcations when saving.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum PublishNotificationSaveOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Do not publish any notifications.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Only publish the saving notification.
|
||||
/// </summary>
|
||||
Saving = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Only publish the saved notification.
|
||||
/// </summary>
|
||||
Saved = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Publish all the notifications.
|
||||
/// </summary>
|
||||
All = Saving | Saved,
|
||||
}
|
||||
@@ -30,7 +30,7 @@ public class PublishedValueFallback : IPublishedValueFallback
|
||||
/// <inheritdoc />
|
||||
public bool TryGetValue<T>(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value)
|
||||
{
|
||||
_variationContextAccessor.ContextualizeVariation(property.PropertyType.Variations, ref culture, ref segment);
|
||||
_variationContextAccessor.ContextualizeVariation(property.PropertyType.Variations, property.Alias, ref culture, ref segment);
|
||||
|
||||
foreach (var f in fallback)
|
||||
{
|
||||
@@ -78,7 +78,7 @@ public class PublishedValueFallback : IPublishedValueFallback
|
||||
return false;
|
||||
}
|
||||
|
||||
_variationContextAccessor.ContextualizeVariation(propertyType.Variations, ref culture, ref segment);
|
||||
_variationContextAccessor.ContextualizeVariation(propertyType.Variations, alias, ref culture, ref segment);
|
||||
|
||||
foreach (var f in fallback)
|
||||
{
|
||||
@@ -124,7 +124,7 @@ public class PublishedValueFallback : IPublishedValueFallback
|
||||
IPublishedPropertyType? propertyType = content.ContentType.GetPropertyType(alias);
|
||||
if (propertyType != null)
|
||||
{
|
||||
_variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment);
|
||||
_variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, alias, ref culture, ref segment);
|
||||
noValueProperty = content.GetProperty(alias);
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ public class PublishedValueFallback : IPublishedValueFallback
|
||||
{
|
||||
culture = null;
|
||||
segment = null;
|
||||
_variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment);
|
||||
_variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, alias, ref culture, ref segment);
|
||||
}
|
||||
|
||||
property = content?.GetProperty(alias);
|
||||
|
||||
@@ -25,9 +25,15 @@ public class VariationContext
|
||||
public string Segment { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the segment for the content item
|
||||
/// Gets the segment for the content item.
|
||||
/// </summary>
|
||||
/// <param name="contentId"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="contentId">The content Id.</param>
|
||||
public virtual string GetSegment(int contentId) => Segment;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the segment for the content item and property alias.
|
||||
/// </summary>
|
||||
/// <param name="contentId">The content Id.</param>
|
||||
/// <param name="propertyAlias">The property alias.</param>
|
||||
public virtual string GetSegment(int contentId, string propertyAlias) => Segment;
|
||||
}
|
||||
|
||||
@@ -8,25 +8,45 @@ namespace Umbraco.Extensions;
|
||||
|
||||
public static class VariationContextAccessorExtensions
|
||||
{
|
||||
[Obsolete("Please use the method overload that accepts all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public static void ContextualizeVariation(
|
||||
this IVariationContextAccessor variationContextAccessor,
|
||||
ContentVariation variations,
|
||||
ref string? culture,
|
||||
ref string? segment)
|
||||
=> variationContextAccessor.ContextualizeVariation(variations, null, ref culture, ref segment);
|
||||
=> variationContextAccessor.ContextualizeVariation(variations, null, null, ref culture, ref segment);
|
||||
|
||||
public static void ContextualizeVariation(
|
||||
this IVariationContextAccessor variationContextAccessor,
|
||||
ContentVariation variations,
|
||||
string? propertyAlias,
|
||||
ref string? culture,
|
||||
ref string? segment)
|
||||
=> variationContextAccessor.ContextualizeVariation(variations, null, propertyAlias, ref culture, ref segment);
|
||||
|
||||
[Obsolete("Please use the method overload that accepts all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public static void ContextualizeVariation(
|
||||
this IVariationContextAccessor variationContextAccessor,
|
||||
ContentVariation variations,
|
||||
int contentId,
|
||||
ref string? culture,
|
||||
ref string? segment)
|
||||
=> variationContextAccessor.ContextualizeVariation(variations, (int?)contentId, ref culture, ref segment);
|
||||
=> variationContextAccessor.ContextualizeVariation(variations, (int?)contentId, null, ref culture, ref segment);
|
||||
|
||||
public static void ContextualizeVariation(
|
||||
this IVariationContextAccessor variationContextAccessor,
|
||||
ContentVariation variations,
|
||||
int contentId,
|
||||
string? propertyAlias,
|
||||
ref string? culture,
|
||||
ref string? segment)
|
||||
=> variationContextAccessor.ContextualizeVariation(variations, (int?)contentId, propertyAlias, ref culture, ref segment);
|
||||
|
||||
private static void ContextualizeVariation(
|
||||
this IVariationContextAccessor variationContextAccessor,
|
||||
ContentVariation variations,
|
||||
int? contentId,
|
||||
string? propertyAlias,
|
||||
ref string? culture,
|
||||
ref string? segment)
|
||||
{
|
||||
@@ -37,18 +57,22 @@ public static class VariationContextAccessorExtensions
|
||||
|
||||
// use context values
|
||||
VariationContext? publishedVariationContext = variationContextAccessor?.VariationContext;
|
||||
if (culture == null)
|
||||
{
|
||||
culture = variations.VariesByCulture() ? publishedVariationContext?.Culture : string.Empty;
|
||||
}
|
||||
culture ??= variations.VariesByCulture() ? publishedVariationContext?.Culture : string.Empty;
|
||||
|
||||
if (segment == null)
|
||||
{
|
||||
if (variations.VariesBySegment())
|
||||
{
|
||||
segment = contentId == null
|
||||
? publishedVariationContext?.Segment
|
||||
: publishedVariationContext?.GetSegment(contentId.Value);
|
||||
if (contentId == null)
|
||||
{
|
||||
segment = publishedVariationContext?.Segment;
|
||||
}
|
||||
else
|
||||
{
|
||||
segment = propertyAlias == null ?
|
||||
publishedVariationContext?.GetSegment(contentId.Value) :
|
||||
publishedVariationContext?.GetSegment(contentId.Value, propertyAlias);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -32,4 +32,14 @@ public class ContentTreeChangeNotification : TreeChangeNotification<IContent>
|
||||
: base(new TreeChange<IContent>(target, changeTypes), messages)
|
||||
{
|
||||
}
|
||||
|
||||
public ContentTreeChangeNotification(
|
||||
IContent target,
|
||||
TreeChangeTypes changeTypes,
|
||||
IEnumerable<string>? publishedCultures,
|
||||
IEnumerable<string>? unpublishedCultures,
|
||||
EventMessages messages)
|
||||
: base(new TreeChange<IContent>(target, changeTypes, publishedCultures, unpublishedCultures), messages)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,29 @@ using Umbraco.Cms.Core.Security;
|
||||
namespace Umbraco.Cms.Core.Persistence.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for external logins with Guid as key, so it can be shared for members and users
|
||||
/// Repository for external logins with Guid as key, so it can be shared for members and users.
|
||||
/// </summary>
|
||||
public interface IExternalLoginWithKeyRepository : IReadWriteQueryRepository<int, IIdentityUserLogin>,
|
||||
IQueryRepository<IIdentityUserToken>
|
||||
{
|
||||
/// <summary>
|
||||
/// Replaces all external login providers for the user/member key
|
||||
/// Replaces all external login providers for the user/member key.
|
||||
/// </summary>
|
||||
void Save(Guid userOrMemberKey, IEnumerable<IExternalLogin> logins);
|
||||
|
||||
/// <summary>
|
||||
/// Replaces all external login provider tokens for the providers specified for the user/member key
|
||||
/// Replaces all external login provider tokens for the providers specified for the user/member key.
|
||||
/// </summary>
|
||||
void Save(Guid userOrMemberKey, IEnumerable<IExternalLoginToken> tokens);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all external logins for the specified the user/member key
|
||||
/// Deletes all external logins for the specified the user/member key.
|
||||
/// </summary>
|
||||
void DeleteUserLogins(Guid userOrMemberKey);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes external logins that aren't associated with the current collection of providers.
|
||||
/// </summary>
|
||||
/// <param name="currentLoginProviders">The names of the currently configured providers.</param>
|
||||
void DeleteUserLoginsForRemovedProviders(IEnumerable<string> currentLoginProviders) { }
|
||||
}
|
||||
|
||||
@@ -38,4 +38,11 @@ public interface IMemberRepository : IContentRepository<int, IMember>
|
||||
/// <param name="query"></param>
|
||||
/// <returns></returns>
|
||||
int GetCountByQuery(IQuery<IMember>? query);
|
||||
|
||||
/// <summary>
|
||||
/// Saves only the properties related to login for the member, using an optimized, non-locking update.
|
||||
/// </summary>
|
||||
/// <param name="member">The member to update.</param>
|
||||
/// <returns>Used to avoid the full save of the member object after a login operation.</returns>
|
||||
Task UpdateLoginPropertiesAsync(IMember member) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Linq.Expressions;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
|
||||
@@ -109,5 +109,26 @@ public interface IUserRepository : IReadWriteQueryRepository<int, IUser>
|
||||
|
||||
void ClearLoginSession(Guid sessionId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a page of users, ordered by Id and starting from the provided Id.
|
||||
/// </summary>
|
||||
/// <param name="id">The user Id to start retrieving users from.</param>
|
||||
/// <param name="count">The number of users to return.</param>
|
||||
/// <returns>A page of <see cref="IUser"/> instances.</returns>
|
||||
[Obsolete("No longer used in Umbraco. Scheduled for removal in Umbraco 18.")]
|
||||
IEnumerable<IUser> GetNextUsers(int id, int count);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a page of approved users, ordered by Id and starting from the provided Id.
|
||||
/// </summary>
|
||||
/// <param name="id">The user Id to start retrieving users from.</param>
|
||||
/// <param name="count">The number of users to return.</param>
|
||||
/// <returns>A page of <see cref="IUser"/> instances.</returns>
|
||||
IEnumerable<IUser> GetNextApprovedUsers(int id, int count) => Enumerable.Empty<IUser>();
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates sessions for users that aren't associated with the current collection of providers.
|
||||
/// </summary>
|
||||
/// <param name="currentProviderKeys">The keys for the currently configured providers.</param>
|
||||
void InvalidateSessionsForRemovedProviders(IEnumerable<string> currentProviderKeys) { }
|
||||
}
|
||||
|
||||
@@ -31,6 +31,12 @@ public interface IPublishedSnapshotService : IDisposable
|
||||
/// </remarks>
|
||||
IPublishedSnapshot CreatePublishedSnapshot(string? previewToken);
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the database cache is in the process of being rebuilt.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool IsRebuilding() => false;
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds internal database caches (but does not reload).
|
||||
/// </summary>
|
||||
@@ -61,6 +67,38 @@ public interface IPublishedSnapshotService : IDisposable
|
||||
IReadOnlyCollection<int>? mediaTypeIds = null,
|
||||
IReadOnlyCollection<int>? memberTypeIds = null);
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds internal database caches (but does not reload).
|
||||
/// </summary>
|
||||
/// <param name="contentTypeIds">
|
||||
/// If not null will process content for the matching content types, if empty will process all
|
||||
/// content
|
||||
/// </param>
|
||||
/// <param name="mediaTypeIds">
|
||||
/// If not null will process content for the matching media types, if empty will process all
|
||||
/// media
|
||||
/// </param>
|
||||
/// <param name="memberTypeIds">
|
||||
/// If not null will process content for the matching members types, if empty will process all
|
||||
/// members
|
||||
/// </param>
|
||||
/// <param name="useBackgroundThread">Flag indicating whether to use a background thread for the operation and immediately return to the caller.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Forces the snapshot service to rebuild its internal database caches. For instance, some caches
|
||||
/// may rely on a database table to store pre-serialized version of documents.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This does *not* reload the caches. Caches need to be reloaded, for instance via
|
||||
/// <see cref="DistributedCache" /> RefreshAllPublishedSnapshot method.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
void Rebuild(
|
||||
bool useBackgroundThread,
|
||||
IReadOnlyCollection<int>? contentTypeIds = null,
|
||||
IReadOnlyCollection<int>? mediaTypeIds = null,
|
||||
IReadOnlyCollection<int>? memberTypeIds = null) => Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds all internal database caches (but does not reload).
|
||||
@@ -77,6 +115,22 @@ public interface IPublishedSnapshotService : IDisposable
|
||||
/// </remarks>
|
||||
void RebuildAll() => Rebuild(Array.Empty<int>(), Array.Empty<int>(), Array.Empty<int>());
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds all internal database caches (but does not reload).
|
||||
/// </summary>
|
||||
/// <param name="useBackgroundThread">Flag indicating whether to use a background thread for the operation and immediately return to the caller.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Forces the snapshot service to rebuild its internal database caches. For instance, some caches
|
||||
/// may rely on a database table to store pre-serialized version of documents.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This does *not* reload the caches. Caches need to be reloaded, for instance via
|
||||
/// <see cref="DistributedCache" /> RefreshAllPublishedSnapshot method.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
void RebuildAll(bool useBackgroundThread) => Rebuild(useBackgroundThread, Array.Empty<int>(), Array.Empty<int>(), Array.Empty<int>());
|
||||
|
||||
/* An IPublishedCachesService implementation can rely on transaction-level events to update
|
||||
* its internal, database-level data, as these events are purely internal. However, it cannot
|
||||
* rely on cache refreshers CacheUpdated events to update itself, as these events are external
|
||||
|
||||
@@ -33,7 +33,7 @@ public interface ITagQuery
|
||||
/// <summary>
|
||||
/// Gets all document tags.
|
||||
/// </summary>
|
||||
/// /// <remarks>
|
||||
/// <remarks>
|
||||
/// If no culture is specified, it retrieves tags with an invariant culture.
|
||||
/// If a culture is specified, it only retrieves tags for that culture.
|
||||
/// Use "*" to retrieve tags for all cultures.
|
||||
|
||||
@@ -148,7 +148,10 @@ public class UmbracoRequestPaths
|
||||
/// <summary>
|
||||
/// Checks if the current uri is an install request
|
||||
/// </summary>
|
||||
public bool IsInstallerRequest(string absPath) => absPath.InvariantStartsWith(_installPath);
|
||||
public bool IsInstallerRequest(string absPath) =>
|
||||
absPath.InvariantEquals(_installPath)
|
||||
|| absPath.InvariantStartsWith(_installPath.EnsureEndsWith('/'))
|
||||
|| absPath.InvariantStartsWith(_installPath.EnsureEndsWith('?'));
|
||||
|
||||
/// <summary>
|
||||
/// Rudimentary check to see if it's not a server side request
|
||||
|
||||
@@ -111,6 +111,12 @@ public sealed class UriUtility
|
||||
if (path != "/")
|
||||
{
|
||||
path = path.TrimEnd(Constants.CharArrays.ForwardSlash);
|
||||
|
||||
// perform fallback to root if the path was all slashes (i.e. https://some.where//////)
|
||||
if (path == string.Empty)
|
||||
{
|
||||
path = "/";
|
||||
}
|
||||
}
|
||||
|
||||
return uri.Rewrite(path);
|
||||
|
||||
@@ -250,6 +250,8 @@ public class CoreScope : ICoreScope
|
||||
_parentScope = coreScope;
|
||||
}
|
||||
|
||||
protected bool HasParentScope => _parentScope is not null;
|
||||
|
||||
protected void HandleScopedNotifications() => _notificationPublisher?.ScopeExit(Completed.HasValue && Completed.Value);
|
||||
|
||||
private void EnsureNotDisposed()
|
||||
|
||||
@@ -18,4 +18,20 @@ public static class ContentTypeChangeExtensions
|
||||
|
||||
public static bool HasTypesNone(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) =>
|
||||
(change & types) == ContentTypeChangeTypes.None;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the change has structural change impact.
|
||||
/// </summary>
|
||||
/// <param name="change">The change to check.</param>
|
||||
/// <returns><c>true</c> if the change has structural impact; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsStructuralChange(this ContentTypeChangeTypes change) =>
|
||||
change.HasType(ContentTypeChangeTypes.RefreshMain);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the change has non-structural change impact.
|
||||
/// </summary>
|
||||
/// <param name="change">The change to check.</param>
|
||||
/// <returns><c>true</c> if the change has non-structural impact; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsNonStructuralChange(this ContentTypeChangeTypes change) =>
|
||||
change.HasType(ContentTypeChangeTypes.RefreshOther) && !change.HasType(ContentTypeChangeTypes.RefreshMain);
|
||||
}
|
||||
|
||||
@@ -11,14 +11,29 @@ public enum ContentTypeChangeTypes : byte
|
||||
Create = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Content type changes impact only the Content type being saved
|
||||
/// Content type changes directly impact existing content of this content type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These changes are "destructive" of nature. They include:
|
||||
/// - Changing the content type alias.
|
||||
/// - Removing a property type or a composition.
|
||||
/// - Changing the alias of a property type (this effectively corresponds to removing a property type).
|
||||
/// - Changing variance, either at property or content type level.
|
||||
/// </remarks>
|
||||
RefreshMain = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Content type changes impacts the content type being saved and others used that are composed of it
|
||||
/// Content type changes that do not directly impact existing content of this content type.
|
||||
/// </summary>
|
||||
RefreshOther = 4, // changed, other change
|
||||
/// <remarks>
|
||||
/// These changes are "constructive" of nature, and include all changes not included in
|
||||
/// <see cref="RefreshMain"/> - for example:
|
||||
/// - Adding a property type or a composition.
|
||||
/// - Rearranging property types or groups.
|
||||
/// - Changes to name, description, icon etc.
|
||||
/// - Changes to other content type settings, i.e. allowed child types and version cleanup.
|
||||
/// </remarks>
|
||||
RefreshOther = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Content type was removed
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user