Compare commits
239
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 | ||
|
|
340ae6c474 | ||
|
|
7495c3c7b2 | ||
|
|
f9496e8067 | ||
|
|
7b10d39d66 | ||
|
|
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
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -45,6 +45,7 @@ internal sealed class MediaTypeEditingPresentationFactory : ContentTypeEditingPr
|
||||
|
||||
updateModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedMediaTypes);
|
||||
updateModel.Compositions = MapCompositions(CompositionTypesByKey(requestModel.Compositions));
|
||||
|
||||
updateModel.ListView = requestModel.Collection?.Id;
|
||||
|
||||
return updateModel;
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -84,9 +84,8 @@ public abstract class ContentTypeMapDefinition<TContentType, TPropertyTypeModel,
|
||||
: CompositionType.Composition;
|
||||
|
||||
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();
|
||||
=> directCompositions
|
||||
.Select(composition => contentTypeCompositionFactory(
|
||||
new ReferenceByIdModel(composition.Key),
|
||||
CalculateCompositionType(contentTypeParentId, composition))).ToArray();
|
||||
}
|
||||
|
||||
@@ -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,6 +33,7 @@ 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;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -325,8 +325,8 @@ internal abstract class ContentTypeEditingServiceBase<TContentType, TContentType
|
||||
// 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))
|
||||
.Select(c => c.Key)
|
||||
.ContainsAll(compositionKeys))
|
||||
{
|
||||
return ContentTypeOperationStatus.Success;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 MediaSearchService : ContentSearchServiceBase<IMedia>, IMediaSearchService
|
||||
{
|
||||
private readonly IMediaService _mediaService;
|
||||
|
||||
public MediaSearchService(ISqlContext sqlContext, IIdKeyMap idKeyMap, ILogger<MediaSearchService> logger, IMediaService mediaService)
|
||||
: base(sqlContext, idKeyMap, logger)
|
||||
=> _mediaService = mediaService;
|
||||
|
||||
protected override UmbracoObjectTypes ObjectType => UmbracoObjectTypes.Media;
|
||||
|
||||
protected override Task<IEnumerable<IMedia>> SearchChildrenAsync(
|
||||
IQuery<IMedia>? query,
|
||||
int parentId,
|
||||
Ordering? ordering,
|
||||
long pageNumber,
|
||||
int pageSize,
|
||||
out long total)
|
||||
=> Task.FromResult(_mediaService.GetPagedChildren(
|
||||
parentId,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
out total,
|
||||
query,
|
||||
ordering));
|
||||
}
|
||||
@@ -77,12 +77,16 @@ internal sealed class MemberEditingService : IMemberEditingService
|
||||
return Attempt.FailWithStatus(status, new MemberCreateResult());
|
||||
}
|
||||
|
||||
// this should be validated already so it's OK to throw an exception here
|
||||
var memberName = createModel.Variants.FirstOrDefault(v => v.Culture is null && v.Segment is null)?.Name
|
||||
?? throw new ArgumentException("Expected an invariant variant for the member name.", nameof(createModel));
|
||||
|
||||
var identityMember = MemberIdentityUser.CreateNew(
|
||||
createModel.Username,
|
||||
createModel.Email,
|
||||
memberType.Alias,
|
||||
createModel.IsApproved,
|
||||
createModel.InvariantName,
|
||||
memberName,
|
||||
createModel.Key);
|
||||
|
||||
IdentityResult createResult = await _memberManager.CreateAsync(identityMember, createModel.Password);
|
||||
@@ -219,7 +223,7 @@ internal sealed class MemberEditingService : IMemberEditingService
|
||||
|
||||
private async Task<MemberEditingOperationStatus> ValidateMemberDataAsync(MemberEditingModelBase model, Guid? memberKey, string? password)
|
||||
{
|
||||
if (model.InvariantName.IsNullOrWhiteSpace())
|
||||
if (model.Variants.FirstOrDefault(v => v.Culture is null && v.Segment is null)?.Name.IsNullOrWhiteSpace() is not false)
|
||||
{
|
||||
return MemberEditingOperationStatus.InvalidName;
|
||||
}
|
||||
|
||||
+2
-27
@@ -25,65 +25,40 @@ internal sealed class CacheRefreshingNotificationHandler :
|
||||
{
|
||||
private readonly IDocumentCacheService _documentCacheService;
|
||||
private readonly IMediaCacheService _mediaCacheService;
|
||||
private readonly IElementsCache _elementsCache;
|
||||
private readonly IRelationService _relationService;
|
||||
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
|
||||
|
||||
public CacheRefreshingNotificationHandler(
|
||||
IDocumentCacheService documentCacheService,
|
||||
IMediaCacheService mediaCacheService,
|
||||
IElementsCache elementsCache,
|
||||
IRelationService relationService,
|
||||
IPublishedContentTypeCache publishedContentTypeCache)
|
||||
{
|
||||
_documentCacheService = documentCacheService;
|
||||
_mediaCacheService = mediaCacheService;
|
||||
_elementsCache = elementsCache;
|
||||
_relationService = relationService;
|
||||
_publishedContentTypeCache = publishedContentTypeCache;
|
||||
}
|
||||
|
||||
public async Task HandleAsync(ContentRefreshNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
ClearElementsCache();
|
||||
|
||||
await _documentCacheService.RefreshContentAsync(notification.Entity);
|
||||
}
|
||||
=> await _documentCacheService.RefreshContentAsync(notification.Entity);
|
||||
|
||||
public async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (IContent deletedEntity in notification.DeletedEntities)
|
||||
{
|
||||
ClearElementsCache();
|
||||
await _documentCacheService.DeleteItemAsync(deletedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandleAsync(MediaRefreshNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
ClearElementsCache();
|
||||
await _mediaCacheService.RefreshMediaAsync(notification.Entity);
|
||||
}
|
||||
=> await _mediaCacheService.RefreshMediaAsync(notification.Entity);
|
||||
|
||||
public async Task HandleAsync(MediaDeletedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (IMedia deletedEntity in notification.DeletedEntities)
|
||||
{
|
||||
ClearElementsCache();
|
||||
await _mediaCacheService.DeleteItemAsync(deletedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearElementsCache()
|
||||
{
|
||||
// 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 lives exclusively in a JSON blob in the umbracoPropertyData table.
|
||||
// This means that the only way to resolve these keys are to actually parse this data with a specific value converter, and for all cultures, which is not feasible.
|
||||
// If published elements become their own entities with relations, instead of just property data, we can revisit this,
|
||||
_elementsCache.Clear();
|
||||
}
|
||||
|
||||
public Task HandleAsync(ContentTypeRefreshedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
const ContentTypeChangeTypes types // only for those that have been refreshed
|
||||
|
||||
@@ -123,72 +123,6 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
|
||||
|
||||
}
|
||||
|
||||
// assumes content tree lock
|
||||
public bool VerifyContentDbCache()
|
||||
{
|
||||
// every document should have a corresponding row for edited properties
|
||||
// and if published, may have a corresponding row for published properties
|
||||
Guid contentObjectType = Constants.ObjectTypes.Document;
|
||||
|
||||
var count = Database.ExecuteScalar<int>(
|
||||
$@"SELECT COUNT(*)
|
||||
FROM umbracoNode
|
||||
JOIN {Constants.DatabaseSchema.Tables.Document} ON umbracoNode.id={Constants.DatabaseSchema.Tables.Document}.nodeId
|
||||
LEFT JOIN cmsContentNu nuEdited ON (umbracoNode.id=nuEdited.nodeId AND nuEdited.published=0)
|
||||
LEFT JOIN cmsContentNu nuPublished ON (umbracoNode.id=nuPublished.nodeId AND nuPublished.published=1)
|
||||
WHERE umbracoNode.nodeObjectType=@objType
|
||||
AND nuEdited.nodeId IS NULL OR ({Constants.DatabaseSchema.Tables.Document}.published=1 AND nuPublished.nodeId IS NULL);",
|
||||
new { objType = contentObjectType });
|
||||
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
// assumes media tree lock
|
||||
public bool VerifyMediaDbCache()
|
||||
{
|
||||
// every media item should have a corresponding row for edited properties
|
||||
Guid mediaObjectType = Constants.ObjectTypes.Media;
|
||||
|
||||
var count = Database.ExecuteScalar<int>(
|
||||
@"SELECT COUNT(*)
|
||||
FROM umbracoNode
|
||||
LEFT JOIN cmsContentNu ON (umbracoNode.id=cmsContentNu.nodeId AND cmsContentNu.published=0)
|
||||
WHERE umbracoNode.nodeObjectType=@objType
|
||||
AND cmsContentNu.nodeId IS NULL
|
||||
",
|
||||
new { objType = mediaObjectType });
|
||||
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Guid>> GetContentKeysAsync(Guid nodeObjectType)
|
||||
{
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<NodeDto>(x => x.UniqueId)
|
||||
.From<NodeDto>()
|
||||
.Where<NodeDto>(x => x.NodeObjectType == nodeObjectType);
|
||||
|
||||
return await Database.FetchAsync<Guid>(sql);
|
||||
}
|
||||
|
||||
// assumes member tree lock
|
||||
public bool VerifyMemberDbCache()
|
||||
{
|
||||
// every member item should have a corresponding row for edited properties
|
||||
Guid memberObjectType = Constants.ObjectTypes.Member;
|
||||
|
||||
var count = Database.ExecuteScalar<int>(
|
||||
@"SELECT COUNT(*)
|
||||
FROM umbracoNode
|
||||
LEFT JOIN cmsContentNu ON (umbracoNode.id=cmsContentNu.nodeId AND cmsContentNu.published=0)
|
||||
WHERE umbracoNode.nodeObjectType=@objType
|
||||
AND cmsContentNu.nodeId IS NULL
|
||||
",
|
||||
new { objType = memberObjectType });
|
||||
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
public async Task<ContentCacheNode?> GetContentSourceAsync(Guid key, bool preview = false)
|
||||
{
|
||||
Sql<ISqlContext>? sql = SqlContentSourcesSelect()
|
||||
|
||||
@@ -52,17 +52,4 @@ internal interface IDatabaseCacheRepository
|
||||
IReadOnlyCollection<int>? contentTypeIds = null,
|
||||
IReadOnlyCollection<int>? mediaTypeIds = null,
|
||||
IReadOnlyCollection<int>? memberTypeIds = null);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the content cache by asserting that every document should have a corresponding row for edited properties and if published,
|
||||
/// may have a corresponding row for published properties
|
||||
/// </summary>
|
||||
bool VerifyContentDbCache();
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the caches for content, media and/or members based on the content type ids specified
|
||||
/// </summary>
|
||||
bool VerifyMediaDbCache();
|
||||
|
||||
Task<IEnumerable<Guid>> GetContentKeysAsync(Guid nodeObjectType);
|
||||
}
|
||||
|
||||
@@ -124,7 +124,8 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
scope.Complete();
|
||||
return contentCacheNode;
|
||||
},
|
||||
GetEntryOptions(key, preview));
|
||||
GetEntryOptions(key, preview),
|
||||
GenerateTags(key));
|
||||
|
||||
// We don't want to cache removed items, this may cause issues if the L2 serializer changes.
|
||||
if (contentCacheNode is null)
|
||||
@@ -158,10 +159,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool GetPreview()
|
||||
{
|
||||
return _previewService.IsInPreview();
|
||||
}
|
||||
private bool GetPreview() => _previewService.IsInPreview();
|
||||
|
||||
public IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType)
|
||||
{
|
||||
@@ -176,28 +174,10 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
|
||||
public async Task ClearMemoryCacheAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: This should be done with tags, however this is not implemented yet, so for now we have to naively get all content keys and clear them all.
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
||||
|
||||
// We have to get ALL document keys in order to be able to remove them from the cache,
|
||||
IEnumerable<Guid> documentKeys = await _databaseCacheRepository.GetContentKeysAsync(Constants.ObjectTypes.Document);
|
||||
|
||||
foreach (Guid documentKey in documentKeys)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// We'll remove both the draft and published cache
|
||||
await _hybridCache.RemoveAsync(GetCacheKey(documentKey, false), cancellationToken);
|
||||
await _hybridCache.RemoveAsync(GetCacheKey(documentKey, true), cancellationToken);
|
||||
}
|
||||
await _hybridCache.RemoveByTagAsync(Constants.Cache.Tags.Content, cancellationToken);
|
||||
|
||||
// We have to run seeding again after the cache is cleared
|
||||
await SeedAsync(cancellationToken);
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
public async Task RefreshMemoryCacheAsync(Guid key)
|
||||
@@ -207,13 +187,13 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
ContentCacheNode? draftNode = await _databaseCacheRepository.GetContentSourceAsync(key, true);
|
||||
if (draftNode is not null)
|
||||
{
|
||||
await _hybridCache.SetAsync(GetCacheKey(draftNode.Key, true), draftNode, GetEntryOptions(draftNode.Key, true));
|
||||
await _hybridCache.SetAsync(GetCacheKey(draftNode.Key, true), draftNode, GetEntryOptions(draftNode.Key, true), GenerateTags(key));
|
||||
}
|
||||
|
||||
ContentCacheNode? publishedNode = await _databaseCacheRepository.GetContentSourceAsync(key, false);
|
||||
if (publishedNode is not null && HasPublishedAncestorPath(publishedNode.Key))
|
||||
{
|
||||
await _hybridCache.SetAsync(GetCacheKey(publishedNode.Key, false), publishedNode, GetEntryOptions(publishedNode.Key, false));
|
||||
await _hybridCache.SetAsync(GetCacheKey(publishedNode.Key, false), publishedNode, GetEntryOptions(publishedNode.Key, false), GenerateTags(key));
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
@@ -229,35 +209,37 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
{
|
||||
foreach (Guid key in SeedKeys)
|
||||
{
|
||||
if(cancellationToken.IsCancellationRequested)
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var cacheKey = GetCacheKey(key, false);
|
||||
|
||||
// We'll use GetOrCreateAsync because it may be in the second level cache, in which case we don't have to re-seed.
|
||||
ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync<ContentCacheNode?>(
|
||||
cacheKey,
|
||||
async cancel =>
|
||||
// We'll use GetOrCreateAsync because it may be in the second level cache, in which case we don't have to re-seed.
|
||||
ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync(
|
||||
cacheKey,
|
||||
async cancel =>
|
||||
{
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
||||
|
||||
ContentCacheNode? cacheNode = await _databaseCacheRepository.GetContentSourceAsync(key);
|
||||
|
||||
scope.Complete();
|
||||
|
||||
// We don't want to seed drafts
|
||||
if (cacheNode is null || cacheNode.IsDraft)
|
||||
{
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
||||
return null;
|
||||
}
|
||||
|
||||
ContentCacheNode? cacheNode = await _databaseCacheRepository.GetContentSourceAsync(key, false);
|
||||
return cacheNode;
|
||||
},
|
||||
GetSeedEntryOptions(),
|
||||
GenerateTags(key),
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
scope.Complete();
|
||||
// We don't want to seed drafts
|
||||
if (cacheNode is null || cacheNode.IsDraft)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return cacheNode;
|
||||
},
|
||||
GetSeedEntryOptions(),
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
// If the value is null, it's likely because
|
||||
// If the value is null, it's likely because
|
||||
if (cachedValue is null)
|
||||
{
|
||||
await _hybridCache.RemoveAsync(cacheKey, cancellationToken);
|
||||
@@ -290,7 +272,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
|
||||
public async Task<bool> HasContentByIdAsync(int id, bool preview = false)
|
||||
{
|
||||
Attempt<Guid> keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document);
|
||||
Attempt<Guid> keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document);
|
||||
if (keyAttempt.Success is false)
|
||||
{
|
||||
return false;
|
||||
@@ -315,7 +297,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
// Always set draft node
|
||||
// We have nodes seperate in the cache, cause 99% of the time, you are only using one
|
||||
// and thus we won't get too much data when retrieving from the cache.
|
||||
ContentCacheNode draftCacheNode = _cacheNodeFactory.ToContentCacheNode(content, true);
|
||||
var draftCacheNode = _cacheNodeFactory.ToContentCacheNode(content, true);
|
||||
|
||||
await _databaseCacheRepository.RefreshContentAsync(draftCacheNode, content.PublishedState);
|
||||
|
||||
@@ -329,13 +311,17 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
{
|
||||
await _hybridCache.RemoveAsync(GetCacheKey(publishedCacheNode.Key, false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
|
||||
private static string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
|
||||
|
||||
// Generates the cache tags for a given CacheNode
|
||||
// We use the tags to be able to clear all cache entries that are related to a given content item.
|
||||
// Tags for now are only content/media, but can be expanded with draft/published later.
|
||||
private static HashSet<string> GenerateTags(Guid? key) => key is null ? [] : [Constants.Cache.Tags.Content];
|
||||
|
||||
public async Task DeleteItemAsync(IContentBase content)
|
||||
{
|
||||
@@ -368,6 +354,5 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
_hybridCache.RemoveAsync(GetCacheKey(content.Key, false)).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,9 @@ internal class MediaCacheService : IMediaCacheService
|
||||
ContentCacheNode? mediaCacheNode = await _databaseCacheRepository.GetMediaSourceAsync(key);
|
||||
scope.Complete();
|
||||
return mediaCacheNode;
|
||||
}, GetEntryOptions(key));
|
||||
},
|
||||
GetEntryOptions(key),
|
||||
GenerateTags(key));
|
||||
|
||||
// We don't want to cache removed items, this may cause issues if the L2 serializer changes.
|
||||
if (contentCacheNode is null)
|
||||
@@ -139,9 +141,6 @@ internal class MediaCacheService : IMediaCacheService
|
||||
public async Task RefreshMediaAsync(IMedia media)
|
||||
{
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
||||
// Always set draft node
|
||||
// We have nodes seperate in the cache, cause 99% of the time, you are only using one
|
||||
// and thus we won't get too much data when retrieving from the cache.
|
||||
var cacheNode = _cacheNodeFactory.ToContentCacheNode(media);
|
||||
await _databaseCacheRepository.RefreshMediaAsync(cacheNode);
|
||||
scope.Complete();
|
||||
@@ -156,7 +155,6 @@ internal class MediaCacheService : IMediaCacheService
|
||||
|
||||
public async Task SeedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
foreach (Guid key in SeedKeys)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
@@ -166,7 +164,7 @@ internal class MediaCacheService : IMediaCacheService
|
||||
|
||||
var cacheKey = GetCacheKey(key, false);
|
||||
|
||||
ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync<ContentCacheNode?>(
|
||||
ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync(
|
||||
cacheKey,
|
||||
async cancel =>
|
||||
{
|
||||
@@ -175,7 +173,9 @@ internal class MediaCacheService : IMediaCacheService
|
||||
scope.Complete();
|
||||
return mediaCacheNode;
|
||||
},
|
||||
GetSeedEntryOptions());
|
||||
GetSeedEntryOptions(),
|
||||
GenerateTags(key),
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
if (cachedValue is null)
|
||||
{
|
||||
@@ -199,27 +199,10 @@ internal class MediaCacheService : IMediaCacheService
|
||||
|
||||
public async Task ClearMemoryCacheAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: This should be done with tags, however this is not implemented yet, so for now we have to naively get all content keys and clear them all.
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
||||
|
||||
// We have to get ALL document keys in order to be able to remove them from the cache,
|
||||
IEnumerable<Guid> documentKeys = await _databaseCacheRepository.GetContentKeysAsync(Constants.ObjectTypes.Media);
|
||||
|
||||
foreach (Guid documentKey in documentKeys)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// We'll remove both the draft and published cache
|
||||
await _hybridCache.RemoveAsync(GetCacheKey(documentKey, false), cancellationToken);
|
||||
}
|
||||
await _hybridCache.RemoveByTagAsync(Constants.Cache.Tags.Media, cancellationToken);
|
||||
|
||||
// We have to run seeding again after the cache is cleared
|
||||
await SeedAsync(cancellationToken);
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
public async Task RemoveFromMemoryCacheAsync(Guid key)
|
||||
@@ -240,6 +223,7 @@ internal class MediaCacheService : IMediaCacheService
|
||||
_hybridCache.RemoveAsync(GetCacheKey(content.Key, false)).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
@@ -295,5 +279,10 @@ internal class MediaCacheService : IMediaCacheService
|
||||
LocalCacheExpiration = _cacheSettings.Entry.Media.SeedCacheDuration,
|
||||
};
|
||||
|
||||
private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
|
||||
private static string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
|
||||
|
||||
// Generates the cache tags for a given CacheNode
|
||||
// We use the tags to be able to clear all cache entries that are related to a given content item.
|
||||
// Tags for now are only content/media, but can be expanded with draft/published later.
|
||||
private static HashSet<string> GenerateTags(Guid? key) => key is null ? [] : [Constants.Cache.Tags.Media];
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ public static class ServiceCollectionExtensions
|
||||
LoggingSettings loggerSettings = GetLoggerSettings(configuration);
|
||||
|
||||
var loggingDir = loggerSettings.GetAbsoluteLoggingPath(hostEnvironment);
|
||||
ILoggingConfiguration loggingConfig = new LoggingConfiguration(loggingDir);
|
||||
ILoggingConfiguration loggingConfig = new LoggingConfiguration(loggingDir, loggerSettings.FileNameFormat, loggerSettings.FileNameFormatArguments);
|
||||
|
||||
var umbracoFileConfiguration = new UmbracoFileConfiguration(configuration);
|
||||
|
||||
|
||||
@@ -30,13 +30,13 @@ public sealed class HtmlStringUtilities
|
||||
|
||||
public HtmlString StripHtmlTags(string html, params string[]? tags)
|
||||
{
|
||||
HtmlDocument doc = new HtmlDocument();
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(html);
|
||||
|
||||
List<HtmlNode> targets = new List<HtmlNode>();
|
||||
HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes(".//*");
|
||||
var targets = new List<HtmlNode>();
|
||||
HtmlNodeCollection? nodes = doc.DocumentNode.SelectNodes(".//*");
|
||||
|
||||
if (nodes != null)
|
||||
if (nodes is not null)
|
||||
{
|
||||
foreach (HtmlNode node in nodes)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ import { join } from 'path';
|
||||
//const __dirname = import.meta.dirname;
|
||||
|
||||
// Adjust this number as needed.
|
||||
const MAX_CIRCULAR_DEPENDENCIES = 5;
|
||||
const MAX_CIRCULAR_DEPENDENCIES = 1;
|
||||
|
||||
const IS_GITHUB_ACTIONS = process.env.GITHUB_ACTIONS === 'true';
|
||||
const IS_AZURE_PIPELINES = process.env.TF_BUILD === 'true';
|
||||
|
||||
@@ -2,8 +2,9 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { createImportMap } from '../importmap/index.js';
|
||||
|
||||
const ILLEGAL_CORE_IMPORTS_THRESHOLD = 7;
|
||||
const SELF_IMPORTS_THRESHOLD = 12;
|
||||
const ILLEGAL_CORE_IMPORTS_THRESHOLD = 5;
|
||||
const SELF_IMPORTS_THRESHOLD = 0;
|
||||
const BIDIRECTIONAL_IMPORTS_THRESHOLD = 18;
|
||||
|
||||
const clientProjectRoot = path.resolve(import.meta.dirname, '../../');
|
||||
const modulePrefix = '@umbraco-cms/backoffice/';
|
||||
@@ -51,10 +52,15 @@ function getImportsInFile(filePath) {
|
||||
if (!['.ts'].includes(ext)) return [];
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// remove all comments from the content
|
||||
const regex = /\/\/.*|\/\*[\s\S]*?\*\//gm;
|
||||
const cleanedContent = content.replace(regex, '');
|
||||
|
||||
const imports = [];
|
||||
|
||||
let match;
|
||||
while ((match = importRegex.exec(content)) !== null) {
|
||||
while ((match = importRegex.exec(cleanedContent)) !== null) {
|
||||
imports.push({ type: 'import', value: match[1] });
|
||||
}
|
||||
|
||||
@@ -136,8 +142,12 @@ function reportIllegalImportsFromCore() {
|
||||
throw new Error(
|
||||
`Illegal imports found in ${total} core modules. ${total - ILLEGAL_CORE_IMPORTS_THRESHOLD} more than the threshold.`,
|
||||
);
|
||||
} else if (total === 0) {
|
||||
console.log(`✅ Success! No illegal imports found.`);
|
||||
} else {
|
||||
console.log(`✅ Success! Still under the threshold of ${ILLEGAL_CORE_IMPORTS_THRESHOLD} illegal imports. `);
|
||||
console.log(
|
||||
`✅ Success! Still (${total}) under the threshold of ${ILLEGAL_CORE_IMPORTS_THRESHOLD} illegal imports. `,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`\n\n`);
|
||||
@@ -160,17 +170,62 @@ function reportSelfImportsFromModules() {
|
||||
|
||||
// If there are self imports, log them
|
||||
console.error(`🚨 ${alias} is importing itself`);
|
||||
console.log(`\n`);
|
||||
total++;
|
||||
});
|
||||
|
||||
console.log(`\n`);
|
||||
|
||||
if (total > SELF_IMPORTS_THRESHOLD) {
|
||||
throw new Error(
|
||||
`Self imports found in ${total} modules. ${total - SELF_IMPORTS_THRESHOLD} more than the threshold.`,
|
||||
);
|
||||
} else if (total === 0) {
|
||||
console.log(`✅ Success! No self imports found.`);
|
||||
} else {
|
||||
console.log(`✅ Success! Still under the threshold of ${SELF_IMPORTS_THRESHOLD} self imports.`);
|
||||
console.log(`✅ Success! Still (${total}) under the threshold of ${SELF_IMPORTS_THRESHOLD} self imports.`);
|
||||
}
|
||||
|
||||
console.log(`\n\n`);
|
||||
}
|
||||
|
||||
function reportBidirectionalModuleImports() {
|
||||
console.error(`🔍 Scanning all modules for bidirectional imports...`);
|
||||
console.log(`\n`);
|
||||
|
||||
let entries = [];
|
||||
|
||||
packageModules.forEach(([alias, path]) => {
|
||||
const importsInModule = getUmbracoModuleImportsInModule(alias);
|
||||
|
||||
// Check imports for all the modules
|
||||
importsInModule.forEach((importedModule) => {
|
||||
// Check if the imported module imports the current module
|
||||
const importedModuleImports = getUmbracoModuleImportsInModule(importedModule);
|
||||
if (importedModuleImports.includes(alias)) {
|
||||
const entry = [alias, importedModule].sort();
|
||||
entries = [...entries, entry];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Remove duplicates
|
||||
const uniqueEntries = [...new Set(entries.map(JSON.stringify))].map(JSON.parse);
|
||||
const total = uniqueEntries.length;
|
||||
|
||||
uniqueEntries.forEach((entry) => {
|
||||
const [moduleA, moduleB] = entry;
|
||||
console.error(`🚨 ${moduleA} and ${moduleB} are importing each other`);
|
||||
});
|
||||
|
||||
if (total > BIDIRECTIONAL_IMPORTS_THRESHOLD) {
|
||||
throw new Error(
|
||||
`Bidirectional imports found in ${total} modules. ${total - BIDIRECTIONAL_IMPORTS_THRESHOLD} more than the threshold.`,
|
||||
);
|
||||
} else if (total === 0) {
|
||||
console.log(`✅ Success! No bidirectional imports found.`);
|
||||
} else {
|
||||
console.log(
|
||||
`✅ Success! Still (${total}) under the threshold of ${BIDIRECTIONAL_IMPORTS_THRESHOLD} bidirectional imports.`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`\n\n`);
|
||||
@@ -179,6 +234,7 @@ function reportSelfImportsFromModules() {
|
||||
function report() {
|
||||
reportIllegalImportsFromCore();
|
||||
reportSelfImportsFromModules();
|
||||
reportBidirectionalModuleImports();
|
||||
}
|
||||
|
||||
report();
|
||||
@@ -186,4 +242,3 @@ report();
|
||||
// TODO:
|
||||
// - Check what packages another package depends on (not modules) - This will be used when we split the tsconfig into multiple configs
|
||||
// - Check for circular module imports
|
||||
// - Report if a module imports itself
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Custom Validation Workspace Context Example
|
||||
|
||||
This example demonstrates how you can append extra validation messages to the Validation Context based on the value of a Property of a property Editor that this Customization is not interested in overriding/replacing.
|
||||
|
||||
See this having an effect on the All RTEs page, where too long words are invalid.
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
|
||||
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UMB_CONTENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content';
|
||||
import {
|
||||
UMB_VALIDATION_CONTEXT,
|
||||
UmbDataPathPropertyValueQuery,
|
||||
type UmbValidator,
|
||||
} from '@umbraco-cms/backoffice/validation';
|
||||
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
|
||||
import type { UmbPropertyEditorRteValueType } from '@umbraco-cms/backoffice/rte';
|
||||
|
||||
// The Example Workspace Context Controller:
|
||||
export class CustomValidationValidator extends UmbControllerBase implements UmbValidator {
|
||||
//
|
||||
#validationContext?: typeof UMB_VALIDATION_CONTEXT.TYPE;
|
||||
#workspaceContext?: typeof UMB_CONTENT_WORKSPACE_CONTEXT.TYPE;
|
||||
|
||||
#propertyAlias: string;
|
||||
#variantId?: UmbVariantId;
|
||||
#dataPath: string;
|
||||
#isValid: boolean = false;
|
||||
#value?: UmbPropertyEditorRteValueType;
|
||||
|
||||
constructor(host: UmbControllerHost, propertyAlias: string, variantId?: UmbVariantId) {
|
||||
super(host);
|
||||
this.#propertyAlias = propertyAlias;
|
||||
this.#variantId = variantId;
|
||||
this.#dataPath = `$.values[${UmbDataPathPropertyValueQuery({
|
||||
alias: this.#propertyAlias,
|
||||
culture: this.#variantId?.culture ?? null,
|
||||
segment: this.#variantId?.segment ?? null,
|
||||
})}].value`;
|
||||
|
||||
this.consumeContext(UMB_VALIDATION_CONTEXT, (context) => {
|
||||
this.#validationContext = context;
|
||||
this.#checkValidation();
|
||||
});
|
||||
|
||||
this.consumeContext(UMB_CONTENT_WORKSPACE_CONTEXT, async (context) => {
|
||||
this.#workspaceContext = context;
|
||||
this.observe(
|
||||
await context?.propertyValueByAlias<UmbPropertyEditorRteValueType>(propertyAlias, this.#variantId),
|
||||
(value) => {
|
||||
this.#value = value;
|
||||
this.#checkValidation();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#checkValidation() {
|
||||
// Check validation:
|
||||
if (this.#value?.markup) {
|
||||
const cleanedMarkup = this.#value.markup?.replace(/<\/?[^>]+(>|$)/g, '');
|
||||
// Check if markup contains words longer than 6 characters:
|
||||
const words = cleanedMarkup.split(/\s+/);
|
||||
const invalidWords = words.filter((word) => word.length > 8);
|
||||
if (invalidWords.length === 0) {
|
||||
this.#isValid = true;
|
||||
} else {
|
||||
this.#isValid = false;
|
||||
}
|
||||
} else {
|
||||
// Notice how we say the validation is valid if the value is empty.
|
||||
// If you end up making a validation rule that requires the value to be present,
|
||||
// then still make this Validator approve when empty,
|
||||
// and then mark the property to be Mandatory so that Validation will enforce the value to be filled in,
|
||||
// and then your Validator can be used to check the value it is present.
|
||||
// — In this way you avoid overwhelming the user with validation messages before the user even entered a value.
|
||||
this.#isValid = true;
|
||||
}
|
||||
|
||||
// Update validation message:
|
||||
if (this.#validationContext && this.#workspaceContext) {
|
||||
if (this.#isValid) {
|
||||
this.#validationContext.messages.removeMessagesByTypeAndPath('custom', this.#dataPath);
|
||||
} else {
|
||||
this.#validationContext.messages.addMessage(
|
||||
'custom',
|
||||
this.#dataPath,
|
||||
'Custom validation says this contains too long words',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async validate(): Promise<void> {
|
||||
// Validate is called when the validation state of this validator is asked to be fully resolved. Like when user clicks Save & Publish.
|
||||
// If you need to ask the server then it could be done here, instead of asking the server each time the value changes.
|
||||
// In this particular example we do not need to do anything, because our validation is represented via a message that we always set no matter the user interaction.
|
||||
// If we did not like to only to check the Validation State when absolute needed then this method must be implemented.
|
||||
}
|
||||
|
||||
get isValid(): boolean {
|
||||
return this.#isValid;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.#isValid = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus the first invalid element.
|
||||
*/
|
||||
focusFirstInvalidElement(): void {
|
||||
alert('custom validation is invalid, you should implement a feature to focus the problematic element');
|
||||
}
|
||||
|
||||
override destroy(): void {
|
||||
this.#validationContext = undefined;
|
||||
this.#workspaceContext = undefined;
|
||||
this.#value = undefined;
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Declare a api export, so Extension Registry can initialize this class:
|
||||
export const api = CustomValidationValidator;
|
||||
|
||||
// Declare a Context Token, to other can request the context and to ensure typings.
|
||||
export const EXAMPLE_CUSTOM_VALIDATION_CONTEXT = new UmbContextToken<CustomValidationValidator>(
|
||||
'UmbWorkspaceContext',
|
||||
'example.workspaceContext.counter',
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user