Compare commits
248
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42ae5f1ac9 | ||
|
|
5260aab5c8 | ||
|
|
3d47dda810 | ||
|
|
ef9a6e1821 | ||
|
|
b4528cf963 | ||
|
|
cb1ec988ce | ||
|
|
8caf4ae3e0 | ||
|
|
97ded97fc7 | ||
|
|
b48d989659 | ||
|
|
297ceaf0aa | ||
|
|
1e99d6f609 | ||
|
|
b8c2a3366f | ||
|
|
6d330a30eb | ||
|
|
607207b13b | ||
|
|
1dd5fe3cbc | ||
|
|
275478cd93 | ||
|
|
80bf2314c6 | ||
|
|
8342d4b94f | ||
|
|
148935a3fc | ||
|
|
60eaef5590 | ||
|
|
3dd32642e2 | ||
|
|
b69ab3ad78 | ||
|
|
513f980e5c | ||
|
|
024a450377 | ||
|
|
9246ecbb08 | ||
|
|
5adc4702a8 | ||
|
|
5b54e8e469 | ||
|
|
5b05094fa2 | ||
|
|
af004230b2 | ||
|
|
a917858265 | ||
|
|
b330395db0 | ||
|
|
2cf28271cd | ||
|
|
a3848077b4 | ||
|
|
3fc1c3afee | ||
|
|
f181c2f82b | ||
|
|
53b94a8f57 | ||
|
|
bdb7baf946 | ||
|
|
b898eb6e03 | ||
|
|
3187d99d23 | ||
|
|
7c98af558d | ||
|
|
1bddcb8d39 | ||
|
|
e4424ff0b5 | ||
|
|
339c852ec2 | ||
|
|
c0bd937216 | ||
|
|
7062042cbc | ||
|
|
af0abf252a | ||
|
|
d9a0f3af02 | ||
|
|
bb3b8b2255 | ||
|
|
60af1e88f0 | ||
|
|
0ea16b41b8 | ||
|
|
ba33de49e7 | ||
|
|
ac81973e3b | ||
|
|
85b58f2015 | ||
|
|
8fe9599e91 | ||
|
|
e017167f57 | ||
|
|
79ea5c12c1 | ||
|
|
d568e981ab | ||
|
|
808e56beec | ||
|
|
c171882d55 | ||
|
|
85cba7e8cb | ||
|
|
dcf601b44e | ||
|
|
20a3e08ae0 | ||
|
|
049fad3261 | ||
|
|
ede48c1b8b | ||
|
|
4114342de2 | ||
|
|
1c93d1e6e1 | ||
|
|
3c6cb488d7 | ||
|
|
33ed2482d8 | ||
|
|
056656042b | ||
|
|
e3eec6ce1f | ||
|
|
5b76d3bc04 | ||
|
|
18a8e3bde5 | ||
|
|
8849647dcd | ||
|
|
fcddba5c73 | ||
|
|
64f1e17479 | ||
|
|
4a768b694e | ||
|
|
c42237ecfa | ||
|
|
37c9d20d60 | ||
|
|
f2a22bc51b | ||
|
|
3baf6faed4 | ||
|
|
a4453243e6 | ||
|
|
a24058a0ab | ||
|
|
548b14610c | ||
|
|
bca39401ac | ||
|
|
b072a4ab60 | ||
|
|
fca17b87da | ||
|
|
b794392df8 | ||
|
|
09d1615e60 | ||
|
|
7d4d8c9a79 | ||
|
|
78877bea57 | ||
|
|
8ef061b768 | ||
|
|
3521549ff3 | ||
|
|
39046c35af | ||
|
|
b4f26a8ffe | ||
|
|
bdabaa80d3 | ||
|
|
1f63e7d41b | ||
|
|
366e8d6809 | ||
|
|
bcafffdbf0 | ||
|
|
f725ea8019 | ||
|
|
548970a27f | ||
|
|
6c6e1f7336 | ||
|
|
1dc9f8540d | ||
|
|
6987515cbe | ||
|
|
8e87f2260c | ||
|
|
2fc5880d7d | ||
|
|
927e4d8cad | ||
|
|
fb39711571 | ||
|
|
225f4d3e09 | ||
|
|
a290601e5e | ||
|
|
0b93561a06 | ||
|
|
01ea259d74 | ||
|
|
141a4a4678 | ||
|
|
74a9b8aae7 | ||
|
|
c38faec74b | ||
|
|
c86a6fa8e5 | ||
|
|
f69f257d12 | ||
|
|
b1795da0ee | ||
|
|
38af101605 | ||
|
|
aa89eed3aa | ||
|
|
bb41647d91 | ||
|
|
235a951f31 | ||
|
|
804995e59e | ||
|
|
5544823b97 | ||
|
|
b9cf156e5a | ||
|
|
2fcf1c7812 | ||
|
|
a9043a543e | ||
|
|
cd83cb2852 | ||
|
|
b2ab769edc | ||
|
|
59cad092ae | ||
|
|
70bdabf9d6 | ||
|
|
3030006e4f | ||
|
|
1e521760df | ||
|
|
1ccb5cc09e | ||
|
|
4561394283 | ||
|
|
6038428f79 | ||
|
|
d52d3aa642 | ||
|
|
20d763f443 | ||
|
|
c63ed69ee2 | ||
|
|
12ca71558a | ||
|
|
8b759cf3a4 | ||
|
|
25fffd4173 | ||
|
|
29c0151460 | ||
|
|
2b6f9fb9c4 | ||
|
|
0eac3952a6 | ||
|
|
b87e8d90e4 | ||
|
|
889bd0b6e4 | ||
|
|
6033daff3b | ||
|
|
4cec19625f | ||
|
|
cc25027e65 | ||
|
|
f91c0568d1 | ||
|
|
d391cff254 | ||
|
|
63485bca15 | ||
|
|
64915eea92 | ||
|
|
fbdf69e680 | ||
|
|
13109d7342 | ||
|
|
e988c5a450 | ||
|
|
eddf06e773 | ||
|
|
761c76143d | ||
|
|
96c0509719 | ||
|
|
fcc8e2fe3a | ||
|
|
dab3975d2e | ||
|
|
beb60065bb | ||
|
|
cc9c33bfe6 | ||
|
|
3cd3abe5c8 | ||
|
|
6edffd9f08 | ||
|
|
e6ab1d9a6b | ||
|
|
37e25c3b2b | ||
|
|
bfa9cab1cf | ||
|
|
4087f3fab7 | ||
|
|
e9b0486988 | ||
|
|
7e57396960 | ||
|
|
981f173a79 | ||
|
|
83fe87ca88 | ||
|
|
432dda8c47 | ||
|
|
ae078bf96b | ||
|
|
6455f96e51 | ||
|
|
906285120e | ||
|
|
a9fc88d7e6 | ||
|
|
76e2103551 | ||
|
|
340ae6c474 | ||
|
|
7495c3c7b2 | ||
|
|
449c94f08d | ||
|
|
83b92666ac | ||
|
|
b7bf30744a | ||
|
|
9e97f6028b | ||
|
|
f9496e8067 | ||
|
|
8ba3a4d926 | ||
|
|
7fe6971a61 | ||
|
|
7b10d39d66 | ||
|
|
9ef2b37be4 | ||
|
|
516d62a348 | ||
|
|
97b3023e14 | ||
|
|
250ddf319e | ||
|
|
015c0cfc16 | ||
|
|
53be19cb00 | ||
|
|
3a93c9b9a4 | ||
|
|
ff39e54ec6 | ||
|
|
5a6d7326d8 | ||
|
|
c8e9beffa1 | ||
|
|
e064169df5 | ||
|
|
d87415099c | ||
|
|
34cfa3cb62 | ||
|
|
9e9b1efdf3 | ||
|
|
f309117223 | ||
|
|
0ab024d7c1 | ||
|
|
302b58e513 | ||
|
|
8d9822ea70 | ||
|
|
15f0f44ff5 | ||
|
|
043b39d553 | ||
|
|
2ff11afa95 | ||
|
|
e61541a2cf | ||
|
|
d6b40ce526 | ||
|
|
f63794737a | ||
|
|
9e5c7977da | ||
|
|
64eb3cdf57 | ||
|
|
6b60ff8ade | ||
|
|
e1e69aeec4 | ||
|
|
746ad55a0b | ||
|
|
cb33afba3b | ||
|
|
98fd90409e | ||
|
|
e8ba63bad4 | ||
|
|
5557ec4cd8 | ||
|
|
dbf0e740d8 | ||
|
|
7a21cf5617 | ||
|
|
c21d160f52 | ||
|
|
b873288015 | ||
|
|
2264ce8678 | ||
|
|
f296d5d2b4 | ||
|
|
bc9cc83eca | ||
|
|
17c4a08ae1 | ||
|
|
f1020436d1 | ||
|
|
b21183c562 | ||
|
|
6d974a624d | ||
|
|
6ba4b36514 | ||
|
|
30f1ec194e | ||
|
|
2801df3c6f | ||
|
|
666f9457f3 | ||
|
|
6e42c53d79 | ||
|
|
7e713cab84 | ||
|
|
0ebe465e6f | ||
|
|
d583169642 | ||
|
|
721e6ccb0c | ||
|
|
45ff3c3a6f | ||
|
|
21a23b8057 | ||
|
|
04d8b29226 | ||
|
|
f76d093226 | ||
|
|
b3055fa5ce | ||
|
|
7f089a630a |
@@ -61,7 +61,7 @@ This guide describes each step to make your first contribution:
|
||||
## Further contribution guides
|
||||
|
||||
- [Before you start](contributing-before-you-start.md)
|
||||
- [Finding your first issue: Up for grabs](contributing-before-you-start.md)
|
||||
- [Finding your first issue: Up for grabs](contributing-first-issue.md)
|
||||
- [Contributing to the new backoffice](https://docs.umbraco.com/umbraco-backoffice/)
|
||||
- [Unwanted changes](contributing-unwanted-changes.md)
|
||||
- [Other ways to contribute](contributing-other-ways-to-contribute.md)
|
||||
|
||||
+45
-46
@@ -5,99 +5,98 @@
|
||||
</PropertyGroup>
|
||||
<!-- Global packages (private, build-time packages for all projects) -->
|
||||
<ItemGroup>
|
||||
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.6.146" />
|
||||
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.7.115" />
|
||||
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<GlobalPackageReference Include="Umbraco.Code" Version="2.2.0" />
|
||||
<GlobalPackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.2.0" />
|
||||
</ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0-preview.9.24556.5" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" /> <!-- TODO: Update the hard-dependency that Umbraco.Code has on 4.10.0 to allow update of this to latest (4.13.0). -->
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.4.0" />
|
||||
</ItemGroup>
|
||||
<!-- Umbraco packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Umbraco.JsonSchema.Extensions" Version="0.3.0" />
|
||||
<PackageVersion Include="Umbraco.CSharpTest.Net.Collections" Version="15.0.0" />
|
||||
</ItemGroup>
|
||||
<!-- Third-party packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
|
||||
<PackageVersion Include="Examine" Version="3.7.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.7.0" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.74" />
|
||||
<PackageVersion Include="JsonPatch.Net" Version="3.1.1" />
|
||||
<PackageVersion Include="Examine" Version="3.7.1" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.7.1" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.12.1" />
|
||||
<PackageVersion Include="JsonPatch.Net" Version="3.3.0" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
|
||||
<PackageVersion Include="MailKit" Version="4.10.0" />
|
||||
<PackageVersion Include="MailKit" Version="4.11.0" />
|
||||
<PackageVersion Include="Markdown" Version="2.2.1" />
|
||||
<PackageVersion Include="MessagePack" Version="2.5.192" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.3" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
|
||||
<PackageVersion Include="MiniProfiler.Shared" Version="4.5.4" />
|
||||
<PackageVersion Include="ncrontab" Version="3.3.3" />
|
||||
<PackageVersion Include="NPoco" Version="5.7.1" />
|
||||
<PackageVersion Include="NPoco.SqlServer" Version="5.7.1" />
|
||||
<PackageVersion Include="OpenIddict.Abstractions" Version="6.1.1" />
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="6.1.1" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="6.1.1" />
|
||||
<PackageVersion Include="OpenIddict.Abstractions" Version="6.2.1" />
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="6.2.1" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="6.2.1" />
|
||||
<PackageVersion Include="Serilog" Version="4.2.0" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Process" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.4" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Map" Version="2.0.0" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.3" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.1.0" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.4" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
||||
</ItemGroup>
|
||||
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
|
||||
<ItemGroup>
|
||||
<!-- Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer brings in a vulnerable version of Azure.Identity -->
|
||||
<!-- Take top-level depedendency on Azure.Identity, because Microsoft.EntityFrameworkCore.SqlServer depends on a vulnerable version -->
|
||||
<PackageVersion Include="Azure.Identity" Version="1.13.1" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.13.2" />
|
||||
<!-- Microsoft.EntityFrameworkCore.SqlServer brings in a vulnerable version of System.Runtime.Caching -->
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.0" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.4" />
|
||||
<!-- 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="9.0.0" />
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="9.0.4" />
|
||||
<!-- Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc brings in a vulnerable version of System.Text.RegularExpressions -->
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<!-- OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer brings in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
|
||||
<!-- Take top-level depedendency on Microsoft.IdentityModel.JsonWebTokens, because OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer depends on a vulnerable version -->
|
||||
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.2.1" />
|
||||
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.8.0" />
|
||||
<!-- Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer and Dazinator.Extensions.FileProviders brings in a legacy version of System.Text.Encodings.Web -->
|
||||
<PackageVersion Include="System.Text.Encodings.Web" Version="9.0.0" />
|
||||
<PackageVersion Include="System.Text.Encodings.Web" Version="9.0.4" />
|
||||
<!-- NPoco.SqlServer brings in a vulnerable version of Microsoft.Data.SqlClient -->
|
||||
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.0.1" />
|
||||
<!-- Examine.Lucene brings in a vulnerable version of Lucene.Net.Replicator -->
|
||||
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
+571
@@ -0,0 +1,571 @@
|
||||
Third-Party Notices
|
||||
===================
|
||||
|
||||
This file contains notices and attributions for third-party software used in the Umbraco CMS project.
|
||||
|
||||
It is not a license and does not grant any rights to use the third-party software.
|
||||
|
||||
Umbraco CMS is licensed under the MIT License, which can be found in the LICENSE file.
|
||||
|
||||
---
|
||||
|
||||
@openid/AppAuth-JS: An OpenID Connect and OAuth 2.0 client library for JavaScript
|
||||
|
||||
URL: https://github.com/openid/AppAuth-JS
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: 2017 Google Inc.
|
||||
|
||||
---
|
||||
|
||||
AutoFixture: Write maintainable unit tests, faster
|
||||
|
||||
URL: https://github.com/AutoFixture/AutoFixture
|
||||
License: MIT License
|
||||
Copyright: 2013 Mark Seemann
|
||||
|
||||
---
|
||||
|
||||
Asp.Versioning.Mvc: A library for ASP.NET Core versioning
|
||||
|
||||
URL: https://github.com/dotnet/aspnet-api-versioning
|
||||
License: MIT License
|
||||
Copyright: .NET Foundation and contributors
|
||||
|
||||
---
|
||||
|
||||
Babel: A JavaScript compiler
|
||||
|
||||
URL: https://babeljs.io/
|
||||
License: MIT License
|
||||
Copyright: 2014-present Sebastian McKenzie and other contributors
|
||||
|
||||
---
|
||||
|
||||
BenchmarkDotNet: Powerful .NET library for benchmarking
|
||||
|
||||
URL: https://github.com/dotnet/BenchmarkDotNet
|
||||
License: MIT License
|
||||
Copyright: .NET Foundation and Contributors
|
||||
|
||||
---
|
||||
|
||||
Bogus: A simple and sane data generator for populating objects that supports different locales.
|
||||
|
||||
URL: https://github.com/bchavez/Bogus
|
||||
License: MIT License
|
||||
Copyright: 2015 Brian Chavez
|
||||
|
||||
---
|
||||
|
||||
CommandLineParser: Terse syntax C# command line parser for .NET
|
||||
|
||||
URL: https://github.com/commandlineparser/commandline
|
||||
License: MIT License
|
||||
Copyright: 2005-2015 Giacomo Stelluti Scala & Contributors
|
||||
|
||||
---
|
||||
|
||||
cross-env: A CLI tool to set environment variables across platforms
|
||||
|
||||
URL: https://github.com/kentcdodds/cross-env
|
||||
License: MIT License
|
||||
Copyright: 2017 Kent C. Dodds
|
||||
|
||||
---
|
||||
|
||||
Dazinator.Extensions.FileProviders: A library for file provider extensions
|
||||
|
||||
URL: https://github.com/dazinator/Dazinator.Extensions.FileProviders
|
||||
License: MIT License
|
||||
Copyright: 2016 Darrell
|
||||
|
||||
---
|
||||
|
||||
DOMPurify: A DOM-only XSS sanitizer for HTML, MathML and SVG
|
||||
|
||||
URL: https://github.com/cure53/DOMPurify
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: 2025 Dr.-Ing. Mario Heiderich, Cure53
|
||||
|
||||
---
|
||||
|
||||
Element Internals Polyfill: A polyfill for the Element Internals API
|
||||
|
||||
URL: https://github.com/calebdwilliams/element-internals-polyfill
|
||||
License: MIT License
|
||||
Copyright: 2021 Caleb Williams
|
||||
|
||||
---
|
||||
|
||||
Eslint: A tool for identifying and reporting on patterns in JavaScript
|
||||
|
||||
URL: https://eslint.org/
|
||||
License: MIT License
|
||||
Copyright: OpenJS Foundation and other contributors
|
||||
|
||||
---
|
||||
|
||||
Examine: A search and indexing library for .NET
|
||||
|
||||
URL: https://github.com/Shazwazza/Examine
|
||||
License: Microsoft Public License (Ms-PL)
|
||||
Copyright: 2023 Shannon Deminick
|
||||
|
||||
---
|
||||
|
||||
Glob: A library for matching file paths using glob patterns
|
||||
|
||||
URL: https://github.com/isaacs/node-glob
|
||||
License: ISC License
|
||||
Copyright: 2009-2023 Isaac Z. Schlueter and Contributors
|
||||
|
||||
---
|
||||
|
||||
Globals: A library for managing global variables in JavaScript
|
||||
|
||||
URL: https://github.com/sindresorhus/globals
|
||||
License: MIT License
|
||||
Copyright: Sindre Sorhus
|
||||
|
||||
---
|
||||
|
||||
Html Agility Pack: An HTML parser for .NET
|
||||
|
||||
URL: https://html-agility-pack.net/
|
||||
License: MIT License
|
||||
Copyright: ZZZ Projects Inc.
|
||||
|
||||
---
|
||||
|
||||
ImageSharp: A cross-platform library for processing images in .NET
|
||||
|
||||
URL: https://github.com/SixLabors/ImageSharp
|
||||
License: Apache License, Version 2.0 under the Six Labors Split License
|
||||
Copyright: Six Labors
|
||||
|
||||
---
|
||||
|
||||
jsdiff: A JavaScript text differencing implementation
|
||||
|
||||
URL: https://github.com/kpdecker/jsdiff
|
||||
License: BSD 3-Clause License
|
||||
Copyright: 2009-2015 Kevin Decker <kpdecker@gmail.com>
|
||||
|
||||
---
|
||||
|
||||
JsonPatch.Net: A library for JSON Patch (RFC 6902) in .NET
|
||||
|
||||
URL: https://github.com/json-everything/json-everything
|
||||
License: MIT License
|
||||
Copyright: .NET Foundation and Contributors
|
||||
|
||||
---
|
||||
|
||||
K4os.Compression.LZ4: A fast LZ4 compression library for .NET
|
||||
|
||||
URL: https://github.com/MiloszKrajewski/K4os.Compression.LZ4
|
||||
License: MIT License
|
||||
Copyright: 2017 Milosz Krajewski
|
||||
|
||||
---
|
||||
|
||||
Lit: A simple library for building fast, lightweight web components
|
||||
|
||||
URL: https://lit.dev
|
||||
License: BSD 3-Clause License
|
||||
Copyright: 2020 Google LLC. All rights reserved.
|
||||
|
||||
---
|
||||
|
||||
Lucide: Beautiful & consistent icons for the web
|
||||
|
||||
URL: https://lucide.dev/
|
||||
License: ISC License
|
||||
Copyright: 2013-2022 Cole Bemis
|
||||
Copyright: 2022 Lucide Contributors
|
||||
|
||||
---
|
||||
|
||||
Madge: A dependency graph generator for JavaScript
|
||||
|
||||
URL: https://github.com/pahen/madge
|
||||
License: MIT License
|
||||
Copyright: 2017 Patrik Henningsson
|
||||
|
||||
---
|
||||
|
||||
MailKit: A library for sending email in .NET
|
||||
|
||||
URL: https://github.com/jstedfast/MailKit
|
||||
License: MIT License
|
||||
Copyright: 2013-2024 .NET Foundation and Contributors
|
||||
|
||||
---
|
||||
|
||||
Markdown: A library for parsing and compiling Markdown
|
||||
|
||||
URL: https://github.com/hey-red/Markdown
|
||||
License: MIT License
|
||||
Copyright: 2018 red
|
||||
|
||||
---
|
||||
|
||||
marked: A markdown parser and compiler
|
||||
|
||||
URL: https://marked.js.org/
|
||||
License: MIT License
|
||||
Copyright: 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
|
||||
Copyright: 2018+, MarkedJS (https://github.com/markedjs/)
|
||||
|
||||
---
|
||||
|
||||
Message Pack: The extremely fast MessagePack serializer for C#
|
||||
|
||||
URL: https://github.com/MessagePack-CSharp/MessagePack-CSharp
|
||||
License: MIT License
|
||||
Copyright: 2017 Yoshifumi Kawai and contributors
|
||||
|
||||
---
|
||||
|
||||
Miniprofiler: A mini profiler for .NET
|
||||
|
||||
URL: https://github.com/MiniProfiler/dotnet
|
||||
License: MIT License
|
||||
Copyright: .NET MiniProfiler Contributors
|
||||
|
||||
---
|
||||
|
||||
Monaco Editor: A browser-based code editor
|
||||
|
||||
URL: https://microsoft.github.io/monaco-editor/
|
||||
License: MIT License
|
||||
Copyright: 2016-present Microsoft Corporation
|
||||
|
||||
---
|
||||
|
||||
Moq: A mocking library for .NET
|
||||
|
||||
URL: https://github.com/moq/moq
|
||||
License: BSD 3-Clause License
|
||||
Copyright: 2007 Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors.
|
||||
|
||||
---
|
||||
|
||||
Mock Service Worker (MSW): A library for mocking API requests in JavaScript
|
||||
|
||||
URL: https://mswjs.io/
|
||||
License: MIT License
|
||||
Copyright: 2018–present Artem Zakharchenko
|
||||
|
||||
---
|
||||
|
||||
NCrontab: A cron schedule parser for .NET
|
||||
|
||||
URL: https://github.com/atifaziz/NCrontab
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: 2001 The OpenSymphony Group
|
||||
Copyright: 2008 Atif Aziz
|
||||
|
||||
---
|
||||
|
||||
Nerdbank.GitVersioning: A library for versioning .NET projects
|
||||
|
||||
URL: https://github.com/dotnet/Nerdbank.GitVersioning
|
||||
License: MIT License
|
||||
Copyright: .NET Foundation and Contributors
|
||||
|
||||
---
|
||||
|
||||
NJsonSchema: A JSON schema validator for .NET
|
||||
|
||||
URL: https://github.com/RicoSuter/NJsonSchema
|
||||
License: MIT License
|
||||
Copyright: 2022 Rico Suter
|
||||
|
||||
---
|
||||
|
||||
NPoco: A micro ORM for .NET
|
||||
|
||||
URL: https://github.com/schotime/NPoco
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: Schotime
|
||||
|
||||
---
|
||||
|
||||
NUnit: A unit testing framework for .NET
|
||||
|
||||
URL: https://github.com/nunit/nunit
|
||||
License: MIT License
|
||||
Copyright: Charlie Poole, Rob Prouse and Contributors
|
||||
|
||||
---
|
||||
|
||||
Open Web Components: A set of standards and libraries for building web components
|
||||
|
||||
URL: https://open-wc.org/
|
||||
License: MIT License
|
||||
Copyright: 2018 open-wc
|
||||
|
||||
---
|
||||
|
||||
Openapi-ts: The OpenAPI to TypeScript codegen
|
||||
|
||||
URL: https://github.com/hey-api/openapi-ts
|
||||
License: MIT License
|
||||
Copyright: Hey API
|
||||
|
||||
---
|
||||
|
||||
OpenIddict: A simple and flexible OpenID Connect server for ASP.NET Core
|
||||
|
||||
URL: https://github.com/openiddict/openiddict-core
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: Kévin Chalet
|
||||
|
||||
---
|
||||
|
||||
Playwright: A Node.js library to automate browser testing
|
||||
|
||||
URL: https://playwright.dev/
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: 2025 Microsoft Corporation
|
||||
|
||||
---
|
||||
|
||||
Playwright-msw: A library to wrap Mock Service Worker with Playwright
|
||||
|
||||
URL: https://github.com/valendres/playwright-msw
|
||||
License: MIT License
|
||||
Copyright: 2022 Peter Weller
|
||||
|
||||
---
|
||||
|
||||
Prettier: An opinionated code formatter
|
||||
|
||||
URL: https://prettier.io/
|
||||
License: MIT License
|
||||
Copyright: James Long and contributors
|
||||
|
||||
---
|
||||
|
||||
Remark-gfm: A GitHub Flavored Markdown plugin for Remark
|
||||
|
||||
URL: https://github.com/remarkjs/remark-gfm
|
||||
License: MIT License
|
||||
Copyright: Titus Wormer
|
||||
|
||||
---
|
||||
|
||||
Rollup: A module bundler for JavaScript
|
||||
|
||||
URL: https://rollupjs.org/
|
||||
License: MIT License
|
||||
Copyright: 2015-present Rollup contributors
|
||||
|
||||
---
|
||||
|
||||
Rollup Plugins: A collection of Rollup plugins
|
||||
|
||||
URL: https://github.com/rollup/plugins
|
||||
License: MIT License
|
||||
Copyright: 2019-present Rollup Plugins contributors
|
||||
|
||||
---
|
||||
|
||||
Rollup-plugin-esbuild: A Rollup plugin for using esbuild
|
||||
|
||||
URL: https://github.com/egoist/rollup-plugin-esbuild
|
||||
License: MIT License
|
||||
Copyright: 2020 EGOIST
|
||||
|
||||
---
|
||||
|
||||
Rollup-plugin-import-css: A Rollup plugin for importing CSS files
|
||||
|
||||
URL: https://github.com/jleeson/rollup-plugin-import-css
|
||||
License: MIT License
|
||||
Copyright: 2020 Jacob Leeson
|
||||
|
||||
---
|
||||
|
||||
rxjs: Reactive Extensions for JavaScript
|
||||
|
||||
URL: https://rxjs.dev/
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: 2015-present Ben Lesh <ben@benlesh.com>, Google, Inc., Netflix, Inc., Microsoft Corp., and contributors
|
||||
|
||||
---
|
||||
|
||||
Serilog: A diagnostic logging library for .NET
|
||||
|
||||
URL: https://github.com/serilog/serilog
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: Serilog Contributors
|
||||
|
||||
---
|
||||
|
||||
Simple Icons: A set of SVG icons for popular brands
|
||||
|
||||
URL: https://simpleicons.org/
|
||||
License: CC0 1.0 Universal License
|
||||
Copyright: Simple Icons Contributors
|
||||
|
||||
---
|
||||
|
||||
Storybook: A UI component explorer for Web Components
|
||||
|
||||
URL: https://storybook.js.org/
|
||||
License: MIT License
|
||||
Copyright: 2024 Storybook
|
||||
|
||||
---
|
||||
|
||||
StyleCop.Analyzers: Analyzers for StyleCop
|
||||
|
||||
URL: https://github.com/DotNetAnalyzers/StyleCopAnalyzers
|
||||
License: MIT License
|
||||
Copyright: Tunnel Vision Laboratories, LLC
|
||||
|
||||
---
|
||||
|
||||
SVGO: A tool for optimizing SVG files
|
||||
|
||||
URL: https://svgo.dev/
|
||||
License: MIT License
|
||||
Copyright: Kir Belevich
|
||||
|
||||
---
|
||||
|
||||
Swashbuckle.AspNetCore: A library for generating Swagger documentation for ASP.NET Core APIs
|
||||
|
||||
URL: https://github.com/domaindrivendev/Swashbuckle.AspNetCore
|
||||
License: MIT License
|
||||
Copyright: 2016 Richard Morris
|
||||
|
||||
---
|
||||
|
||||
Tiny Glob: A tiny globbing library for Node.js
|
||||
|
||||
URL: https://github.com/terkelg/tiny-glob
|
||||
License: MIT License
|
||||
Copyright: 2018 Terkel
|
||||
|
||||
---
|
||||
|
||||
Tiptap: A renderless rich-text editor for the web
|
||||
|
||||
URL: https://tiptap.dev/
|
||||
License: MIT License
|
||||
Copyright: 2025 Tiptap GmbH
|
||||
|
||||
---
|
||||
|
||||
Tsc-alias: A TypeScript compiler plugin for aliasing module paths
|
||||
|
||||
URL: https://github.com/justkey007/tsc-alias
|
||||
License: MIT License
|
||||
Copyright: 2018 Justkey
|
||||
|
||||
---
|
||||
|
||||
Typedoc: A documentation generator for TypeScript projects
|
||||
|
||||
URL: https://typedoc.org/
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: Gerrit Birkeland and Contributors
|
||||
|
||||
---
|
||||
|
||||
Typescript: A typed superset of JavaScript that compiles to plain JavaScript
|
||||
|
||||
URL: https://www.typescriptlang.org/
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: 2012-present Microsoft Corporation
|
||||
|
||||
---
|
||||
|
||||
Typescript-eslint: A set of tools for linting TypeScript code
|
||||
|
||||
URL: https://github.com/typescript-eslint/typescript-eslint
|
||||
License: MIT License
|
||||
Copyright: 2019 typescript-eslint and other contributors
|
||||
|
||||
---
|
||||
|
||||
Typescript-json-schema: A library for generating JSON schema from TypeScript types
|
||||
|
||||
URL: https://github.com/YousefED/typescript-json-schema
|
||||
License: BSD 3-Clause License
|
||||
Copyright: 2016 typescript-json-schema contributors
|
||||
|
||||
---
|
||||
|
||||
Umbraco.Code: Provides code-level tools for Umbraco
|
||||
|
||||
URL: https://github.com/umbraco/Umbraco-Code
|
||||
License: MIT License
|
||||
Copyright: 2005-present Umbraco A/S
|
||||
|
||||
---
|
||||
|
||||
Umbraco.GitVersioning.Extensions: Utilities for Nerdbank.GitVersioning
|
||||
|
||||
URL: https://github.com/umbraco/Umbraco.GitVersioning.Extensions
|
||||
License: MIT License
|
||||
Copyright: 2005-present Umbraco A/S
|
||||
|
||||
---
|
||||
|
||||
Umbraco.JsonSchema.Extensions: Utilities for JSON schema generation
|
||||
|
||||
URL: https://github.com/umbraco/Umbraco.JsonSchema.Extensions
|
||||
License: MIT License
|
||||
Copyright: 2005-present Umbraco A/S
|
||||
|
||||
---
|
||||
|
||||
Umbraco UI Library: A set of UI components for building web applications
|
||||
|
||||
URL: https://uui.umbraco.com/
|
||||
License: MIT License
|
||||
Copyright: 2005-present Umbraco A/S
|
||||
|
||||
---
|
||||
|
||||
uuid: A library for generating unique identifiers
|
||||
|
||||
URL: https://github.com/uuidjs/uuid
|
||||
License: MIT License
|
||||
Copyright: 2010-2020 Robert Kieffer and other contributors
|
||||
|
||||
---
|
||||
|
||||
Vite: A fast build tool and development server for modern web projects
|
||||
|
||||
URL: https://vite.dev/
|
||||
License: MIT License
|
||||
Copyright: 2019-present VoidZero Inc. and Vite contributors
|
||||
|
||||
---
|
||||
|
||||
Vite-plugin-static-copy: A Vite plugin for copying static files
|
||||
|
||||
URL: https://github.com/sapphi-red/vite-plugin-static-copy
|
||||
License: MIT License
|
||||
Copyright: 2021 sapphi-red
|
||||
|
||||
---
|
||||
|
||||
Vite-tsconfig-paths: A Vite plugin for resolving TypeScript paths
|
||||
|
||||
URL: https://github.com/aleclarson/vite-tsconfig-paths
|
||||
License: MIT License
|
||||
Copyright: Alec Larson
|
||||
|
||||
---
|
||||
|
||||
Web Component Analyzer: A tool for analyzing web components
|
||||
|
||||
URL: https://github.com/runem/web-component-analyzer
|
||||
License: MIT License
|
||||
Copyright: 2019 Rune Mehlsen
|
||||
@@ -13,7 +13,7 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
[DeliveryApiAccess]
|
||||
[VersionedDeliveryApiRoute("content")]
|
||||
[ApiExplorerSettings(GroupName = "Content")]
|
||||
[LocalizeFromAcceptLanguageHeader]
|
||||
[ContextualizeFromAcceptHeaders]
|
||||
[ValidateStartItem]
|
||||
[AddVaryHeader]
|
||||
[OutputCache(PolicyName = Constants.DeliveryApi.OutputCache.ContentCachePolicy)]
|
||||
|
||||
@@ -49,6 +49,7 @@ public static class UmbracoBuilderExtensions
|
||||
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
|
||||
});
|
||||
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
|
||||
builder.Services.AddSingleton<IRequestSegmmentService, RequestSegmentService>();
|
||||
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
|
||||
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
|
||||
builder.Services.AddSingleton<IRequestPreviewService, RequestPreviewService>();
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal sealed class ContextualizeFromAcceptHeadersAttribute : TypeFilterAttribute
|
||||
{
|
||||
public ContextualizeFromAcceptHeadersAttribute()
|
||||
: base(typeof(LocalizeFromAcceptLanguageHeaderAttributeFilter))
|
||||
{
|
||||
}
|
||||
|
||||
private class LocalizeFromAcceptLanguageHeaderAttributeFilter : IActionFilter
|
||||
{
|
||||
private readonly IRequestCultureService _requestCultureService;
|
||||
private readonly IRequestSegmmentService _requestSegmentService;
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
|
||||
public LocalizeFromAcceptLanguageHeaderAttributeFilter(
|
||||
IRequestCultureService requestCultureService,
|
||||
IRequestSegmmentService requestSegmentService,
|
||||
IVariationContextAccessor variationContextAccessor)
|
||||
{
|
||||
_requestCultureService = requestCultureService;
|
||||
_requestSegmentService = requestSegmentService;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var requestedCulture = _requestCultureService.GetRequestedCulture().NullOrWhiteSpaceAsNull();
|
||||
var requestedSegment = _requestSegmentService.GetRequestedSegment().NullOrWhiteSpaceAsNull();
|
||||
if (requestedCulture.IsNullOrWhiteSpace() && requestedSegment.IsNullOrWhiteSpace())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// contextualize the request
|
||||
// NOTE: request culture or segment can be null here (but not both), so make sure to retain any existing
|
||||
// context by means of fallback to current variation context (if available)
|
||||
_variationContextAccessor.VariationContext = new VariationContext(
|
||||
requestedCulture ?? _variationContextAccessor.VariationContext?.Culture,
|
||||
requestedSegment ?? _variationContextAccessor.VariationContext?.Segment);
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal sealed class LocalizeFromAcceptLanguageHeaderAttribute : TypeFilterAttribute
|
||||
{
|
||||
public LocalizeFromAcceptLanguageHeaderAttribute()
|
||||
: base(typeof(LocalizeFromAcceptLanguageHeaderAttributeFilter))
|
||||
{
|
||||
}
|
||||
|
||||
private class LocalizeFromAcceptLanguageHeaderAttributeFilter : IActionFilter
|
||||
{
|
||||
private readonly IRequestCultureService _requestCultureService;
|
||||
|
||||
public LocalizeFromAcceptLanguageHeaderAttributeFilter(IRequestCultureService requestCultureService)
|
||||
=> _requestCultureService = requestCultureService;
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var requestedCulture = _requestCultureService.GetRequestedCulture();
|
||||
if (requestedCulture.IsNullOrWhiteSpace())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_requestCultureService.SetRequestCulture(requestedCulture);
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,20 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
|
||||
}
|
||||
});
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Accept-Segment",
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Defines the segment to return. Use this when querying segment variant content items.",
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
{ "Default", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{ "Segment One", new OpenApiExample { Value = new OpenApiString("segment-one") } }
|
||||
}
|
||||
});
|
||||
|
||||
AddApiKey(operation);
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
|
||||
@@ -2,17 +2,15 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
internal sealed partial class RequestCultureService : RequestHeaderHandler, IRequestCultureService
|
||||
{
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
|
||||
public RequestCultureService(IHttpContextAccessor httpContextAccessor, IVariationContextAccessor variationContextAccessor)
|
||||
: base(httpContextAccessor) =>
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
public RequestCultureService(IHttpContextAccessor httpContextAccessor)
|
||||
: base(httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? GetRequestedCulture()
|
||||
@@ -21,15 +19,10 @@ internal sealed partial class RequestCultureService : RequestHeaderHandler, IReq
|
||||
return ValidLanguageHeaderRegex().IsMatch(acceptLanguage) ? acceptLanguage : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Use IVariationContextAccessor to manipulate the variation context. Scheduled for removal in V17.")]
|
||||
public void SetRequestCulture(string culture)
|
||||
{
|
||||
if (_variationContextAccessor.VariationContext?.Culture == culture)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_variationContextAccessor.VariationContext = new VariationContext(culture);
|
||||
// no-op
|
||||
}
|
||||
|
||||
// at the time of writing we're introducing this to get rid of accept-language header values like "en-GB,en-US;q=0.9,en;q=0.8",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
@@ -11,14 +10,19 @@ namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
internal sealed class RequestRoutingService : RoutingServiceBase, IRequestRoutingService
|
||||
{
|
||||
private readonly IRequestCultureService _requestCultureService;
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
|
||||
public RequestRoutingService(
|
||||
IDomainCache domainCache,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IRequestStartItemProviderAccessor requestStartItemProviderAccessor,
|
||||
IRequestCultureService requestCultureService)
|
||||
: base(domainCache, httpContextAccessor, requestStartItemProviderAccessor) =>
|
||||
IRequestCultureService requestCultureService,
|
||||
IVariationContextAccessor variationContextAccessor)
|
||||
: base(domainCache, httpContextAccessor, requestStartItemProviderAccessor)
|
||||
{
|
||||
_requestCultureService = requestCultureService;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetContentRoute(string requestedPath)
|
||||
@@ -50,9 +54,14 @@ internal sealed class RequestRoutingService : RoutingServiceBase, IRequestRoutin
|
||||
}
|
||||
|
||||
// the Accept-Language header takes precedence over configured domain culture
|
||||
if (domainAndUri.Culture != null && _requestCultureService.GetRequestedCulture().IsNullOrWhiteSpace())
|
||||
if (domainAndUri.Culture != null
|
||||
&& _requestCultureService.GetRequestedCulture().IsNullOrWhiteSpace()
|
||||
&& _variationContextAccessor.VariationContext?.Culture != domainAndUri.Culture)
|
||||
{
|
||||
_requestCultureService.SetRequestCulture(domainAndUri.Culture);
|
||||
// update the variation context to match the requested domain culture while retaining any contextualized segment
|
||||
_variationContextAccessor.VariationContext = new VariationContext(
|
||||
culture: domainAndUri.Culture,
|
||||
segment: _variationContextAccessor.VariationContext?.Segment);
|
||||
}
|
||||
|
||||
// when resolving content from a configured domain, the content cache expects the content route
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
internal sealed class RequestSegmentService : RequestHeaderHandler, IRequestSegmmentService
|
||||
{
|
||||
public RequestSegmentService(IHttpContextAccessor httpContextAccessor)
|
||||
: base(httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? GetRequestedSegment()
|
||||
=> GetHeaderValue("Accept-Segment");
|
||||
}
|
||||
@@ -27,6 +27,10 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
|
||||
.WithTitle("Content type culture variance mismatch")
|
||||
.WithDetail("The content type variance did not match that of the passed content data.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.ContentTypeSegmentVarianceMismatch => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Content type segment variance mismatch")
|
||||
.WithDetail("The content type variance did not match that of the passed content data.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.NotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("The content could not be found")
|
||||
.Build()),
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@ public class SearchDocumentItemController : DocumentItemControllerBase
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<DocumentItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<IActionResult> SearchWithTrashed(
|
||||
public async Task<IActionResult> SearchWithTrashed(
|
||||
CancellationToken cancellationToken,
|
||||
string query,
|
||||
bool? trashed = null,
|
||||
@@ -33,13 +33,13 @@ public class SearchDocumentItemController : DocumentItemControllerBase
|
||||
Guid? parentId = null,
|
||||
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null)
|
||||
{
|
||||
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, trashed, skip, take);
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, trashed, skip, take);
|
||||
var result = new PagedModel<DocumentItemResponseModel>
|
||||
{
|
||||
Items = searchResult.Items.OfType<IDocumentEntitySlim>().Select(_documentPresentationFactory.CreateItemResponseModel),
|
||||
Total = searchResult.Total,
|
||||
};
|
||||
|
||||
return Task.FromResult<IActionResult>(Ok(result));
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
|
||||
public class ScaffoldDocumentBlueprintController : DocumentBlueprintControllerBase
|
||||
{
|
||||
private readonly IContentBlueprintEditingService _contentBlueprintEditingService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
|
||||
public ScaffoldDocumentBlueprintController(IContentBlueprintEditingService contentBlueprintEditingService, IUmbracoMapper umbracoMapper)
|
||||
{
|
||||
_contentBlueprintEditingService = contentBlueprintEditingService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/scaffold")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(DocumentBlueprintResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Scaffold(CancellationToken cancellationToken, Guid id)
|
||||
{
|
||||
IContent? blueprint = await _contentBlueprintEditingService.GetScaffoldedAsync(id);
|
||||
return blueprint is not null
|
||||
? Ok(_umbracoMapper.Map<DocumentBlueprintResponseModel>(blueprint))
|
||||
: DocumentBlueprintNotFound();
|
||||
}
|
||||
}
|
||||
@@ -29,15 +29,15 @@ public class SearchMediaItemController : MediaItemControllerBase
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<MediaItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<IActionResult> SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, bool? trashed = null, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
|
||||
public async Task<IActionResult> SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, bool? trashed = null, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
|
||||
{
|
||||
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, trashed, skip, take);
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, trashed, skip, take);
|
||||
var result = new PagedModel<MediaItemResponseModel>
|
||||
{
|
||||
Items = searchResult.Items.OfType<IMediaEntitySlim>().Select(_mediaPresentationFactory.CreateItemResponseModel),
|
||||
Total = searchResult.Total,
|
||||
};
|
||||
|
||||
return Task.FromResult<IActionResult>(Ok(result));
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Umbraco.Cms.Api.Management.Extensions;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Content;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Factories;
|
||||
|
||||
@@ -8,32 +6,22 @@ internal abstract class ContentEditingPresentationFactory<TValueModel, TVariantM
|
||||
where TValueModel : ValueModelBase
|
||||
where TVariantModel : VariantModelBase
|
||||
{
|
||||
protected TContentEditingModel MapContentEditingModel<TContentEditingModel>(ContentModelBase<TValueModel, TVariantModel> contentModel)
|
||||
protected TContentEditingModel MapContentEditingModel<TContentEditingModel>(
|
||||
ContentModelBase<TValueModel, TVariantModel> contentModel)
|
||||
where TContentEditingModel : ContentEditingModelBase, new()
|
||||
{
|
||||
TVariantModel? invariantVariant = contentModel.Variants.FirstOrDefault(variant => variant.DoesNotVaryByCulture() && variant.DoesNotVaryBySegment());
|
||||
TValueModel[] invariantProperties = contentModel.Values.Where(value => value.DoesNotVaryByCulture() && value.DoesNotVaryBySegment()).ToArray();
|
||||
|
||||
PropertyValueModel ToPropertyValueModel(TValueModel valueModel)
|
||||
=> new() { Alias = valueModel.Alias, Value = valueModel.Value };
|
||||
|
||||
return new TContentEditingModel
|
||||
=> new()
|
||||
{
|
||||
InvariantName = invariantVariant?.Name,
|
||||
InvariantProperties = invariantProperties.Select(ToPropertyValueModel).ToArray(),
|
||||
Properties = contentModel
|
||||
.Values
|
||||
.Select(value => new PropertyValueModel
|
||||
{
|
||||
Alias = value.Alias, Value = value.Value, Culture = value.Culture, Segment = value.Segment
|
||||
}).ToArray(),
|
||||
Variants = contentModel
|
||||
.Variants
|
||||
.Where(variant => variant.VariesByCulture() || variant.VariesBySegment())
|
||||
.Select(variant => new VariantModel
|
||||
{
|
||||
Culture = variant.Culture,
|
||||
Segment = variant.Segment,
|
||||
Name = variant.Name,
|
||||
Properties = contentModel
|
||||
.Values
|
||||
.Where(value => value.Culture == variant.Culture && value.Segment == variant.Segment)
|
||||
.Select(ToPropertyValueModel).ToArray()
|
||||
Culture = variant.Culture, Segment = variant.Segment, Name = variant.Name
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
using ContentTypeEditingModels = Umbraco.Cms.Core.Models.ContentTypeEditing;
|
||||
@@ -38,12 +39,27 @@ internal abstract class ContentTypeEditingPresentationFactory<TContentType>
|
||||
VariesByCulture = viewModel.VariesByCulture,
|
||||
VariesBySegment = viewModel.VariesBySegment,
|
||||
Containers = MapContainers<TPropertyTypeContainerEditingModel>(viewModel.Containers),
|
||||
Properties = MapProperties<TPropertyTypeEditingModel>(viewModel.Properties)
|
||||
Properties = MapProperties<TPropertyTypeEditingModel>(viewModel.Properties),
|
||||
};
|
||||
|
||||
return editingModel;
|
||||
}
|
||||
|
||||
protected Guid? CalculateCreateContainerKey(ReferenceByIdModel? parent, IDictionary<Guid, ContentTypeViewModels.CompositionType> compositions)
|
||||
{
|
||||
// special case:
|
||||
// the API is somewhat confusing when it comes to inheritance. the parent denotes a container (folder), but it
|
||||
// is easily confused with the parent for inheritance.
|
||||
// if the request model contains the same key for container and "inheritance composition", we'll be lenient and
|
||||
// allow it - just remove the container, inheritance takes precedence as intent.
|
||||
Guid? parentId = parent?.Id;
|
||||
return parentId.HasValue
|
||||
&& compositions.TryGetValue(parentId.Value, out ContentTypeViewModels.CompositionType compositionType)
|
||||
&& compositionType is ContentTypeViewModels.CompositionType.Inheritance
|
||||
? null
|
||||
: parentId;
|
||||
}
|
||||
|
||||
protected T MapCompositionModel<T>(ContentTypeAvailableCompositionsResult compositionResult)
|
||||
where T : ContentTypeViewModels.AvailableContentTypeCompositionResponseModelBase, new()
|
||||
{
|
||||
|
||||
@@ -45,8 +45,6 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory
|
||||
{
|
||||
DocumentResponseModel responseModel = _umbracoMapper.Map<DocumentResponseModel>(content)!;
|
||||
|
||||
responseModel.Urls = await _documentUrlFactory.CreateUrlsAsync(content);
|
||||
|
||||
Guid? templateKey = content.TemplateId.HasValue
|
||||
? _templateService.GetAsync(content.TemplateId.Value).Result?.Key
|
||||
: null;
|
||||
@@ -62,8 +60,6 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory
|
||||
{
|
||||
PublishedDocumentResponseModel responseModel = _umbracoMapper.Map<PublishedDocumentResponseModel>(content)!;
|
||||
|
||||
responseModel.Urls = await _documentUrlFactory.CreateUrlsAsync(content);
|
||||
|
||||
Guid? templateKey = content.PublishTemplateId.HasValue
|
||||
? _templateService.GetAsync(content.PublishTemplateId.Value).Result?.Key
|
||||
: null;
|
||||
@@ -80,8 +76,6 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory
|
||||
DocumentResponseModel responseModel = _umbracoMapper.Map<DocumentResponseModel>(content)!;
|
||||
_umbracoMapper.Map(schedule, responseModel);
|
||||
|
||||
responseModel.Urls = await _documentUrlFactory.CreateUrlsAsync(content);
|
||||
|
||||
Guid? templateKey = content.TemplateId.HasValue
|
||||
? _templateService.GetAsync(content.TemplateId.Value).Result?.Key
|
||||
: null;
|
||||
|
||||
@@ -25,12 +25,14 @@ internal sealed class DocumentTypeEditingPresentationFactory : ContentTypeEditin
|
||||
MapCleanup(createModel, requestModel.Cleanup);
|
||||
|
||||
createModel.Key = requestModel.Id;
|
||||
createModel.ContainerKey = requestModel.Parent?.Id;
|
||||
createModel.AllowedTemplateKeys = requestModel.AllowedTemplates.Select(reference => reference.Id).ToArray();
|
||||
createModel.DefaultTemplateKey = requestModel.DefaultTemplate?.Id;
|
||||
createModel.ListView = requestModel.Collection?.Id;
|
||||
createModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedDocumentTypes);
|
||||
createModel.Compositions = MapCompositions(requestModel.Compositions);
|
||||
|
||||
IDictionary<Guid, ViewModels.ContentType.CompositionType> compositionTypesByKey = CompositionTypesByKey(requestModel.Compositions);
|
||||
createModel.Compositions = MapCompositions(compositionTypesByKey);
|
||||
createModel.ContainerKey = CalculateCreateContainerKey(requestModel.Parent, compositionTypesByKey);
|
||||
|
||||
return createModel;
|
||||
}
|
||||
@@ -51,7 +53,7 @@ internal sealed class DocumentTypeEditingPresentationFactory : ContentTypeEditin
|
||||
updateModel.DefaultTemplateKey = requestModel.DefaultTemplate?.Id;
|
||||
updateModel.ListView = requestModel.Collection?.Id;
|
||||
updateModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedDocumentTypes);
|
||||
updateModel.Compositions = MapCompositions(requestModel.Compositions);
|
||||
updateModel.Compositions = MapCompositions(CompositionTypesByKey(requestModel.Compositions));
|
||||
|
||||
return updateModel;
|
||||
}
|
||||
@@ -72,8 +74,8 @@ internal sealed class DocumentTypeEditingPresentationFactory : ContentTypeEditin
|
||||
.DistinctBy(t => t.DocumentType.Id)
|
||||
.ToDictionary(t => t.DocumentType.Id, t => t.SortOrder));
|
||||
|
||||
private IEnumerable<Composition> MapCompositions(IEnumerable<DocumentTypeComposition> documentTypeCompositions)
|
||||
=> MapCompositions(documentTypeCompositions
|
||||
private IDictionary<Guid, ViewModels.ContentType.CompositionType> CompositionTypesByKey(IEnumerable<DocumentTypeComposition> documentTypeCompositions)
|
||||
=> documentTypeCompositions
|
||||
.DistinctBy(c => c.DocumentType.Id)
|
||||
.ToDictionary(c => c.DocumentType.Id, c => c.CompositionType));
|
||||
.ToDictionary(c => c.DocumentType.Id, c => c.CompositionType);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ internal sealed class DocumentVersionPresentationFactory : IDocumentVersionPrese
|
||||
new ReferenceByIdModel(_entityService.GetKey(contentVersion.ContentTypeId, UmbracoObjectTypes.DocumentType)
|
||||
.Result),
|
||||
new ReferenceByIdModel(await _userIdKeyResolver.GetAsync(contentVersion.UserId)),
|
||||
new DateTimeOffset(contentVersion.VersionDate, TimeSpan.Zero), // todo align with datetime offset rework
|
||||
new DateTimeOffset(contentVersion.VersionDate),
|
||||
contentVersion.CurrentPublishedVersion,
|
||||
contentVersion.CurrentDraftVersion,
|
||||
contentVersion.PreventCleanup);
|
||||
|
||||
@@ -23,11 +23,13 @@ internal sealed class MediaTypeEditingPresentationFactory : ContentTypeEditingPr
|
||||
>(requestModel);
|
||||
|
||||
createModel.Key = requestModel.Id;
|
||||
createModel.ContainerKey = requestModel.Parent?.Id;
|
||||
createModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedMediaTypes);
|
||||
createModel.Compositions = MapCompositions(requestModel.Compositions);
|
||||
createModel.ListView = requestModel.Collection?.Id;
|
||||
|
||||
IDictionary<Guid, ViewModels.ContentType.CompositionType> compositionTypesByKey = CompositionTypesByKey(requestModel.Compositions);
|
||||
createModel.Compositions = MapCompositions(compositionTypesByKey);
|
||||
createModel.ContainerKey = CalculateCreateContainerKey(requestModel.Parent, compositionTypesByKey);
|
||||
|
||||
return createModel;
|
||||
}
|
||||
|
||||
@@ -42,7 +44,8 @@ internal sealed class MediaTypeEditingPresentationFactory : ContentTypeEditingPr
|
||||
>(requestModel);
|
||||
|
||||
updateModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedMediaTypes);
|
||||
updateModel.Compositions = MapCompositions(requestModel.Compositions);
|
||||
updateModel.Compositions = MapCompositions(CompositionTypesByKey(requestModel.Compositions));
|
||||
|
||||
updateModel.ListView = requestModel.Collection?.Id;
|
||||
|
||||
return updateModel;
|
||||
@@ -56,8 +59,8 @@ internal sealed class MediaTypeEditingPresentationFactory : ContentTypeEditingPr
|
||||
.DistinctBy(t => t.MediaType.Id)
|
||||
.ToDictionary(t => t.MediaType.Id, t => t.SortOrder));
|
||||
|
||||
private IEnumerable<Composition> MapCompositions(IEnumerable<MediaTypeComposition> documentTypeCompositions)
|
||||
=> MapCompositions(documentTypeCompositions
|
||||
private IDictionary<Guid, ViewModels.ContentType.CompositionType> CompositionTypesByKey(IEnumerable<MediaTypeComposition> documentTypeCompositions)
|
||||
=> documentTypeCompositions
|
||||
.DistinctBy(c => c.MediaType.Id)
|
||||
.ToDictionary(c => c.MediaType.Id, c => c.CompositionType));
|
||||
.ToDictionary(c => c.MediaType.Id, c => c.CompositionType);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,8 @@ public abstract class ContentMapDefinition<TContent, TValueViewModel, TVariantVi
|
||||
{
|
||||
IPropertyValue[] propertyValues = source.Properties.SelectMany(propertyCollection => propertyCollection.Values).ToArray();
|
||||
var cultures = source.AvailableCultures.DefaultIfEmpty(null).ToArray();
|
||||
var segments = propertyValues.Select(property => property.Segment).Distinct().DefaultIfEmpty(null).ToArray();
|
||||
// the default segment (null) must always be included in the view model - both for variant and invariant documents
|
||||
var segments = propertyValues.Select(property => property.Segment).Union([null]).Distinct().ToArray();
|
||||
|
||||
return cultures
|
||||
.SelectMany(culture => segments.Select(segment =>
|
||||
|
||||
@@ -83,24 +83,9 @@ public abstract class ContentTypeMapDefinition<TContentType, TPropertyTypeModel,
|
||||
? CompositionType.Inheritance
|
||||
: CompositionType.Composition;
|
||||
|
||||
protected static IEnumerable<T> MapNestedCompositions<T>(IEnumerable<IContentTypeComposition> directCompositions, int contentTypeParentId, Func<ReferenceByIdModel, CompositionType, T> contentTypeCompositionFactory)
|
||||
{
|
||||
var allCompositions = new List<T>();
|
||||
|
||||
foreach (var composition in directCompositions)
|
||||
{
|
||||
CompositionType compositionType = CalculateCompositionType(contentTypeParentId, composition);
|
||||
T contentTypeComposition = contentTypeCompositionFactory(new ReferenceByIdModel(composition.Key), compositionType);
|
||||
allCompositions.Add(contentTypeComposition);
|
||||
|
||||
// When we have composition inheritance, we have to find all ancestor compositions recursively
|
||||
if (compositionType == CompositionType.Inheritance && composition.ContentTypeComposition.Any())
|
||||
{
|
||||
var nestedCompositions = MapNestedCompositions(composition.ContentTypeComposition, composition.ParentId, contentTypeCompositionFactory);
|
||||
allCompositions.AddRange(nestedCompositions);
|
||||
}
|
||||
}
|
||||
|
||||
return allCompositions;
|
||||
}
|
||||
protected static IEnumerable<T> MapCompositions<T>(IEnumerable<IContentTypeComposition> directCompositions, int contentTypeParentId, Func<ReferenceByIdModel, CompositionType, T> contentTypeCompositionFactory)
|
||||
=> directCompositions
|
||||
.Select(composition => contentTypeCompositionFactory(
|
||||
new ReferenceByIdModel(composition.Key),
|
||||
CalculateCompositionType(contentTypeParentId, composition))).ToArray();
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public class DocumentTypeMapDefinition : ContentTypeMapDefinition<IContentType,
|
||||
new DocumentTypeSort { DocumentType = new ReferenceByIdModel(ct.Key), SortOrder = ct.SortOrder })
|
||||
.OrderBy(ct => ct.SortOrder)
|
||||
.ToArray() ?? Enumerable.Empty<DocumentTypeSort>();
|
||||
target.Compositions = MapNestedCompositions(
|
||||
target.Compositions = MapCompositions(
|
||||
source.ContentTypeComposition,
|
||||
source.ParentId,
|
||||
(referenceByIdModel, compositionType) => new DocumentTypeComposition
|
||||
|
||||
@@ -40,7 +40,7 @@ public class MediaTypeMapDefinition : ContentTypeMapDefinition<IMediaType, Media
|
||||
target.AllowedMediaTypes = source.AllowedContentTypes?.Select(ct =>
|
||||
new MediaTypeSort { MediaType = new ReferenceByIdModel(ct.Key), SortOrder = ct.SortOrder })
|
||||
.ToArray() ?? Enumerable.Empty<MediaTypeSort>();
|
||||
target.Compositions = MapNestedCompositions(
|
||||
target.Compositions = MapCompositions(
|
||||
source.ContentTypeComposition,
|
||||
source.ParentId,
|
||||
(referenceByIdModel, compositionType) => new MediaTypeComposition
|
||||
|
||||
@@ -32,7 +32,7 @@ public class MemberTypeMapDefinition : ContentTypeMapDefinition<IMemberType, Mem
|
||||
target.IsElement = source.IsElement;
|
||||
target.Containers = MapPropertyTypeContainers(source);
|
||||
target.Properties = MapPropertyTypes(source);
|
||||
target.Compositions = MapNestedCompositions(
|
||||
target.Compositions = MapCompositions(
|
||||
source.ContentTypeComposition,
|
||||
source.ParentId,
|
||||
(referenceByIdModel, compositionType) => new MemberTypeComposition
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"openapi": "3.0.1",
|
||||
"openapi": "3.0.4",
|
||||
"info": {
|
||||
"title": "Umbraco Management API",
|
||||
"description": "This shows all APIs available in this version of Umbraco - including all the legacy apis that are available for backward compatibility",
|
||||
@@ -3485,6 +3485,66 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/document-blueprint/{id}/scaffold": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Document Blueprint"
|
||||
],
|
||||
"operationId": "GetDocumentBlueprintByIdScaffold",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/DocumentBlueprintResponseModel"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "The resource is protected and requires an authentication token"
|
||||
},
|
||||
"403": {
|
||||
"description": "The authenticated user does not have access to this resource"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"Backoffice User": [ ]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/document-blueprint/folder": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -37553,7 +37613,8 @@
|
||||
"$ref": "#/components/schemas/DocumentUrlInfoModel"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"deprecated": true
|
||||
},
|
||||
"template": {
|
||||
"oneOf": [
|
||||
@@ -43429,7 +43490,8 @@
|
||||
"$ref": "#/components/schemas/DocumentUrlInfoModel"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"deprecated": true
|
||||
},
|
||||
"template": {
|
||||
"oneOf": [
|
||||
|
||||
@@ -33,8 +33,11 @@ public class ConfigureUmbracoBackofficeJsonOptions : IConfigureNamedOptions<Json
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonUdiConverter());
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonUdiRangeConverter());
|
||||
options.JsonSerializerOptions.Converters.Add(new ValidationProblemDetailsConverter());
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonObjectConverter());
|
||||
|
||||
options.JsonSerializerOptions.TypeInfoResolver = _umbracoJsonTypeInfoResolver;
|
||||
|
||||
options.JsonSerializerOptions.MaxDepth = 64; // Ensures the maximum possible value is used, in particular to support handling as best we can levels of nested blocks.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Custom JSON converter registered via <see cref="ConfigureUmbracoBackofficeJsonOptions"/> to serialize <see cref="ValidationProblemDetails" /> to JSON.
|
||||
/// </summary>
|
||||
public sealed class ValidationProblemDetailsConverter : JsonConverter<ValidationProblemDetails>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override ValidationProblemDetails? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, ValidationProblemDetails value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", value.Type);
|
||||
writer.WriteString("title", value.Title);
|
||||
|
||||
if (value.Status.HasValue)
|
||||
{
|
||||
writer.WriteNumber("status", value.Status.Value);
|
||||
}
|
||||
|
||||
if (value.Detail is not null)
|
||||
{
|
||||
writer.WriteString("detail", value.Detail);
|
||||
}
|
||||
|
||||
if (value.Instance is not null)
|
||||
{
|
||||
writer.WriteString("instance", value.Instance);
|
||||
}
|
||||
|
||||
if (value.Errors.Any())
|
||||
{
|
||||
writer.WritePropertyName("errors");
|
||||
writer.WriteStartArray();
|
||||
|
||||
foreach (KeyValuePair<string, string[]> error in value.Errors)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName($"$.{string.Join(".", error.Key.Split(Constants.CharArrays.Period).Select(s => s.ToFirstLowerInvariant()))}");
|
||||
writer.WriteStartArray();
|
||||
|
||||
foreach (var errorDetails in error.Value)
|
||||
{
|
||||
writer.WriteStringValue(errorDetails);
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
if (value.Extensions.TryGetValue("traceId", out var traceId) && traceId is string traceIdValue)
|
||||
{
|
||||
writer.WriteString("traceId", traceIdValue);
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
|
||||
public class DocumentResponseModel : DocumentResponseModelBase<DocumentValueResponseModel, DocumentVariantResponseModel>
|
||||
{
|
||||
[Obsolete("This property is no longer populated. Please use /document/{id}/urls instead to retrieve the URLs for a document. Scheduled for removal in Umbraco 17.")]
|
||||
public IEnumerable<DocumentUrlInfo> Urls { get; set; } = Enumerable.Empty<DocumentUrlInfo>();
|
||||
|
||||
public ReferenceByIdModel? Template { get; set; }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
@@ -21,9 +23,11 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
private readonly IDocumentNavigationManagementService _documentNavigationManagementService;
|
||||
private readonly IContentService _contentService;
|
||||
private readonly IDocumentCacheService _documentCacheService;
|
||||
private readonly ICacheManager _cacheManager;
|
||||
private readonly IPublishStatusManagementService _publishStatusManagementService;
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
|
||||
[Obsolete("Use the constructor with ICacheManager instead, scheduled for removal in V17.")]
|
||||
public ContentCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
@@ -38,6 +42,39 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
IContentService contentService,
|
||||
IPublishStatusManagementService publishStatusManagementService,
|
||||
IDocumentCacheService documentCacheService)
|
||||
: this(
|
||||
appCaches,
|
||||
serializer,
|
||||
idKeyMap,
|
||||
domainService,
|
||||
eventAggregator,
|
||||
factory,
|
||||
documentUrlService,
|
||||
domainCacheService,
|
||||
documentNavigationQueryService,
|
||||
documentNavigationManagementService,
|
||||
contentService,
|
||||
publishStatusManagementService,
|
||||
documentCacheService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<ICacheManager>())
|
||||
{
|
||||
}
|
||||
|
||||
public ContentCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
IIdKeyMap idKeyMap,
|
||||
IDomainService domainService,
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheRefresherNotificationFactory factory,
|
||||
IDocumentUrlService documentUrlService,
|
||||
IDomainCacheService domainCacheService,
|
||||
IDocumentNavigationQueryService documentNavigationQueryService,
|
||||
IDocumentNavigationManagementService documentNavigationManagementService,
|
||||
IContentService contentService,
|
||||
IPublishStatusManagementService publishStatusManagementService,
|
||||
IDocumentCacheService documentCacheService,
|
||||
ICacheManager cacheManager)
|
||||
: base(appCaches, serializer, eventAggregator, factory)
|
||||
{
|
||||
_idKeyMap = idKeyMap;
|
||||
@@ -49,6 +86,11 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
_contentService = contentService;
|
||||
_documentCacheService = documentCacheService;
|
||||
_publishStatusManagementService = publishStatusManagementService;
|
||||
|
||||
// TODO: Ideally we should inject IElementsCache
|
||||
// this interface is in infrastructure, and changing this is very breaking
|
||||
// so as long as we have the cache manager, which casts the IElementsCache to a simple AppCache we might as well use that.
|
||||
_cacheManager = cacheManager;
|
||||
}
|
||||
|
||||
#region Indirect
|
||||
@@ -83,6 +125,13 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
AppCaches.RuntimeCache.ClearOfType<PublicAccessEntry>();
|
||||
AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey);
|
||||
|
||||
// Ideally, we'd like to not have to clear the entire cache here. However, this was the existing behavior in NuCache.
|
||||
// The reason for this is that we have no way to know which elements are affected by the changes or what their keys are.
|
||||
// This is because currently published elements live exclusively in a JSON blob in the umbracoPropertyData table.
|
||||
// This means that the only way to resolve these keys is to actually parse this data with a specific value converter, and for all cultures, which is not possible.
|
||||
// If published elements become their own entities with relations, instead of just property data, we can revisit this.
|
||||
_cacheManager.ElementsCache.Clear();
|
||||
|
||||
var idsRemoved = new HashSet<int>();
|
||||
IAppPolicyCache isolatedCache = AppCaches.IsolatedCaches.GetOrCreate<IContent>();
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
@@ -18,7 +20,9 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
|
||||
private readonly IMediaNavigationManagementService _mediaNavigationManagementService;
|
||||
private readonly IMediaService _mediaService;
|
||||
private readonly IMediaCacheService _mediaCacheService;
|
||||
private readonly ICacheManager _cacheManager;
|
||||
|
||||
[Obsolete("Use the constructor with ICacheManager instead, scheduled for removal in V17.")]
|
||||
public MediaCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
@@ -29,6 +33,31 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
|
||||
IMediaNavigationManagementService mediaNavigationManagementService,
|
||||
IMediaService mediaService,
|
||||
IMediaCacheService mediaCacheService)
|
||||
: this(
|
||||
appCaches,
|
||||
serializer,
|
||||
idKeyMap,
|
||||
eventAggregator,
|
||||
factory,
|
||||
mediaNavigationQueryService,
|
||||
mediaNavigationManagementService,
|
||||
mediaService,
|
||||
mediaCacheService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<ICacheManager>())
|
||||
{
|
||||
}
|
||||
|
||||
public MediaCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
IIdKeyMap idKeyMap,
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheRefresherNotificationFactory factory,
|
||||
IMediaNavigationQueryService mediaNavigationQueryService,
|
||||
IMediaNavigationManagementService mediaNavigationManagementService,
|
||||
IMediaService mediaService,
|
||||
IMediaCacheService mediaCacheService,
|
||||
ICacheManager cacheManager)
|
||||
: base(appCaches, serializer, eventAggregator, factory)
|
||||
{
|
||||
_idKeyMap = idKeyMap;
|
||||
@@ -36,6 +65,9 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
|
||||
_mediaNavigationManagementService = mediaNavigationManagementService;
|
||||
_mediaService = mediaService;
|
||||
_mediaCacheService = mediaCacheService;
|
||||
|
||||
// TODO: Use IElementsCache instead of ICacheManager, see ContentCacheRefresher for more information.
|
||||
_cacheManager = cacheManager;
|
||||
}
|
||||
|
||||
#region Indirect
|
||||
@@ -87,6 +119,13 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
|
||||
AppCaches.RuntimeCache.ClearByKey(CacheKeys.MediaRecycleBinCacheKey);
|
||||
Attempt<IAppPolicyCache?> mediaCache = AppCaches.IsolatedCaches.Get<IMedia>();
|
||||
|
||||
// Ideally, we'd like to not have to clear the entire cache here. However, this was the existing behavior in NuCache.
|
||||
// The reason for this is that we have no way to know which elements are affected by the changes or what their keys are.
|
||||
// This is because currently published elements live exclusively in a JSON blob in the umbracoPropertyData table.
|
||||
// This means that the only way to resolve these keys is to actually parse this data with a specific value converter, and for all cultures, which is not possible.
|
||||
// If published elements become their own entities with relations, instead of just property data, we can revisit this.
|
||||
_cacheManager.ElementsCache.Clear();
|
||||
|
||||
foreach (JsonPayload payload in payloads)
|
||||
{
|
||||
if (payload.ChangeTypes == TreeChangeTypes.Remove)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.ComponentModel;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
|
||||
namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
@@ -13,6 +14,8 @@ public class LoggingSettings
|
||||
{
|
||||
internal const string StaticMaxLogAge = "1.00:00:00"; // TimeSpan.FromHours(24);
|
||||
internal const string StaticDirectory = Constants.SystemDirectories.LogFiles;
|
||||
internal const string StaticFileNameFormat = LoggingConfiguration.DefaultLogFileNameFormat;
|
||||
internal const string StaticFileNameFormatArguments = "MachineName";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the maximum age of a log file.
|
||||
@@ -31,4 +34,25 @@ public class LoggingSettings
|
||||
/// </value>
|
||||
[DefaultValue(StaticDirectory)]
|
||||
public string Directory { get; set; } = StaticDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the file name format to use for log files.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The file name format.
|
||||
/// </value>
|
||||
[DefaultValue(StaticFileNameFormat)]
|
||||
public string FileNameFormat { get; set; } = StaticFileNameFormat;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the file name format arguments to use for log files.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The file name format arguments as a comma delimited string of accepted values.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Accepted values for format arguments are: MachineName, EnvironmentName.
|
||||
/// </remarks>
|
||||
[DefaultValue(StaticFileNameFormatArguments)]
|
||||
public string FileNameFormatArguments { get; set; } = StaticFileNameFormatArguments;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ public class RuntimeSettings
|
||||
public RuntimeMode Mode { get; set; } = RuntimeMode.BackofficeDevelopment;
|
||||
|
||||
/// <summary>
|
||||
[Obsolete("No longer used and will be removed in Umbraco 16.")]
|
||||
/// Gets or sets a value for the maximum request length in kb.
|
||||
/// </summary>
|
||||
public long? MaxRequestLength { get; set; }
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
|
||||
namespace Umbraco.Cms.Core.Configuration.Models.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for configuration representated as <see cref="LoggingSettings" />.
|
||||
/// </summary>
|
||||
public class LoggingSettingsValidator : ConfigurationValidatorBase, IValidateOptions<LoggingSettings>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ValidateOptionsResult Validate(string? name, LoggingSettings options)
|
||||
{
|
||||
if (!ValidateFileNameFormatArgument(options.FileNameFormat, options.FileNameFormatArguments, out var message))
|
||||
{
|
||||
return ValidateOptionsResult.Fail(message);
|
||||
}
|
||||
|
||||
|
||||
return ValidateOptionsResult.Success;
|
||||
}
|
||||
|
||||
private bool ValidateFileNameFormatArgument(string fileNameFormat, string fileNameFormatArguments, out string message)
|
||||
{
|
||||
var fileNameFormatArgumentsAsArray = fileNameFormatArguments
|
||||
.Split([','], StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => x.Trim())
|
||||
.ToArray();
|
||||
if (fileNameFormatArgumentsAsArray.Any(x => LoggingConfiguration.SupportedFileNameFormatArguments.Contains(x) is false))
|
||||
{
|
||||
message = $"The file name arguments '{string.Join(",", fileNameFormatArgumentsAsArray)}' contain one or more values that aren't in the supported list of values '{string.Join(",", LoggingConfiguration.SupportedFileNameFormatArguments)}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = string.Format(fileNameFormat, fileNameFormatArgumentsAsArray);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
message = $"The provided file name format '{fileNameFormat}' could not be used with the provided arguments '{string.Join(",", fileNameFormatArgumentsAsArray)}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Umbraco.Cms.Core;
|
||||
public static partial class Constants
|
||||
{
|
||||
public static class Cache
|
||||
{
|
||||
public static class Tags
|
||||
{
|
||||
public const string Content = "content";
|
||||
|
||||
public const string Media = "media";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,6 @@ public interface IRequestCultureService
|
||||
/// </summary>
|
||||
string? GetRequestedCulture();
|
||||
|
||||
/// <summary>
|
||||
/// Updates the current request culture if applicable.
|
||||
/// </summary>
|
||||
/// <param name="culture">The culture to use for the current request.</param>
|
||||
[Obsolete("Use IVariationContextAccessor to manipulate the variation context. Scheduled for removal in V17.")]
|
||||
void SetRequestCulture(string culture);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IRequestSegmmentService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the requested segment from the "Accept-Segment" header, if present.
|
||||
/// </summary>
|
||||
string? GetRequestedSegment();
|
||||
}
|
||||
@@ -43,6 +43,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder.Services.AddSingleton<IValidateOptions<ContentSettings>, ContentSettingsValidator>();
|
||||
builder.Services.AddSingleton<IValidateOptions<GlobalSettings>, GlobalSettingsValidator>();
|
||||
builder.Services.AddSingleton<IValidateOptions<HealthChecksSettings>, HealthChecksSettingsValidator>();
|
||||
builder.Services.AddSingleton<IValidateOptions<LoggingSettings>, LoggingSettingsValidator>();
|
||||
builder.Services.AddSingleton<IValidateOptions<RequestHandlerSettings>, RequestHandlerSettingsValidator>();
|
||||
builder.Services.AddSingleton<IValidateOptions<UnattendedSettings>, UnattendedSettingsValidator>();
|
||||
builder.Services.AddSingleton<IValidateOptions<SecuritySettings>, SecuritySettingsValidator>();
|
||||
|
||||
@@ -11,17 +11,18 @@ public interface IFileType
|
||||
/// <value>
|
||||
/// <c>true</c> if the file can be set using a physical path; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
[Obsolete("An interface should not expose implementation details. Scheduled for removal in Umbraco 18.")]
|
||||
bool CanSetPhysical { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stream as an asynchronous operation.
|
||||
/// Gets the stream in an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="udi">The UDI.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// The task object representing the asynchronous operation.
|
||||
/// The task object representing the asynchronous operation. The task result contains the stream.
|
||||
/// </returns>
|
||||
Task<Stream> GetStreamAsync(StringUdi udi, CancellationToken token);
|
||||
Task<Stream> GetStreamAsync(StringUdi udi, CancellationToken token); // TODO: Rename token to cancellationToken and add default value
|
||||
|
||||
/// <summary>
|
||||
/// Gets the checksum stream.
|
||||
@@ -30,8 +31,22 @@ public interface IFileType
|
||||
/// <returns>
|
||||
/// The checksum stream.
|
||||
/// </returns>
|
||||
[Obsolete("Use GetChecksumStreamAsync() instead. Scheduled for removal in Umbraco 18.")]
|
||||
Stream GetChecksumStream(StringUdi udi);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the checksum stream in an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="udi">The UDI.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// The task object representing the asynchronous operation. The task result contains the checksum stream.
|
||||
/// </returns>
|
||||
Task<Stream> GetChecksumStreamAsync(StringUdi udi, CancellationToken cancellationToken = default)
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
=> Task.FromResult(GetChecksumStream(udi));
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file length in bytes or <c>-1</c> if not found.
|
||||
/// </summary>
|
||||
@@ -39,8 +54,22 @@ public interface IFileType
|
||||
/// <returns>
|
||||
/// The file length in bytes or <c>-1</c> if not found.
|
||||
/// </returns>
|
||||
[Obsolete("Use GetLengthAsync() instead. Scheduled for removal in Umbraco 18.")]
|
||||
long GetLength(StringUdi udi);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file length in bytes or <c>-1</c> if not found in an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="udi">The UDI.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// The task object representing the asynchronous operation. The task result contains the file length in bytes or <c>-1</c> if not found.
|
||||
/// </returns>
|
||||
Task<long> GetLengthAsync(StringUdi udi, CancellationToken cancellationToken = default)
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
=> Task.FromResult(GetLength(udi));
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
/// <summary>
|
||||
/// Sets the stream as an asynchronous operation.
|
||||
/// </summary>
|
||||
@@ -50,7 +79,7 @@ public interface IFileType
|
||||
/// <returns>
|
||||
/// The task object representing the asynchronous operation.
|
||||
/// </returns>
|
||||
Task SetStreamAsync(StringUdi udi, Stream stream, CancellationToken token);
|
||||
Task SetStreamAsync(StringUdi udi, Stream stream, CancellationToken token); // TODO: Rename token to cancellationToken and add default value
|
||||
|
||||
/// <summary>
|
||||
/// Sets the physical path of the file.
|
||||
@@ -58,20 +87,16 @@ public interface IFileType
|
||||
/// <param name="udi">The UDI.</param>
|
||||
/// <param name="physicalPath">The physical path.</param>
|
||||
/// <param name="copy">If set to <c>true</c> copies the file instead of moving.</param>
|
||||
[Obsolete("Use SetStreamAsync() instead to not rely on physical file paths. Scheduled for removal in Umbraco 18.")]
|
||||
void Set(StringUdi udi, string physicalPath, bool copy = false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the physical path or <see cref="string.Empty"/> if not found.
|
||||
/// Gets the path to the file, including the file name. Returns <see cref="string.Empty" /> if the file is not directly accessible.
|
||||
/// </summary>
|
||||
/// <param name="udi">The UDI.</param>
|
||||
/// <returns>
|
||||
/// The physical path or <see cref="string.Empty"/> if not found.
|
||||
/// The path to the file, including the file name or <see cref="string.Empty" /> if the file is not directly accessible.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This is not pretty as *everywhere* in Deploy we take care of ignoring
|
||||
/// the physical path and always rely on the virtual IFileSystem,
|
||||
/// but Cloud wants to add some of these files to Git and needs the path...
|
||||
/// </remarks>
|
||||
string GetPhysicalPath(StringUdi udi);
|
||||
|
||||
/// <summary>
|
||||
@@ -81,5 +106,6 @@ public interface IFileType
|
||||
/// <returns>
|
||||
/// The virtual path or <see cref="string.Empty"/> if not found.
|
||||
/// </returns>
|
||||
[Obsolete("This is not used anymore. Scheduled for removal in Umbraco 18.")]
|
||||
string GetVirtualPath(StringUdi udi);
|
||||
}
|
||||
|
||||
@@ -377,7 +377,7 @@
|
||||
<key alias="invalidEmpty">Value cannot be empty</key>
|
||||
<key alias="invalidPattern">Value is invalid, it does not match the correct pattern</key>
|
||||
<key alias="entriesShort"><![CDATA[Minimum %0% entries, requires <strong>%1%</strong> more.]]></key>
|
||||
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, <strong>%1%</strong> too many.]]></key>
|
||||
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, you have entered <strong>%1%</strong> too many.]]></key>
|
||||
<key alias="stringLengthExceeded">The string length exceeds the maximum length of %0% characters, %1% too many.</key>
|
||||
<key alias="entriesAreasMismatch">The content amount requirements are not met for one or more areas.</key>
|
||||
<key alias="invalidMemberGroupName">Invalid member group name</key>
|
||||
|
||||
@@ -394,7 +394,7 @@
|
||||
<key alias="unexpectedRange">The value %0% is not expected to contain a range</key>
|
||||
<key alias="invalidRange">The value %0% is not expected to have a to value less than the from value</key>
|
||||
<key alias="entriesShort"><![CDATA[Minimum %0% entries, requires <strong>%1%</strong> more.]]></key>
|
||||
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, <strong>%1%</strong> too many.]]></key>
|
||||
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, you have entered <strong>%1%</strong> too many.]]></key>
|
||||
<key alias="stringLengthExceeded">The string length exceeds the maximum length of %0% characters, %1% too many.</key>
|
||||
<key alias="entriesAreasMismatch">The content amount requirements are not met for one or more areas.</key>
|
||||
<key alias="invalidMediaType">The chosen media type is invalid.</key>
|
||||
|
||||
@@ -245,13 +245,13 @@ public static class ContentExtensions
|
||||
}
|
||||
|
||||
IEnumerable<ContentSchedule> expires = contentSchedule.GetSchedule(culture, ContentScheduleAction.Expire);
|
||||
if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date))
|
||||
if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.UtcNow > x.Date))
|
||||
{
|
||||
return ContentStatus.Expired;
|
||||
}
|
||||
|
||||
IEnumerable<ContentSchedule> release = contentSchedule.GetSchedule(culture, ContentScheduleAction.Release);
|
||||
if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now))
|
||||
if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.UtcNow))
|
||||
{
|
||||
return ContentStatus.AwaitingRelease;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,17 @@ namespace Umbraco.Cms.Core.Logging;
|
||||
public interface ILoggingConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the physical path where logs are stored
|
||||
/// Gets the physical path where logs are stored.
|
||||
/// </summary>
|
||||
string LogDirectory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file name format for the log files.
|
||||
/// </summary>
|
||||
string LogFileNameFormat { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file name format arguments for the log files.
|
||||
/// </summary>
|
||||
string[] GetLogFileNameFormatArguments();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,73 @@
|
||||
namespace Umbraco.Cms.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="ILoggingConfiguration"/> to provide configuration for logging to files.
|
||||
/// </summary>
|
||||
public class LoggingConfiguration : ILoggingConfiguration
|
||||
{
|
||||
public LoggingConfiguration(string logDirectory) =>
|
||||
LogDirectory = logDirectory ?? throw new ArgumentNullException(nameof(logDirectory));
|
||||
/// <summary>
|
||||
/// The default log file name format.
|
||||
/// </summary>
|
||||
public const string DefaultLogFileNameFormat = "UmbracoTraceLog.{0}..json";
|
||||
|
||||
/// <summary>
|
||||
/// The default log file name format arguments.
|
||||
/// </summary>
|
||||
public const string DefaultLogFileNameFormatArguments = MachineNameFileFormatArgument;
|
||||
|
||||
/// <summary>
|
||||
/// The collection of supported file name format arguments.
|
||||
/// </summary>
|
||||
public static readonly string[] SupportedFileNameFormatArguments =
|
||||
{
|
||||
MachineNameFileFormatArgument,
|
||||
EnvironmentNameFileFormatArgument,
|
||||
};
|
||||
|
||||
private readonly string _logFileNameFormatArguments;
|
||||
|
||||
private const string MachineNameFileFormatArgument = "MachineName";
|
||||
private const string EnvironmentNameFileFormatArgument = "EnvironmentName";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LoggingConfiguration"/> class with the default log file name format and arguments.
|
||||
/// </summary>
|
||||
/// <param name="logDirectory">The log file directory.</param>
|
||||
public LoggingConfiguration(string logDirectory)
|
||||
: this(logDirectory, DefaultLogFileNameFormat, DefaultLogFileNameFormatArguments)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LoggingConfiguration"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logDirectory">The log file directory.</param>
|
||||
/// <param name="logFileNameFormat">The log file name format.</param>
|
||||
/// <param name="logFileNameFormatArguments">The log file name format arguments as a comma delimited string.</param>
|
||||
public LoggingConfiguration(string logDirectory, string logFileNameFormat, string logFileNameFormatArguments)
|
||||
{
|
||||
LogDirectory = logDirectory;
|
||||
LogFileNameFormat = logFileNameFormat;
|
||||
_logFileNameFormatArguments = logFileNameFormatArguments;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string LogDirectory { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string LogFileNameFormat { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string[] GetLogFileNameFormatArguments() => _logFileNameFormatArguments.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => x.Trim())
|
||||
.Select(GetValue)
|
||||
.ToArray();
|
||||
|
||||
private static string GetValue(string arg) =>
|
||||
arg switch
|
||||
{
|
||||
MachineNameFileFormatArgument => Environment.MachineName,
|
||||
EnvironmentNameFileFormatArgument => Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production",
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
public abstract class ContentEditingModelBase
|
||||
{
|
||||
public string? InvariantName { get; set; }
|
||||
|
||||
public IEnumerable<PropertyValueModel> InvariantProperties { get; set; } = Array.Empty<PropertyValueModel>();
|
||||
public IEnumerable<PropertyValueModel> Properties { get; set; } = Array.Empty<PropertyValueModel>();
|
||||
|
||||
public IEnumerable<VariantModel> Variants { get; set; } = Array.Empty<VariantModel>();
|
||||
}
|
||||
|
||||
@@ -5,4 +5,8 @@ public class PropertyValueModel
|
||||
public required string Alias { get; set; }
|
||||
|
||||
public required object? Value { get; set; }
|
||||
|
||||
public string? Culture { get; set; }
|
||||
|
||||
public string? Segment { get; set; }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,4 @@ public class VariantModel
|
||||
public string? Segment { get; set; }
|
||||
|
||||
public required string Name { get; set; }
|
||||
|
||||
public required IEnumerable<PropertyValueModel> Properties { get; set; }
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public class ContentVersionMeta
|
||||
|
||||
public int UserId { get; }
|
||||
|
||||
public DateTime VersionDate { get; }
|
||||
public DateTime VersionDate { get; private set; }
|
||||
|
||||
public bool CurrentPublishedVersion { get; }
|
||||
|
||||
@@ -47,5 +47,7 @@ public class ContentVersionMeta
|
||||
|
||||
public string? Username { get; }
|
||||
|
||||
public void SpecifyVersionDateKind(DateTimeKind kind) => VersionDate = DateTime.SpecifyKind(VersionDate, kind);
|
||||
|
||||
public override string ToString() => $"ContentVersionMeta(versionId: {VersionId}, versionDate: {VersionDate:s}";
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ public sealed class PropertyValidationContext
|
||||
|
||||
public required IEnumerable<string> CulturesBeingValidated { get; init; }
|
||||
|
||||
public required IEnumerable<string> SegmentsBeingValidated { get; init; }
|
||||
public required IEnumerable<string?> SegmentsBeingValidated { get; init; }
|
||||
|
||||
public static PropertyValidationContext Empty() => new()
|
||||
{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
@@ -35,6 +37,21 @@ internal sealed class ContentBlueprintEditingService
|
||||
return Task.FromResult(blueprint);
|
||||
}
|
||||
|
||||
public Task<IContent?> GetScaffoldedAsync(Guid key)
|
||||
{
|
||||
IContent? blueprint = ContentService.GetBlueprintById(key);
|
||||
if (blueprint is null)
|
||||
{
|
||||
return Task.FromResult<IContent?>(null);
|
||||
}
|
||||
|
||||
using ICoreScope scope = CoreScopeProvider.CreateCoreScope();
|
||||
scope.Notifications.Publish(new ContentScaffoldedNotification(blueprint, blueprint, Constants.System.Root, new EventMessages()));
|
||||
scope.Complete();
|
||||
|
||||
return Task.FromResult<IContent?>(blueprint);
|
||||
}
|
||||
|
||||
public async Task<Attempt<PagedModel<IContent>?, ContentEditingOperationStatus>> GetPagedByContentTypeAsync(Guid contentTypeKey, int skip, int take)
|
||||
{
|
||||
IContentType? contentType = await ContentTypeService.GetAsync(contentTypeKey);
|
||||
@@ -69,7 +86,7 @@ internal sealed class ContentBlueprintEditingService
|
||||
|
||||
IContent blueprint = result.Result.Content!;
|
||||
|
||||
if (ValidateUniqueName(createModel.InvariantName ?? string.Empty, blueprint) is false)
|
||||
if (ValidateUniqueNames(createModel.Variants, blueprint) is false)
|
||||
{
|
||||
return Attempt.FailWithStatus(ContentEditingOperationStatus.DuplicateName, new ContentCreateResult());
|
||||
}
|
||||
@@ -116,7 +133,7 @@ internal sealed class ContentBlueprintEditingService
|
||||
return Attempt.FailWithStatus(ContentEditingOperationStatus.NotFound, new ContentUpdateResult());
|
||||
}
|
||||
|
||||
if (ValidateUniqueName(updateModel.InvariantName ?? string.Empty, blueprint) is false)
|
||||
if (ValidateUniqueNames(updateModel.Variants, blueprint) is false)
|
||||
{
|
||||
return Attempt.FailWithStatus(ContentEditingOperationStatus.DuplicateName, new ContentUpdateResult());
|
||||
}
|
||||
@@ -232,4 +249,18 @@ internal sealed class ContentBlueprintEditingService
|
||||
IEnumerable<IContent> existing = ContentService.GetBlueprintsForContentTypes(content.ContentTypeId);
|
||||
return existing.Any(c => c.Name == name && c.Id != content.Id) is false;
|
||||
}
|
||||
|
||||
private bool ValidateUniqueNames(IEnumerable<VariantModel> variants, IContent content)
|
||||
{
|
||||
IContent[] existing = ContentService.GetBlueprintsForContentTypes(content.ContentTypeId).ToArray();
|
||||
foreach (VariantModel variant in variants)
|
||||
{
|
||||
if (existing.Any(c => c.GetCultureName(variant.Culture) == variant.Name && c.Id != content.Id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,34 +318,36 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
|
||||
return null;
|
||||
}
|
||||
|
||||
if (contentType.VariesByCulture() == false)
|
||||
if (contentType.VariesByNothing() && contentEditingModelBase.Variants.Any(v => v.Culture is null && v.Segment is null) is false)
|
||||
{
|
||||
if (contentEditingModelBase.InvariantName.IsNullOrWhiteSpace() || contentEditingModelBase.Variants.Any())
|
||||
{
|
||||
operationStatus = ContentEditingOperationStatus.ContentTypeCultureVarianceMismatch;
|
||||
return null;
|
||||
}
|
||||
// does not vary by anything and is missing the invariant name = invalid
|
||||
operationStatus = ContentEditingOperationStatus.ContentTypeCultureVarianceMismatch;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (contentType.VariesByCulture() && contentEditingModelBase.Variants.Any(v => v.Culture is null))
|
||||
{
|
||||
// varies by culture with one or more variants not bound to a culture = invalid
|
||||
operationStatus = ContentEditingOperationStatus.ContentTypeCultureVarianceMismatch;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (contentType.VariesBySegment() && contentEditingModelBase.Variants.Any(v => v.Segment is null) is false)
|
||||
{
|
||||
// varies by segment with no default segment variants = invalid
|
||||
operationStatus = ContentEditingOperationStatus.ContentTypeSegmentVarianceMismatch;
|
||||
return null;
|
||||
}
|
||||
|
||||
var propertyTypesByAlias = contentType.CompositionPropertyTypes.ToDictionary(pt => pt.Alias);
|
||||
var propertyValuesAndVariance = contentEditingModelBase
|
||||
.InvariantProperties
|
||||
.Properties
|
||||
.Select(pv => new
|
||||
{
|
||||
VariesByCulture = false,
|
||||
VariesBySegment = false,
|
||||
VariesByCulture = pv.Culture is not null,
|
||||
VariesBySegment = pv.Segment is not null,
|
||||
PropertyValue = pv
|
||||
})
|
||||
.Union(contentEditingModelBase
|
||||
.Variants
|
||||
.SelectMany(v => v
|
||||
.Properties
|
||||
.Select(vpv => new
|
||||
{
|
||||
VariesByCulture = true,
|
||||
VariesBySegment = v.Segment.IsNullOrWhiteSpace() == false,
|
||||
PropertyValue = vpv
|
||||
})))
|
||||
.ToArray();
|
||||
|
||||
// verify that all property values are defined as property types
|
||||
@@ -359,7 +361,8 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
|
||||
if (propertyValuesAndVariance.Any(pv =>
|
||||
{
|
||||
IPropertyType propertyType = propertyTypesByAlias[pv.PropertyValue.Alias];
|
||||
return propertyType.VariesByCulture() != pv.VariesByCulture || propertyType.VariesBySegment() != pv.VariesBySegment;
|
||||
return (propertyType.VariesByCulture() != pv.VariesByCulture)
|
||||
|| (propertyType.VariesBySegment() is false && pv.VariesBySegment);
|
||||
}))
|
||||
{
|
||||
operationStatus = ContentEditingOperationStatus.PropertyTypeNotFound;
|
||||
@@ -424,10 +427,16 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
|
||||
content.SetCultureName(name, culture);
|
||||
}
|
||||
}
|
||||
else if (contentType.VariesBySegment())
|
||||
{
|
||||
// this should be validated already so it's OK to throw an exception here
|
||||
content.Name = contentEditingModelBase.Variants.FirstOrDefault(v => v.Segment is null)?.Name
|
||||
?? throw new ArgumentException("Could not find the default segment variant", nameof(contentEditingModelBase));
|
||||
}
|
||||
else
|
||||
{
|
||||
// this should be validated already so it's OK to throw an exception here
|
||||
content.Name = contentEditingModelBase.InvariantName
|
||||
content.Name = contentEditingModelBase.Variants.FirstOrDefault(v => v.Culture is null && v.Segment is null)?.Name
|
||||
?? throw new ArgumentException("Could not find a culture invariant variant", nameof(contentEditingModelBase));
|
||||
}
|
||||
}
|
||||
@@ -437,26 +446,19 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
|
||||
// create a mapping dictionary for all content type property types by their property aliases
|
||||
Dictionary<string, IPropertyType> propertyTypesByAlias = GetPropertyTypesByAlias(contentType);
|
||||
|
||||
// flatten the invariant and variant property values from the model into one array, and remove any properties
|
||||
// that do not exist on the content type
|
||||
var propertyValues = contentEditingModelBase
|
||||
.InvariantProperties
|
||||
.Select(pv => new { Culture = (string?)null, Segment = (string?)null, Alias = pv.Alias, Value = pv.Value })
|
||||
.Union(contentEditingModelBase
|
||||
.Variants
|
||||
.SelectMany(v => v
|
||||
.Properties
|
||||
.Select(vpv => new { Culture = v.Culture, Segment = v.Segment, Alias = vpv.Alias, Value = vpv.Value })))
|
||||
// remove any properties that do not exist on the content type
|
||||
PropertyValueModel[] propertyValues = contentEditingModelBase
|
||||
.Properties
|
||||
.Where(propertyValue => propertyTypesByAlias.ContainsKey(propertyValue.Alias))
|
||||
.ToArray();
|
||||
|
||||
// update all properties on the content item
|
||||
foreach (var propertyValue in propertyValues)
|
||||
foreach (PropertyValueModel propertyValue in propertyValues)
|
||||
{
|
||||
// the following checks should already have been validated by now, so it's OK to throw exceptions here
|
||||
if(propertyTypesByAlias.TryGetValue(propertyValue.Alias, out IPropertyType? propertyType) == false
|
||||
|| (propertyType.VariesByCulture() && propertyValue.Culture.IsNullOrWhiteSpace())
|
||||
|| (propertyType.VariesBySegment() && propertyValue.Segment.IsNullOrWhiteSpace()))
|
||||
|| (propertyType.VariesBySegment() is false && propertyValue.Segment.IsNullOrWhiteSpace() is false))
|
||||
{
|
||||
throw new ArgumentException($"Culture or segment variance mismatch for property: {propertyValue.Alias}", nameof(contentEditingModelBase));
|
||||
}
|
||||
@@ -472,8 +474,8 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
|
||||
// create a mapping dictionary for all content type property types by their property aliases
|
||||
Dictionary<string, IPropertyType> propertyTypesByAlias = GetPropertyTypesByAlias(contentType);
|
||||
var knownPropertyAliases = contentEditingModelBase
|
||||
.InvariantProperties.Select(pv => pv.Alias)
|
||||
.Union(contentEditingModelBase.Variants.SelectMany(v => v.Properties.Select(vpv => vpv.Alias)))
|
||||
.Properties
|
||||
.Select(pv => pv.Alias)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
|
||||
@@ -238,28 +238,33 @@ internal sealed class ContentPublishingService : IContentPublishingService
|
||||
|
||||
private async Task<ContentValidationResult> ValidateCurrentContentAsync(IContent content, string[] cultures)
|
||||
{
|
||||
IEnumerable<string?> effectiveCultures = content.ContentType.VariesByCulture()
|
||||
? cultures.Union([null])
|
||||
: [null];
|
||||
|
||||
// Would be better to be able to use a mapper/factory, but currently all that functionality is very much presentation logic.
|
||||
var model = new ContentUpdateModel()
|
||||
{
|
||||
InvariantName = content.Name,
|
||||
// NOTE KJA: this needs redoing; we need to make an informed decision whether to include invariant properties, depending on if editing invariant properties is allowed on all variants, or if the default language is included in cultures
|
||||
InvariantProperties = content.Properties.Where(x => x.PropertyType.VariesByCulture() is false).Select(x => new PropertyValueModel()
|
||||
{
|
||||
Alias = x.Alias,
|
||||
Value = x.GetValue()
|
||||
}),
|
||||
Properties = effectiveCultures.SelectMany(culture =>
|
||||
content.Properties.Select(property => property.PropertyType.VariesByCulture() == (culture is not null)
|
||||
? new PropertyValueModel
|
||||
{
|
||||
Alias = property.Alias,
|
||||
Value = property.GetValue(culture: culture, segment: null, published: false),
|
||||
Culture = culture
|
||||
}
|
||||
: null)
|
||||
.WhereNotNull())
|
||||
.ToArray(),
|
||||
Variants = cultures.Select(culture => new VariantModel()
|
||||
{
|
||||
Name = content.GetPublishName(culture) ?? string.Empty,
|
||||
Culture = culture,
|
||||
Segment = null,
|
||||
Properties = content.Properties.Where(prop => prop.PropertyType.VariesByCulture()).Select(prop => new PropertyValueModel()
|
||||
{
|
||||
Alias = prop.Alias,
|
||||
Value = prop.GetValue(culture: culture, segment: null, published: false)
|
||||
})
|
||||
})
|
||||
Segment = null
|
||||
}).ToArray()
|
||||
};
|
||||
|
||||
IContentType? contentType = _contentTypeService.Get(content.ContentType.Key)!;
|
||||
ContentValidationResult validationResult = await _contentValidationService.ValidatePropertiesAsync(model, contentType, cultures);
|
||||
return validationResult;
|
||||
|
||||
@@ -322,6 +322,15 @@ internal abstract class ContentTypeEditingServiceBase<TContentType, TContentType
|
||||
// get the content type keys we want to use for compositions
|
||||
Guid[] compositionKeys = KeysForCompositionTypes(model, CompositionType.Composition);
|
||||
|
||||
// if the content type keys are already set as compositions, don't perform any additional validation
|
||||
// - this covers an edge case where compositions are configured for a content type before child content types are created
|
||||
if (contentType is not null && contentType.ContentTypeComposition
|
||||
.Select(c => c.Key)
|
||||
.ContainsAll(compositionKeys))
|
||||
{
|
||||
return ContentTypeOperationStatus.Success;
|
||||
}
|
||||
|
||||
// verify that all compositions keys are allowed
|
||||
Guid[] allowedCompositionKeys = _contentTypeService.GetAvailableCompositeContentTypes(contentType, allContentTypeCompositions, isElement: model.IsElement)
|
||||
.Results
|
||||
|
||||
@@ -31,9 +31,17 @@ internal abstract class ContentValidationServiceBase<TContentType>
|
||||
|
||||
IPropertyType[] contentTypePropertyTypes = contentType.CompositionPropertyTypes.ToArray();
|
||||
IPropertyType[] invariantPropertyTypes = contentTypePropertyTypes
|
||||
.Where(propertyType => propertyType.VariesByNothing())
|
||||
.Where(propertyType => propertyType.Variations == ContentVariation.Nothing)
|
||||
.ToArray();
|
||||
IPropertyType[] cultureVariantPropertyTypes = contentTypePropertyTypes
|
||||
.Where(propertyType => propertyType.Variations == ContentVariation.Culture)
|
||||
.ToArray();
|
||||
IPropertyType[] segmentVariantPropertyTypes = contentTypePropertyTypes
|
||||
.Where(propertyType => propertyType.Variations == ContentVariation.Segment)
|
||||
.ToArray();
|
||||
IPropertyType[] cultureAndSegmentVariantPropertyTypes = contentTypePropertyTypes
|
||||
.Where(propertyType => propertyType.Variations == ContentVariation.CultureAndSegment)
|
||||
.ToArray();
|
||||
IPropertyType[] variantPropertyTypes = contentTypePropertyTypes.Except(invariantPropertyTypes).ToArray();
|
||||
|
||||
var cultures = culturesToValidate?.WhereNotNull().Except(["*"]).ToArray();
|
||||
if (cultures?.Any() is not true)
|
||||
@@ -42,11 +50,14 @@ internal abstract class ContentValidationServiceBase<TContentType>
|
||||
}
|
||||
|
||||
// we don't have any managed segments, so we have to make do with the ones passed in the model
|
||||
var segments = contentEditingModelBase.Variants
|
||||
.Where(variant => variant.Culture is null || cultures.Contains(variant.Culture))
|
||||
.DistinctBy(variant => variant.Segment).Select(variant => variant.Segment)
|
||||
.WhereNotNull()
|
||||
.ToArray();
|
||||
var segments =
|
||||
new string?[] { null }
|
||||
.Union(contentEditingModelBase.Variants
|
||||
.Where(variant => variant.Culture is null || cultures.Contains(variant.Culture))
|
||||
.DistinctBy(variant => variant.Segment).Select(variant => variant.Segment)
|
||||
.WhereNotNull()
|
||||
)
|
||||
.ToArray();
|
||||
|
||||
foreach (IPropertyType propertyType in invariantPropertyTypes)
|
||||
{
|
||||
@@ -55,16 +66,45 @@ internal abstract class ContentValidationServiceBase<TContentType>
|
||||
Culture = null, Segment = null, CulturesBeingValidated = cultures, SegmentsBeingValidated = segments
|
||||
};
|
||||
|
||||
PropertyValueModel? propertyValueModel = contentEditingModelBase.InvariantProperties.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias);
|
||||
PropertyValueModel? propertyValueModel = contentEditingModelBase
|
||||
.Properties
|
||||
.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias && propertyValue.Culture is null && propertyValue.Segment is null);
|
||||
validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext));
|
||||
}
|
||||
|
||||
if (variantPropertyTypes.Any() is false)
|
||||
foreach (IPropertyType propertyType in cultureVariantPropertyTypes)
|
||||
{
|
||||
return new ContentValidationResult { ValidationErrors = validationErrors };
|
||||
foreach (var culture in cultures)
|
||||
{
|
||||
var validationContext = new PropertyValidationContext
|
||||
{
|
||||
Culture = culture, Segment = null, CulturesBeingValidated = cultures, SegmentsBeingValidated = segments
|
||||
};
|
||||
|
||||
PropertyValueModel? propertyValueModel = contentEditingModelBase
|
||||
.Properties
|
||||
.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias && propertyValue.Culture.InvariantEquals(culture) && propertyValue.Segment is null);
|
||||
validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (IPropertyType propertyType in variantPropertyTypes)
|
||||
foreach (IPropertyType propertyType in segmentVariantPropertyTypes)
|
||||
{
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
var validationContext = new PropertyValidationContext
|
||||
{
|
||||
Culture = null, Segment = segment, CulturesBeingValidated = cultures, SegmentsBeingValidated = segments
|
||||
};
|
||||
|
||||
PropertyValueModel? propertyValueModel = contentEditingModelBase
|
||||
.Properties
|
||||
.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias && propertyValue.Culture is null && propertyValue.Segment.InvariantEquals(segment));
|
||||
validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (IPropertyType propertyType in cultureAndSegmentVariantPropertyTypes)
|
||||
{
|
||||
foreach (var culture in cultures)
|
||||
{
|
||||
@@ -76,10 +116,8 @@ internal abstract class ContentValidationServiceBase<TContentType>
|
||||
};
|
||||
|
||||
PropertyValueModel? propertyValueModel = contentEditingModelBase
|
||||
.Variants
|
||||
.FirstOrDefault(variant => string.Equals(variant.Culture, culture, StringComparison.InvariantCultureIgnoreCase) && string.Equals(segment, variant.Segment, StringComparison.InvariantCultureIgnoreCase))?
|
||||
.Properties
|
||||
.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias);
|
||||
.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias && propertyValue.Culture.InvariantEquals(culture) && propertyValue.Segment.InvariantEquals(segment));
|
||||
validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ public interface IContentBlueprintEditingService
|
||||
{
|
||||
Task<IContent?> GetAsync(Guid key);
|
||||
|
||||
Task<IContent?> GetScaffoldedAsync(Guid key) => Task.FromResult<IContent?>(null);
|
||||
|
||||
Task<Attempt<PagedModel<IContent>?, ContentEditingOperationStatus>> GetPagedByContentTypeAsync(
|
||||
Guid contentTypeKey,
|
||||
int skip,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
public interface IContentSearchService : IContentSearchService<IContent>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
public interface IContentSearchService<TContent>
|
||||
where TContent : class, IContentBase
|
||||
{
|
||||
Task<PagedModel<TContent>> SearchChildrenAsync(
|
||||
string? query,
|
||||
Guid? parentId,
|
||||
Ordering? ordering,
|
||||
int skip = 0,
|
||||
int take = 100);
|
||||
}
|
||||
@@ -25,6 +25,7 @@ public interface IIndexedEntitySearchService
|
||||
=> Search(objectType,query, skip, take, ignoreUserStartNodes);
|
||||
|
||||
// default implementation to avoid breaking changes falls back to old behaviour
|
||||
[Obsolete("Please use the async version of this method, SearchAsync. Scheduled for removal in V17.")]
|
||||
PagedModel<IEntitySlim> Search(
|
||||
UmbracoObjectTypes objectType,
|
||||
string query,
|
||||
@@ -35,4 +36,14 @@ public interface IIndexedEntitySearchService
|
||||
int take = 100,
|
||||
bool ignoreUserStartNodes = false)
|
||||
=> Search(objectType,query, skip, take, ignoreUserStartNodes);
|
||||
|
||||
Task<PagedModel<IEntitySlim>> SearchAsync(
|
||||
UmbracoObjectTypes objectType,
|
||||
string query,
|
||||
Guid? parentId,
|
||||
IEnumerable<Guid>? contentTypeIds,
|
||||
bool? trashed,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
bool ignoreUserStartNodes = false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
public interface IMediaSearchService : IContentSearchService<IMedia>
|
||||
{
|
||||
}
|
||||
@@ -123,8 +123,7 @@ internal sealed class MemberContentEditingService
|
||||
|
||||
var sensitivePropertyAliases = memberType.GetSensitivePropertyTypeAliases().ToArray();
|
||||
return updateModel
|
||||
.InvariantProperties
|
||||
.Union(updateModel.Variants.SelectMany(variant => variant.Properties))
|
||||
.Properties
|
||||
.Select(property => property.Alias)
|
||||
.Intersect(sensitivePropertyAliases, StringComparer.OrdinalIgnoreCase)
|
||||
.Any() is false;
|
||||
|
||||
@@ -6,6 +6,7 @@ public enum ContentEditingOperationStatus
|
||||
CancelledByNotification,
|
||||
ContentTypeNotFound,
|
||||
ContentTypeCultureVarianceMismatch,
|
||||
ContentTypeSegmentVarianceMismatch,
|
||||
NotFound,
|
||||
ParentNotFound,
|
||||
ParentInvalid,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
@@ -67,19 +65,14 @@ internal sealed class PublicAccessService : RepositoryService, IPublicAccessServ
|
||||
{
|
||||
// Get all ids in the path for the content item and ensure they all
|
||||
// parse to ints that are not -1.
|
||||
var ids = contentPath.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val) ? val : -1)
|
||||
.Where(x => x != -1)
|
||||
.ToList();
|
||||
|
||||
// start with the deepest id
|
||||
ids.Reverse();
|
||||
// Start with the deepest id.
|
||||
IEnumerable<int> ids = contentPath.GetIdsFromPathReversed().Where(x => x != -1);
|
||||
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
{
|
||||
// This will retrieve from cache!
|
||||
var entries = _publicAccessRepository.GetMany().ToList();
|
||||
foreach (var id in CollectionsMarshal.AsSpan(ids))
|
||||
foreach (var id in ids)
|
||||
{
|
||||
PublicAccessEntry? found = entries.Find(x => x.ProtectedNodeId == id);
|
||||
if (found != null)
|
||||
@@ -286,7 +279,7 @@ internal sealed class PublicAccessService : RepositoryService, IPublicAccessServ
|
||||
return Attempt.FailWithStatus(PublicAccessOperationStatus.NoAllowedEntities, result);
|
||||
}
|
||||
|
||||
if(entry.MemberUserNames.Any() && entry.MemberGroupNames.Any())
|
||||
if (entry.MemberUserNames.Any() && entry.MemberGroupNames.Any())
|
||||
{
|
||||
return Attempt.FailWithStatus(PublicAccessOperationStatus.AmbiguousRule, result);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,8 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder.Services.AddUnique<IContentTypeSearchService, ContentTypeSearchService>();
|
||||
builder.Services.AddUnique<IIndexedEntitySearchService, IndexedEntitySearchService>();
|
||||
builder.Services.TryAddTransient<IReservedFieldNamesService, ReservedFieldNamesService>();
|
||||
builder.Services.AddUnique<IContentSearchService, ContentSearchService>();
|
||||
builder.Services.AddUnique<IMediaSearchService, MediaSearchService>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace Umbraco.Extensions
|
||||
.Enrich.FromLogContext(); // allows us to dynamically enrich
|
||||
|
||||
logConfig.WriteTo.UmbracoFile(
|
||||
path: umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory),
|
||||
path: umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory, loggingConfiguration.LogFileNameFormat, loggingConfiguration.GetLogFileNameFormatArguments()),
|
||||
fileSizeLimitBytes: umbracoFileConfiguration.FileSizeLimitBytes,
|
||||
restrictedToMinimumLevel: umbracoFileConfiguration.RestrictedToMinimumLevel,
|
||||
rollingInterval: umbracoFileConfiguration.RollingInterval,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Logging.Serilog;
|
||||
|
||||
@@ -43,5 +44,8 @@ public class UmbracoFileConfiguration
|
||||
public int RetainedFileCountLimit { get; set; } = 31;
|
||||
|
||||
public string GetPath(string logDirectory) =>
|
||||
Path.Combine(logDirectory, $"UmbracoTraceLog.{Environment.MachineName}..json");
|
||||
GetPath(logDirectory, LoggingConfiguration.DefaultLogFileNameFormat, Environment.MachineName);
|
||||
|
||||
public string GetPath(string logDirectory, string fileNameFormat, params string[] fileNameArgs) =>
|
||||
Path.Combine(logDirectory, string.Format(fileNameFormat, fileNameArgs));
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ internal class AccessDto
|
||||
[ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id2")]
|
||||
public int NoAccessNodeId { get; set; }
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
[Column("updateDate")]
|
||||
[Column("updateDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@ internal class AccessRuleDto
|
||||
[Column("ruleType")]
|
||||
public string? RuleType { get; set; }
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
[Column("updateDate")]
|
||||
[Column("updateDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime UpdateDate { get; set; }
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ internal class AuditEntryDto
|
||||
[Length(Constants.Audit.IpLength)]
|
||||
public string? PerformingIp { get; set; }
|
||||
|
||||
[Column("eventDateUtc")]
|
||||
[Column("eventDateUtc", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime EventDateUtc { get; set; }
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ public class ConsentDto
|
||||
[Length(512)]
|
||||
public string? Action { get; set; }
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ internal class ContentScheduleDto
|
||||
[NullSetting(NullSetting = NullSettings.Null)] // can be invariant
|
||||
public int? LanguageId { get; set; }
|
||||
|
||||
// NOTE: this date is explicitly stored and treated as UTC despite the lack of "Utc" postfix.
|
||||
[Column("date")]
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
|
||||
@@ -27,6 +27,6 @@ internal class ContentVersionCleanupPolicyDto
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public int? KeepLatestVersionPerDayForDays { get; set; }
|
||||
|
||||
[Column("updated")]
|
||||
[Column("updated", ForceToUtc = false)]
|
||||
public DateTime Updated { get; set; }
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ internal class ContentVersionCultureVariationDto
|
||||
[Column("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[Column("date")] // TODO: db rename to 'updateDate'
|
||||
[Column("date", ForceToUtc = false)] // TODO: db rename to 'updateDate'
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
[Column("availableUserId")] // TODO: db rename to 'updateDate'
|
||||
|
||||
@@ -22,7 +22,7 @@ public class ContentVersionDto
|
||||
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current", IncludeColumns = "id,versionDate,text,userId,preventCleanup")]
|
||||
public int NodeId { get; set; }
|
||||
|
||||
[Column("versionDate")] // TODO: db rename to 'updateDate'
|
||||
[Column("versionDate", ForceToUtc = false)] // TODO: db rename to 'updateDate'
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime VersionDate { get; set; }
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ public class CreatedPackageSchemaDto
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
public string Value { get; set; } = null!;
|
||||
|
||||
[Column("updateDate")]
|
||||
[Column("updateDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
|
||||
@@ -20,6 +20,6 @@ internal class DocumentPublishedReadOnlyDto
|
||||
[Column("newest")]
|
||||
public bool Newest { get; set; }
|
||||
|
||||
[Column("updateDate")]
|
||||
[Column("updateDate", ForceToUtc = false)]
|
||||
public DateTime VersionDate { get; set; }
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ internal class ExternalLoginDto
|
||||
[Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", Name = "IX_" + TableName + "_ProviderKey")]
|
||||
public string ProviderKey { get; set; } = null!;
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ internal class ExternalLoginTokenDto
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
public string Value { get; set; } = null!;
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ internal class KeyValueDto
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public string? Value { get; set; }
|
||||
|
||||
[Column("updated")]
|
||||
[Column("updated", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ internal class LogDto
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public string? EntityType { get; set; }
|
||||
|
||||
[Column("Datestamp")]
|
||||
[Column("Datestamp", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_datestamp", ForColumns = "Datestamp,userId,NodeId")]
|
||||
public DateTime Datestamp { get; set; }
|
||||
|
||||
@@ -45,7 +45,7 @@ internal class MemberDto
|
||||
[Length(255)]
|
||||
public string? SecurityStampToken { get; set; }
|
||||
|
||||
[Column("emailConfirmedDate")]
|
||||
[Column("emailConfirmedDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? EmailConfirmedDate { get; set; }
|
||||
|
||||
@@ -62,15 +62,15 @@ internal class MemberDto
|
||||
[Constraint(Default = 1)]
|
||||
public bool IsApproved { get; set; }
|
||||
|
||||
[Column("lastLoginDate")]
|
||||
[Column("lastLoginDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? LastLoginDate { get; set; }
|
||||
|
||||
[Column("lastLockoutDate")]
|
||||
[Column("lastLockoutDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? LastLockoutDate { get; set; }
|
||||
|
||||
[Column("lastPasswordChangeDate")]
|
||||
[Column("lastPasswordChangeDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? LastPasswordChangeDate { get; set; }
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ public class NodeDto
|
||||
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType", ForColumns = "nodeObjectType,trashed", IncludeColumns = "uniqueId,parentId,level,path,sortOrder,nodeUser,text,createDate")]
|
||||
public Guid? NodeObjectType { get; set; }
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; }
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ internal class RelationDto
|
||||
[ForeignKey(typeof(RelationTypeDto))]
|
||||
public int RelationType { get; set; }
|
||||
|
||||
[Column("datetime")]
|
||||
[Column("datetime", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime Datetime { get; set; }
|
||||
|
||||
|
||||
@@ -23,11 +23,11 @@ internal class ServerRegistrationDto
|
||||
[Index(IndexTypes.UniqueNonClustered, Name = "IX_computerName")] // server identity is unique
|
||||
public string? ServerIdentity { get; set; }
|
||||
|
||||
[Column("registeredDate")]
|
||||
[Column("registeredDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime DateRegistered { get; set; }
|
||||
|
||||
[Column("lastNotifiedDate")]
|
||||
[Column("lastNotifiedDate", ForceToUtc = false)]
|
||||
public DateTime DateAccessed { get; set; }
|
||||
|
||||
[Column("isActive")]
|
||||
|
||||
@@ -73,32 +73,32 @@ public class UserDto
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public int? FailedLoginAttempts { get; set; }
|
||||
|
||||
[Column("lastLockoutDate")]
|
||||
[Column("lastLockoutDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? LastLockoutDate { get; set; }
|
||||
|
||||
[Column("lastPasswordChangeDate")]
|
||||
[Column("lastPasswordChangeDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? LastPasswordChangeDate { get; set; }
|
||||
|
||||
[Column("lastLoginDate")]
|
||||
[Column("lastLoginDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? LastLoginDate { get; set; }
|
||||
|
||||
[Column("emailConfirmedDate")]
|
||||
[Column("emailConfirmedDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? EmailConfirmedDate { get; set; }
|
||||
|
||||
[Column("invitedDate")]
|
||||
[Column("invitedDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? InvitedDate { get; set; }
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; } = DateTime.Now;
|
||||
|
||||
[Column("updateDate")]
|
||||
[Column("updateDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime UpdateDate { get; set; } = DateTime.Now;
|
||||
|
||||
@@ -44,12 +44,12 @@ public class UserGroupDto
|
||||
[Obsolete("Is not used anymore Use UserGroup2PermissionDtos instead. This will be removed in Umbraco 18.")]
|
||||
public string? DefaultPermissions { get; set; }
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
[Column("updateDate")]
|
||||
[Column("updateDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using NPoco;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
|
||||
|
||||
@@ -24,7 +24,7 @@ internal class WebhookLogDto
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
public string StatusCode { get; set; } = string.Empty;
|
||||
|
||||
[Column(Name = "date")]
|
||||
[Column(Name = "date", ForceToUtc = false)]
|
||||
[Index(IndexTypes.NonClustered, Name = "IX_" + Constants.DatabaseSchema.Tables.WebhookLog + "_date")]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
@@ -39,8 +39,11 @@ internal class ContentBaseFactory
|
||||
|
||||
content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId;
|
||||
content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId;
|
||||
content.CreateDate = nodeDto.CreateDate;
|
||||
content.UpdateDate = contentVersionDto.VersionDate;
|
||||
|
||||
// Dates stored in the database are local server time, but for SQL Server, will be considered
|
||||
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
|
||||
content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local);
|
||||
content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local);
|
||||
|
||||
content.Published = dto.Published;
|
||||
content.Edited = dto.Edited;
|
||||
@@ -52,7 +55,7 @@ internal class ContentBaseFactory
|
||||
content.PublishedVersionId = publishedVersionDto.Id;
|
||||
if (dto.Published)
|
||||
{
|
||||
content.PublishDate = publishedVersionDto.ContentVersionDto.VersionDate;
|
||||
content.PublishDate = DateTime.SpecifyKind(publishedVersionDto.ContentVersionDto.VersionDate, DateTimeKind.Local);
|
||||
content.PublishName = publishedVersionDto.ContentVersionDto.Text;
|
||||
content.PublisherId = publishedVersionDto.ContentVersionDto.UserId;
|
||||
}
|
||||
@@ -71,7 +74,7 @@ internal class ContentBaseFactory
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an IMedia item from a dto and content type.
|
||||
/// Builds a Media item from a dto and content type.
|
||||
/// </summary>
|
||||
public static Core.Models.Media BuildEntity(ContentDto dto, IMediaType? contentType)
|
||||
{
|
||||
@@ -97,8 +100,8 @@ internal class ContentBaseFactory
|
||||
|
||||
content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId;
|
||||
content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId;
|
||||
content.CreateDate = nodeDto.CreateDate;
|
||||
content.UpdateDate = contentVersionDto.VersionDate;
|
||||
content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local);
|
||||
content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local);
|
||||
|
||||
// reset dirty initial properties (U4-1946)
|
||||
content.ResetDirtyProperties(false);
|
||||
@@ -111,7 +114,7 @@ internal class ContentBaseFactory
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an IMedia item from a dto and content type.
|
||||
/// Builds a Member item from a dto and member type.
|
||||
/// </summary>
|
||||
public static Member BuildEntity(MemberDto dto, IMemberType? contentType)
|
||||
{
|
||||
@@ -126,7 +129,9 @@ internal class ContentBaseFactory
|
||||
|
||||
content.Id = dto.NodeId;
|
||||
content.SecurityStamp = dto.SecurityStampToken;
|
||||
content.EmailConfirmedDate = dto.EmailConfirmedDate;
|
||||
content.EmailConfirmedDate = dto.EmailConfirmedDate.HasValue
|
||||
? DateTime.SpecifyKind(dto.EmailConfirmedDate.Value, DateTimeKind.Local)
|
||||
: null;
|
||||
content.PasswordConfiguration = dto.PasswordConfig;
|
||||
content.Key = nodeDto.UniqueId;
|
||||
content.VersionId = contentVersionDto.Id;
|
||||
@@ -140,14 +145,20 @@ internal class ContentBaseFactory
|
||||
|
||||
content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId;
|
||||
content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId;
|
||||
content.CreateDate = nodeDto.CreateDate;
|
||||
content.UpdateDate = contentVersionDto.VersionDate;
|
||||
content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local);
|
||||
content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local);
|
||||
content.FailedPasswordAttempts = dto.FailedPasswordAttempts ?? default;
|
||||
content.IsLockedOut = dto.IsLockedOut;
|
||||
content.IsApproved = dto.IsApproved;
|
||||
content.LastLoginDate = dto.LastLoginDate;
|
||||
content.LastLockoutDate = dto.LastLockoutDate;
|
||||
content.LastPasswordChangeDate = dto.LastPasswordChangeDate;
|
||||
content.LastLockoutDate = dto.LastLockoutDate.HasValue
|
||||
? DateTime.SpecifyKind(dto.LastLockoutDate.Value, DateTimeKind.Local)
|
||||
: null;
|
||||
content.LastLoginDate = dto.LastLoginDate.HasValue
|
||||
? DateTime.SpecifyKind(dto.LastLoginDate.Value, DateTimeKind.Local)
|
||||
: null;
|
||||
content.LastPasswordChangeDate = dto.LastPasswordChangeDate.HasValue
|
||||
? DateTime.SpecifyKind(dto.LastPasswordChangeDate.Value, DateTimeKind.Local)
|
||||
: null;
|
||||
|
||||
// reset dirty initial properties (U4-1946)
|
||||
content.ResetDirtyProperties(false);
|
||||
@@ -186,7 +197,7 @@ internal class ContentBaseFactory
|
||||
new ContentScheduleDto
|
||||
{
|
||||
Action = x.Action.ToString(),
|
||||
Date = x.Date,
|
||||
Date = DateTime.SpecifyKind(x.Date, DateTimeKind.Local),
|
||||
NodeId = entity.Id,
|
||||
LanguageId = languageRepository.GetIdByIsoCode(x.Culture, false),
|
||||
Id = x.Id,
|
||||
@@ -261,7 +272,7 @@ internal class ContentBaseFactory
|
||||
UserId = entity.CreatorId,
|
||||
Text = entity.Name,
|
||||
NodeObjectType = objectType,
|
||||
CreateDate = entity.CreateDate,
|
||||
CreateDate = DateTime.SpecifyKind(entity.CreateDate, DateTimeKind.Local),
|
||||
};
|
||||
|
||||
return dto;
|
||||
@@ -275,7 +286,7 @@ internal class ContentBaseFactory
|
||||
{
|
||||
Id = entity.VersionId,
|
||||
NodeId = entity.Id,
|
||||
VersionDate = entity.UpdateDate,
|
||||
VersionDate = DateTime.SpecifyKind(entity.UpdateDate, DateTimeKind.Local),
|
||||
UserId = entity.WriterId,
|
||||
Current = true, // always building the current one
|
||||
Text = entity.Name,
|
||||
|
||||
@@ -39,16 +39,25 @@ internal static class UserFactory
|
||||
user.Language = dto.UserLanguage;
|
||||
user.SecurityStamp = dto.SecurityStampToken;
|
||||
user.FailedPasswordAttempts = dto.FailedLoginAttempts ?? 0;
|
||||
user.LastLockoutDate = dto.LastLockoutDate;
|
||||
user.LastLoginDate = dto.LastLoginDate;
|
||||
user.LastPasswordChangeDate = dto.LastPasswordChangeDate;
|
||||
user.CreateDate = dto.CreateDate;
|
||||
user.UpdateDate = dto.UpdateDate;
|
||||
user.Avatar = dto.Avatar;
|
||||
user.EmailConfirmedDate = dto.EmailConfirmedDate;
|
||||
user.InvitedDate = dto.InvitedDate;
|
||||
user.Kind = (UserKind)dto.Kind;
|
||||
|
||||
// Dates stored in the database are local server time, but for SQL Server, will be considered
|
||||
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
|
||||
user.LastLockoutDate = dto.LastLockoutDate.HasValue
|
||||
? DateTime.SpecifyKind(dto.LastLockoutDate.Value, DateTimeKind.Local)
|
||||
: null;
|
||||
user.LastLoginDate = dto.LastLoginDate.HasValue
|
||||
? DateTime.SpecifyKind(dto.LastLoginDate.Value, DateTimeKind.Local)
|
||||
: null;
|
||||
user.LastPasswordChangeDate = dto.LastPasswordChangeDate.HasValue
|
||||
? DateTime.SpecifyKind(dto.LastPasswordChangeDate.Value, DateTimeKind.Local)
|
||||
: null;
|
||||
user.CreateDate = DateTime.SpecifyKind(dto.CreateDate, DateTimeKind.Local);
|
||||
user.UpdateDate = DateTime.SpecifyKind(dto.UpdateDate, DateTimeKind.Local);
|
||||
|
||||
// reset dirty initial properties (U4-1946)
|
||||
user.ResetDirtyProperties(false);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
|
||||
|
||||
List<LogDto>? dtos = Database.Fetch<LogDto>(sql);
|
||||
|
||||
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, x.Datestamp)).ToList();
|
||||
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, DateTime.SpecifyKind(x.Datestamp, DateTimeKind.Local))).ToList();
|
||||
}
|
||||
|
||||
public void CleanLogs(int maximumAgeOfLogsInMinutes)
|
||||
@@ -104,12 +104,12 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
|
||||
totalRecords = page.TotalItems;
|
||||
|
||||
var items = page.Items.Select(
|
||||
dto => new AuditItem(dto.NodeId, Enum<AuditType>.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp)).ToList();
|
||||
dto => new AuditItem(dto.NodeId, Enum<AuditType>.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, DateTime.SpecifyKind(dto.Datestamp, DateTimeKind.Local))).ToList();
|
||||
|
||||
// map the DateStamp
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
items[i].CreateDate = page.Items[i].Datestamp;
|
||||
items[i].CreateDate = DateTime.SpecifyKind(page.Items[i].Datestamp, DateTimeKind.Local);
|
||||
}
|
||||
|
||||
return items;
|
||||
@@ -149,7 +149,7 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
|
||||
LogDto? dto = Database.First<LogDto>(sql);
|
||||
return dto == null
|
||||
? null
|
||||
: new AuditItem(dto.NodeId, Enum<AuditType>.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp);
|
||||
: new AuditItem(dto.NodeId, Enum<AuditType>.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, DateTime.SpecifyKind(dto.Datestamp, DateTimeKind.Local));
|
||||
}
|
||||
|
||||
protected override IEnumerable<IAuditItem> PerformGetAll(params int[]? ids) => throw new NotImplementedException();
|
||||
@@ -162,7 +162,7 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
|
||||
|
||||
List<LogDto>? dtos = Database.Fetch<LogDto>(sql);
|
||||
|
||||
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, x.Datestamp)).ToList();
|
||||
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, DateTime.SpecifyKind(x.Datestamp, DateTimeKind.Local))).ToList();
|
||||
}
|
||||
|
||||
protected override Sql<ISqlContext> GetBaseQuery(bool isCount)
|
||||
|
||||
@@ -400,15 +400,17 @@ public class DocumentRepository : ContentRepositoryBase<int, IContent, DocumentR
|
||||
{
|
||||
foreach (ContentVariation v in contentVariation)
|
||||
{
|
||||
content.SetCultureInfo(v.Culture, v.Name, v.Date);
|
||||
content.SetCultureInfo(v.Culture, v.Name, DateTime.SpecifyKind(v.Date, DateTimeKind.Local));
|
||||
}
|
||||
}
|
||||
|
||||
// Dates stored in the database are local server time, but for SQL Server, will be considered
|
||||
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
|
||||
if (content.PublishedState is PublishedState.Published && content.PublishedVersionId > 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation))
|
||||
{
|
||||
foreach (ContentVariation v in contentVariation)
|
||||
{
|
||||
content.SetPublishInfo(v.Culture, v.Name, v.Date);
|
||||
content.SetPublishInfo(v.Culture, v.Name, DateTime.SpecifyKind(v.Date, DateTimeKind.Local));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
@@ -1,3 +1,4 @@
|
||||
using System.Data;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
@@ -98,6 +99,16 @@ internal class DocumentVersionRepository : IDocumentVersionRepository
|
||||
Page<ContentVersionMeta>? page =
|
||||
_scopeAccessor.AmbientScope?.Database.Page<ContentVersionMeta>(pageIndex + 1, pageSize, query);
|
||||
|
||||
// Dates stored in the database are local server time, but for SQL Server, will be considered
|
||||
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
|
||||
if (page is not null)
|
||||
{
|
||||
foreach (ContentVersionMeta item in page.Items)
|
||||
{
|
||||
item.SpecifyVersionDateKind(DateTimeKind.Local);
|
||||
}
|
||||
}
|
||||
|
||||
totalRecords = page?.TotalItems ?? 0;
|
||||
|
||||
return page?.Items;
|
||||
|
||||
@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
@@ -263,6 +264,11 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
|
||||
bool canUpdateInvariantData,
|
||||
HashSet<string> allowedCultures)
|
||||
{
|
||||
if (canUpdateInvariantData is false && targetValue is null)
|
||||
{
|
||||
return sourceValue;
|
||||
}
|
||||
|
||||
BlockEditorData<TValue, TLayout>? source = BlockEditorValues.DeserializeAndClean(sourceValue);
|
||||
BlockEditorData<TValue, TLayout>? target = BlockEditorValues.DeserializeAndClean(targetValue);
|
||||
|
||||
@@ -282,31 +288,29 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
|
||||
bool canUpdateInvariantData,
|
||||
HashSet<string> allowedCultures)
|
||||
{
|
||||
source = UpdateSourceInvariantData(source, target, canUpdateInvariantData);
|
||||
var mergedInvariant = UpdateSourceInvariantData(source, target, canUpdateInvariantData);
|
||||
|
||||
if (source is null && target is null)
|
||||
// if the structure (invariant) is not defined after merger, the target content does not matter
|
||||
if (mergedInvariant is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (source is null && target?.Layout is not null)
|
||||
// since we merged the invariant data (layout) before we get to this point
|
||||
// we just need an empty valid object to run comparisons at this point
|
||||
if (source is null)
|
||||
{
|
||||
source = new BlockEditorData<TValue, TLayout>([], CreateWithLayout(target.Layout));
|
||||
}
|
||||
else if (target is null && source?.Layout is not null)
|
||||
{
|
||||
target = new BlockEditorData<TValue, TLayout>([], CreateWithLayout(source.Layout));
|
||||
source = new BlockEditorData<TValue, TLayout>([], new TValue());
|
||||
}
|
||||
|
||||
// at this point the layout should have been merged or fallback created
|
||||
if (source is null || target is null)
|
||||
{
|
||||
throw new ArgumentException("invalid sourceValue or targetValue");
|
||||
}
|
||||
// update the target with the merged invariant
|
||||
target!.BlockValue.Layout = mergedInvariant.BlockValue.Layout;
|
||||
|
||||
// remove all the blocks that are no longer part of the layout
|
||||
target.BlockValue.ContentData.RemoveAll(contentBlock =>
|
||||
target.Layout!.Any(layoutItem => layoutItem.ReferencesContent(contentBlock.Key)) is false);
|
||||
// remove any exposes that no longer have content assigned to them
|
||||
target.BlockValue.Expose.RemoveAll(expose => target.BlockValue.ContentData.Any(data => data.Key == expose.ContentKey) is false);
|
||||
|
||||
target.BlockValue.SettingsData.RemoveAll(settingsBlock =>
|
||||
target.Layout!.Any(layoutItem => layoutItem.ReferencesSetting(settingsBlock.Key)) is false);
|
||||
@@ -314,16 +318,80 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
|
||||
CleanupVariantValues(source.BlockValue.ContentData, target.BlockValue.ContentData, canUpdateInvariantData, allowedCultures);
|
||||
CleanupVariantValues(source.BlockValue.SettingsData, target.BlockValue.SettingsData, canUpdateInvariantData, allowedCultures);
|
||||
|
||||
// every source block value for a culture that is not allowed to be edited should be present on the target
|
||||
RestoreMissingValues(
|
||||
source.BlockValue.ContentData,
|
||||
target.BlockValue.ContentData,
|
||||
mergedInvariant.Layout!,
|
||||
(layoutItem, itemData) => layoutItem.ContentKey == itemData.Key,
|
||||
canUpdateInvariantData,
|
||||
allowedCultures);
|
||||
RestoreMissingValues(
|
||||
source.BlockValue.SettingsData,
|
||||
target.BlockValue.SettingsData,
|
||||
mergedInvariant.Layout!,
|
||||
(layoutItem, itemData) => layoutItem.SettingsKey == itemData.Key,
|
||||
canUpdateInvariantData,
|
||||
allowedCultures);
|
||||
|
||||
// update the expose list from source for any blocks that were restored
|
||||
var missingSourceExposes =
|
||||
source.BlockValue.Expose.Where(sourceExpose =>
|
||||
target.BlockValue.Expose.Any(targetExpose => targetExpose.ContentKey == sourceExpose.ContentKey) is false
|
||||
&& target.BlockValue.ContentData.Any(data => data.Key == sourceExpose.ContentKey)).ToList();
|
||||
foreach (BlockItemVariation missingSourceExpose in missingSourceExposes)
|
||||
{
|
||||
target.BlockValue.Expose.Add(missingSourceExpose);
|
||||
}
|
||||
|
||||
return target.BlockValue;
|
||||
}
|
||||
|
||||
private void RestoreMissingValues(
|
||||
List<BlockItemData> sourceBlockItemData,
|
||||
List<BlockItemData> targetBlockItemData,
|
||||
IEnumerable<TLayout> mergedLayout,
|
||||
Func<TLayout, BlockItemData, bool> relevantBlockItemMatcher,
|
||||
bool canUpdateInvariantData,
|
||||
HashSet<string> allowedCultures)
|
||||
{
|
||||
IEnumerable<BlockItemData> blockItemsToCheck = sourceBlockItemData.Where(itemData =>
|
||||
mergedLayout.Any(layoutItem => relevantBlockItemMatcher(layoutItem, itemData)));
|
||||
foreach (BlockItemData blockItemData in blockItemsToCheck)
|
||||
{
|
||||
var relevantValues = blockItemData.Values.Where(value =>
|
||||
(value.Culture is null && canUpdateInvariantData is false)
|
||||
|| (value.Culture is not null && allowedCultures.Contains(value.Culture) is false)).ToList();
|
||||
if (relevantValues.Count < 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
BlockItemData targetBlockData =
|
||||
targetBlockItemData.FirstOrDefault(itemData => itemData.Key == blockItemData.Key)
|
||||
?? new BlockItemData(blockItemData.Key, blockItemData.ContentTypeKey, blockItemData.ContentTypeAlias);
|
||||
foreach (BlockPropertyValue missingValue in relevantValues.Where(value => targetBlockData.Values.Any(targetValue =>
|
||||
targetValue.Alias == value.Alias
|
||||
&& targetValue.Culture == value.Culture
|
||||
&& targetValue.Segment == value.Segment) is false))
|
||||
{
|
||||
targetBlockData.Values.Add(missingValue);
|
||||
}
|
||||
|
||||
if (targetBlockItemData.Any(existingBlockItemData => existingBlockItemData.Key == targetBlockData.Key) is false)
|
||||
{
|
||||
targetBlockItemData.Add(blockItemData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupVariantValues(
|
||||
List<BlockItemData> sourceBlockItems,
|
||||
List<BlockItemData> targetBlockItems,
|
||||
bool canUpdateInvariantData,
|
||||
HashSet<string> allowedCultures)
|
||||
{
|
||||
// merge the source values into the target values for culture
|
||||
// merge the source values into the target values per culture
|
||||
foreach (BlockItemData targetBlockItem in targetBlockItems)
|
||||
{
|
||||
BlockItemData? sourceBlockItem = sourceBlockItems.FirstOrDefault(i => i.Key == targetBlockItem.Key);
|
||||
|
||||
@@ -190,7 +190,7 @@ public sealed class RichTextEditorPastedImages
|
||||
});
|
||||
}
|
||||
|
||||
img.SetAttributeValue("src", location);
|
||||
img.SetAttributeValue("src", location ?? string.Empty);
|
||||
|
||||
// Remove the data attribute (so we do not re-process this)
|
||||
img.Attributes.Remove(TemporaryImageDataAttribute);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Services;
|
||||
@@ -17,24 +15,17 @@ internal abstract class ContentListViewServiceBase<TContent, TContentType, TCont
|
||||
{
|
||||
private readonly TContentTypeService _contentTypeService;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly ISqlContext _sqlContext;
|
||||
private readonly IContentSearchService<TContent> _contentSearchService;
|
||||
|
||||
protected ContentListViewServiceBase(TContentTypeService contentTypeService, IDataTypeService dataTypeService, ISqlContext sqlContext)
|
||||
protected ContentListViewServiceBase(TContentTypeService contentTypeService, IDataTypeService dataTypeService, IContentSearchService<TContent> contentSearchService)
|
||||
{
|
||||
_contentTypeService = contentTypeService;
|
||||
_dataTypeService = dataTypeService;
|
||||
_sqlContext = sqlContext;
|
||||
_contentSearchService = contentSearchService;
|
||||
}
|
||||
|
||||
protected abstract Guid DefaultListViewKey { get; }
|
||||
|
||||
protected abstract Task<PagedModel<TContent>> GetPagedChildrenAsync(
|
||||
int id,
|
||||
IQuery<TContent>? filter,
|
||||
Ordering? ordering,
|
||||
int skip,
|
||||
int take);
|
||||
|
||||
protected abstract Task<bool> HasAccessToListViewItemAsync(IUser user, Guid key);
|
||||
|
||||
protected async Task<Attempt<ListViewPagedModel<TContent>?, ContentCollectionOperationStatus>> GetListViewResultAsync(
|
||||
@@ -62,7 +53,7 @@ internal abstract class ContentListViewServiceBase<TContent, TContentType, TCont
|
||||
return Attempt.FailWithStatus<ListViewPagedModel<TContent>?, ContentCollectionOperationStatus>(orderingAttempt.Status, null);
|
||||
}
|
||||
|
||||
PagedModel<TContent> items = await GetAllowedListViewItemsAsync(user, content?.Id ?? Constants.System.Root, filter, orderingAttempt.Result, skip, take);
|
||||
PagedModel<TContent> items = await GetAllowedListViewItemsAsync(user, content?.Key, filter, orderingAttempt.Result, skip, take);
|
||||
|
||||
var result = new ListViewPagedModel<TContent>
|
||||
{
|
||||
@@ -216,20 +207,13 @@ internal abstract class ContentListViewServiceBase<TContent, TContentType, TCont
|
||||
return await _dataTypeService.GetAsync(configuredListViewKey);
|
||||
}
|
||||
|
||||
private async Task<PagedModel<TContent>> GetAllowedListViewItemsAsync(IUser user, int contentId, string? filter, Ordering? ordering, int skip, int take)
|
||||
private async Task<PagedModel<TContent>> GetAllowedListViewItemsAsync(IUser user, Guid? contentId, string? filter, Ordering? ordering, int skip, int take)
|
||||
{
|
||||
var queryFilter = ParseQueryFilter(filter);
|
||||
|
||||
var pagedChildren = await GetPagedChildrenAsync(
|
||||
contentId,
|
||||
queryFilter,
|
||||
ordering,
|
||||
skip,
|
||||
take);
|
||||
PagedModel<TContent> pagedChildren = await _contentSearchService.SearchChildrenAsync(filter, contentId, ordering, skip, take);
|
||||
|
||||
// Filtering out child nodes after getting a paged result is an active choice here, even though the pagination might get off.
|
||||
// This has been the case with this functionality in Umbraco for a long time.
|
||||
var items = await FilterItemsBasedOnAccessAsync(user, pagedChildren.Items);
|
||||
IEnumerable<TContent> items = await FilterItemsBasedOnAccessAsync(user, pagedChildren.Items);
|
||||
|
||||
var pagedResult = new PagedModel<TContent>
|
||||
{
|
||||
@@ -240,18 +224,6 @@ internal abstract class ContentListViewServiceBase<TContent, TContentType, TCont
|
||||
return pagedResult;
|
||||
}
|
||||
|
||||
private IQuery<TContent>? ParseQueryFilter(string? filter)
|
||||
{
|
||||
// Adding multiple conditions - considering key (as Guid) & name as filter param
|
||||
Guid.TryParse(filter, out Guid filterAsGuid);
|
||||
|
||||
return filter.IsNullOrWhiteSpace()
|
||||
? null
|
||||
: _sqlContext.Query<TContent>()
|
||||
.Where(c => (c.Name != null && c.Name.Contains(filter)) ||
|
||||
c.Key == filterAsGuid);
|
||||
}
|
||||
|
||||
// TODO: Optimize the way we filter out only the nodes the user is allowed to see - instead of checking one by one
|
||||
private async Task<IEnumerable<TContent>> FilterItemsBasedOnAccessAsync(IUser user, IEnumerable<TContent> items)
|
||||
{
|
||||
|
||||
@@ -21,9 +21,9 @@ internal sealed class ContentListViewService : ContentListViewServiceBase<IConte
|
||||
IContentService contentService,
|
||||
IContentTypeService contentTypeService,
|
||||
IDataTypeService dataTypeService,
|
||||
ISqlContext sqlContext,
|
||||
IContentSearchService contentSearchService,
|
||||
IContentPermissionAuthorizer contentPermissionAuthorizer)
|
||||
: base(contentTypeService, dataTypeService, sqlContext)
|
||||
: base(contentTypeService, dataTypeService, contentSearchService)
|
||||
{
|
||||
_contentService = contentService;
|
||||
_contentPermissionAuthorizer = contentPermissionAuthorizer;
|
||||
@@ -49,32 +49,6 @@ internal sealed class ContentListViewService : ContentListViewServiceBase<IConte
|
||||
return await GetListViewResultAsync(user, content, dataTypeKey, orderBy, orderCulture, orderDirection, filter, skip, take);
|
||||
}
|
||||
|
||||
protected override Task<PagedModel<IContent>> GetPagedChildrenAsync(
|
||||
int id,
|
||||
IQuery<IContent>? filter,
|
||||
Ordering? ordering,
|
||||
int skip,
|
||||
int take)
|
||||
{
|
||||
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize);
|
||||
|
||||
IEnumerable<IContent> items = _contentService.GetPagedChildren(
|
||||
id,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
out var total,
|
||||
filter,
|
||||
ordering);
|
||||
|
||||
var pagedResult = new PagedModel<IContent>
|
||||
{
|
||||
Items = items,
|
||||
Total = total,
|
||||
};
|
||||
|
||||
return Task.FromResult(pagedResult);
|
||||
}
|
||||
|
||||
// We can use an authorizer here, as it already handles all the necessary checks for this filtering.
|
||||
// However, we cannot pass in all the items; we want only the ones that comply, as opposed to
|
||||
// a general response whether the user has access to all nodes.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Services.Implement;
|
||||
|
||||
internal sealed class ContentSearchService : ContentSearchServiceBase<IContent>, IContentSearchService
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
|
||||
public ContentSearchService(ISqlContext sqlContext, IIdKeyMap idKeyMap, ILogger<ContentSearchService> logger, IContentService contentService)
|
||||
: base(sqlContext, idKeyMap, logger)
|
||||
=> _contentService = contentService;
|
||||
|
||||
protected override UmbracoObjectTypes ObjectType => UmbracoObjectTypes.Document;
|
||||
|
||||
protected override Task<IEnumerable<IContent>> SearchChildrenAsync(
|
||||
IQuery<IContent>? query,
|
||||
int parentId,
|
||||
Ordering? ordering,
|
||||
long pageNumber,
|
||||
int pageSize,
|
||||
out long total)
|
||||
=> Task.FromResult(_contentService.GetPagedChildren(
|
||||
parentId,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
out total,
|
||||
query,
|
||||
ordering));
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Services.Implement;
|
||||
|
||||
internal abstract class ContentSearchServiceBase<TContent> : IContentSearchService<TContent>
|
||||
where TContent : class, IContentBase
|
||||
{
|
||||
private readonly ISqlContext _sqlContext;
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
private readonly ILogger<ContentSearchServiceBase<TContent>> _logger;
|
||||
|
||||
protected ContentSearchServiceBase(ISqlContext sqlContext, IIdKeyMap idKeyMap, ILogger<ContentSearchServiceBase<TContent>> logger)
|
||||
{
|
||||
_sqlContext = sqlContext;
|
||||
_idKeyMap = idKeyMap;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected abstract UmbracoObjectTypes ObjectType { get; }
|
||||
|
||||
protected abstract Task<IEnumerable<TContent>> SearchChildrenAsync(
|
||||
IQuery<TContent>? query,
|
||||
int parentId,
|
||||
Ordering? ordering,
|
||||
long pageNumber,
|
||||
int pageSize,
|
||||
out long total);
|
||||
|
||||
public async Task<PagedModel<TContent>> SearchChildrenAsync(
|
||||
string? query,
|
||||
Guid? parentId,
|
||||
Ordering? ordering,
|
||||
int skip = 0,
|
||||
int take = 100)
|
||||
{
|
||||
var parentIdAsInt = Constants.System.Root;
|
||||
if (parentId.HasValue)
|
||||
{
|
||||
Attempt<int> keyToId = _idKeyMap.GetIdForKey(parentId.Value, ObjectType);
|
||||
if (keyToId.Success is false)
|
||||
{
|
||||
_logger.LogWarning("Could not obtain an ID for parent key: {parentId} (object type: {contentType}", parentId, typeof(TContent).FullName);
|
||||
return new PagedModel<TContent>(0, []);
|
||||
}
|
||||
|
||||
parentIdAsInt = keyToId.Result;
|
||||
}
|
||||
|
||||
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize);
|
||||
IQuery<TContent>? contentQuery = ParseQuery(query);
|
||||
|
||||
IEnumerable<TContent> items = await SearchChildrenAsync(contentQuery, parentIdAsInt, ordering, pageNumber, pageSize, out var total);
|
||||
return new PagedModel<TContent>
|
||||
{
|
||||
Items = items,
|
||||
Total = total,
|
||||
};
|
||||
}
|
||||
|
||||
private IQuery<TContent>? ParseQuery(string? query)
|
||||
{
|
||||
// Adding multiple conditions - considering key (as Guid) & name as filter param
|
||||
Guid.TryParse(query, out Guid filterAsGuid);
|
||||
|
||||
return query.IsNullOrWhiteSpace()
|
||||
? null
|
||||
: _sqlContext
|
||||
.Query<TContent>()
|
||||
.Where(c => (c.Name != null && c.Name.Contains(query)) || c.Key == filterAsGuid);
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,17 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
bool ignoreUserStartNodes = false)
|
||||
=> SearchAsync(objectType, query, parentId, contentTypeIds, trashed, skip, take, ignoreUserStartNodes).GetAwaiter().GetResult();
|
||||
|
||||
public Task<PagedModel<IEntitySlim>> SearchAsync(
|
||||
UmbracoObjectTypes objectType,
|
||||
string query,
|
||||
Guid? parentId,
|
||||
IEnumerable<Guid>? contentTypeIds,
|
||||
bool? trashed,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
bool ignoreUserStartNodes = false)
|
||||
{
|
||||
UmbracoEntityTypes entityType = objectType switch
|
||||
{
|
||||
@@ -115,12 +126,12 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService
|
||||
.Where(key => key != Guid.Empty)
|
||||
.ToArray();
|
||||
|
||||
return new PagedModel<IEntitySlim>
|
||||
return Task.FromResult(new PagedModel<IEntitySlim>
|
||||
{
|
||||
Items = keys.Any()
|
||||
? _entityService.GetAll(objectType, keys)
|
||||
: Enumerable.Empty<IEntitySlim>(),
|
||||
Total = totalFound
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Services.Implement;
|
||||
|
||||
@@ -20,9 +18,9 @@ internal sealed class MediaListViewService : ContentListViewServiceBase<IMedia,
|
||||
IMediaService mediaService,
|
||||
IMediaTypeService mediaTypeService,
|
||||
IDataTypeService dataTypeService,
|
||||
ISqlContext sqlContext,
|
||||
IMediaSearchService mediaSearchService,
|
||||
IMediaPermissionAuthorizer mediaPermissionAuthorizer)
|
||||
: base(mediaTypeService, dataTypeService, sqlContext)
|
||||
: base(mediaTypeService, dataTypeService, mediaSearchService)
|
||||
{
|
||||
_mediaService = mediaService;
|
||||
_mediaPermissionAuthorizer = mediaPermissionAuthorizer;
|
||||
@@ -50,27 +48,6 @@ internal sealed class MediaListViewService : ContentListViewServiceBase<IMedia,
|
||||
return await GetListViewResultAsync(user, media, dataTypeKey, orderBy, null, orderDirection, filter, skip, take);
|
||||
}
|
||||
|
||||
protected override Task<PagedModel<IMedia>> GetPagedChildrenAsync(int id, IQuery<IMedia>? filter, Ordering? ordering, int skip, int take)
|
||||
{
|
||||
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize);
|
||||
|
||||
IEnumerable<IMedia> items = _mediaService.GetPagedChildren(
|
||||
id,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
out var total,
|
||||
filter,
|
||||
ordering);
|
||||
|
||||
var pagedResult = new PagedModel<IMedia>
|
||||
{
|
||||
Items = items,
|
||||
Total = total,
|
||||
};
|
||||
|
||||
return Task.FromResult(pagedResult);
|
||||
}
|
||||
|
||||
// We can use an authorizer here, as it already handles all the necessary checks for this filtering.
|
||||
// However, we cannot pass in all the items; we want only the ones that comply, as opposed to
|
||||
// a general response whether the user has access to all nodes.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user